view api/Factories/Xml/WarFoundryXmlFactory.cs @ 49:9d31d063b194

Re #10 - Refactor source code for readability * Add SelectSingleElement method that checks type and casts return as XmlElement Also: * Delete code to staged load Army and add TODO
author IBBoard <dev@ibboard.co.uk>
date Sat, 28 Mar 2009 16:45:24 +0000
parents b49372dd8afa
children bb6b993b98bf
line wrap: on
line source

// This file (WarFoundryXmlFactory.cs) is a part of the IBBoard.WarFoundry.API project and is copyright 2009 IBBoard.
//
// The file and the library/program it is in are licensed under the GNU LGPL license, either version 3 of the License or (at your option) any later version. Please see COPYING.LGPL for more information and the full license.

using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using System.Collections.Generic;
using System.Text;
using IBBoard;
using IBBoard.IO;
using IBBoard.Lang;
using IBBoard.Logging;
using IBBoard.Xml;
using IBBoard.WarFoundry.API.Requirements;
using IBBoard.WarFoundry.API.Objects;
using ICSharpCode.SharpZipLib.Zip;

namespace IBBoard.WarFoundry.API.Factories.Xml
{
	/// <summary>
	/// The WarFoundryXmlFactory loads WarFoundry classes from the native "XML in a zip" file format. Files are validated using the schema for the file type, so structurally invalid files should be identified at initial load.
	/// </summary>
	public class WarFoundryXmlFactory : AbstractNativeWarFoundryFactory
	{
		private static WarFoundryXmlFactory factory;
		private XmlReaderSettings settings;
		private XmlNamespaceManager nsManager;
		private Dictionary<IWarFoundryObject, XmlDocument> extraData = new Dictionary<IWarFoundryObject, XmlDocument>();

		public static AbstractNativeWarFoundryFactory GetFactory()
		{
			if (factory == null)
			{
				factory = new WarFoundryXmlFactory();
			}
			
			return factory;
		}
		
		protected WarFoundryXmlFactory() : base()
		{
			//Hide constructor
		}
		
		protected override bool CheckCanFindArmyFileContent(ZipFile file)
		{
			return file.FindEntry("data.armyx", true) > -1;
		}
		
		protected override bool CheckCanFindSystemFileContent(ZipFile file)
		{
			return file.FindEntry("data.systemx", true) > -1;
		}
		
		protected override bool CheckCanFindRaceFileContent(ZipFile file)
		{
			return file.FindEntry("data.racex", true) > -1;
		}
		
		protected XmlElement GetRootElementFromStream(Stream stream, WarFoundryXmlElementName elementName)
		{
			XmlDocument doc = CreateXmlDocumentFromStream(stream);			
			XmlElement elem = (XmlElement)doc.LastChild;
			
			if (!elem.LocalName.Equals(elementName.Value))
			{
				throw new InvalidFileException(String.Format("Root element of XML was not valid. Expected {0} but got {1}", elementName.Value, elem.Name));
			}
			
			return elem;
		}
		
		protected override Stream GetArmyDataStream(ZipFile file)
		{
			return file.GetInputStream(file.FindEntry("data.armyx", true));
		}
		
		protected override Army CreateArmyFromStream (ZipFile file, Stream dataStream)
		{
			XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.ARMY_ELEMENT);
			return CreateArmyFromElement(file, elem);
		}
		
		private Army CreateArmyFromElement(ZipFile file, XmlElement elem)
		{
			string name = elem.GetAttribute("name");
			string systemID = elem.GetAttribute("gameSystem");
			GameSystem system = WarFoundryLoader.GetDefault().GetGameSystem(systemID);
			string raceID = elem.GetAttribute("race");
			Race race = WarFoundryLoader.GetDefault().GetRace(system, raceID);
			int points = GetIntValueFromAttribute(elem, "maxPoints");			
			Army army = new Army(race, name, points, file);
			//TODO: Complete loading of army
			return army;
		}
		
		private void StoreExtraData(WarFoundryStagedLoadingObject wfObject, XmlElement elem)
		{
			extraData[wfObject] = elem.OwnerDocument;
		}
	
		public XmlDocument GetExtraData(IWarFoundryObject obj)
		{
			XmlDocument extra = null;
			extraData.TryGetValue(obj, out extra);			
			return extra;
		}

		protected override Stream GetGameSystemDataStream (ZipFile file)
		{
			return file.GetInputStream(file.FindEntry("data.systemx", true));
		}
		
		protected override GameSystem CreateGameSystemFromStream (ZipFile file, Stream dataStream)
		{
			XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.SYSTEM_ELEMENT);
			LogNotifier.Debug(GetType(), "Create GameSystem");
			return CreateSystemFromElement(file, elem);
		}
		
		private GameSystem CreateSystemFromElement(ZipFile file, XmlElement elem)
		{
			string id = elem.GetAttribute("id");
			string name = elem.GetAttribute("name");
			GameSystem system = new GameSystem(id, name, this);
			StoreExtraData(system, elem);
			return system;
		}
		
		protected override Stream GetRaceDataStream (ZipFile file)
		{
			return file.GetInputStream(file.FindEntry("data.racex", true));
		}
		
		protected override Race CreateRaceFromStream (ZipFile file, Stream dataStream)
		{
			XmlElement elem = GetRootElementFromStream(dataStream, WarFoundryXmlElementName.RACE_ELEMENT);
			LogNotifier.Debug(GetType(), "Create Race");
			return CreateRaceFromElement(file, elem);
		}
		
		private Race CreateRaceFromElement(ZipFile file, XmlElement elem)
		{
			string id = elem.GetAttribute("id");
			string subid = elem.GetAttribute("subid");
			string systemID = elem.GetAttribute("system");
			string name = elem.GetAttribute("name");
			Race race = new Race(id, subid, name, systemID, this);
			StoreExtraData(race, elem);
			return race;
		}
		
		protected XmlDocument CreateXmlDocumentFromStream(Stream stream)
		{
			XmlDocument doc = new XmlDocument();
			XmlReader reader = XmlReader.Create(stream, GetReaderSettings());
			
			try
			{
				doc.Load(reader);
			}
			//Don't catch XMLSchemaExceptions - let them get thrown out
			finally
			{
				reader.Close();
			}

			return doc;
		}
		
		/// <summary>
		/// Lazy-getter for XML reader settings. May throw a <see cref="InvalidDataException"/> if there is a problem with the translation schema.
		/// </summary>
		/// <returns>
		/// A <see cref="XmlReaderSettings"/> with the default values for validating the translation document against the translation schema
		/// </returns>
		private XmlReaderSettings GetReaderSettings()
		{
			if (settings == null)
			{
				settings = new XmlReaderSettings();
				settings.ValidationType = ValidationType.Schema;
				settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings;
				settings.ProhibitDtd = true;
				settings.ValidationEventHandler+= new ValidationEventHandler(ValidationEventMethod);
				XmlSchemaSet cache = new XmlSchemaSet();
				string path =  IBBoard.Constants.ExecutablePath + "/dtds/";
				string nsBase = "http://ibboard.co.uk/warfoundry/";
				AddSchemaToCache(cache, nsBase + "core", path + "warfoundry-core.xsd");
				AddSchemaToCache(cache, nsBase + "cats", path + "warfoundry-cats.xsd");
				AddSchemaToCache(cache, nsBase + "race", path + "race.xsd");
				AddSchemaToCache(cache, nsBase + "system", path + "system.xsd");
				AddSchemaToCache(cache, nsBase + "army", path + "army.xsd");
				settings.Schemas.Add(cache);
			}
			
			return settings;
		}
		
		private void ValidationEventMethod(object sender, ValidationEventArgs e)
		{
			throw new InvalidDataException("Problem validating against schema for WarFoundry data: " + e.Exception.Message, e.Exception);
		}
		
		private void AddSchemaToCache(XmlSchemaSet cache, string xmlNamespace, string schemaLocation)
		{
			try
			{
				cache.Add(xmlNamespace, schemaLocation);
			}
			catch (IOException ex)
			{
				LogNotifier.Warn(GetType(), "Problem reading schema: " + ex.Message, ex);
			}
			catch (XmlSchemaException ex)
			{
				LogNotifier.Warn(GetType(), "Problem validating schema for WarFoundry data: " + ex.Message, ex);
			}
			catch (XmlException ex)
			{
				LogNotifier.Warn(GetType(), "Problem reading data for schema: " + ex.Message, ex);
			}
		}
		
		private XmlNamespaceManager GetNamespaceManager()
		{
			if (nsManager == null)
			{
				nsManager = new XmlNamespaceManager(new NameTable());
				nsManager.AddNamespace("core", "http://ibboard.co.uk/warfoundry/core");
				nsManager.AddNamespace("cat", "http://ibboard.co.uk/warfoundry/cats");
				nsManager.AddNamespace("race", "http://ibboard.co.uk/warfoundry/race");
				nsManager.AddNamespace("system", "http://ibboard.co.uk/warfoundry/system");
				nsManager.AddNamespace("army", "http://ibboard.co.uk/warfoundry/army");
			}
			
			return nsManager;
		}
		
		private XmlNodeList SelectNodes(XmlNode element, string xpathQuery)
		{
			return element.SelectNodes(xpathQuery, GetNamespaceManager());
		}
		
		private XmlNode SelectSingleNode(XmlNode element, string xpathQuery)
		{
			return element.SelectSingleNode(xpathQuery, GetNamespaceManager());
		}
		
		private XmlElement SelectSingleElement(XmlNode element, string xpathQuery)
		{
			XmlNode node = SelectSingleNode(element, xpathQuery);
			return (node is XmlElement) ? (XmlElement) node : null;
		}
				
		private int GetIntValueFromAttribute(XmlElement elem, string attributeName)
		{
			try
			{
				return int.Parse(elem.GetAttribute(attributeName));
			}
			catch(FormatException)
			{
				throw new FormatException(String.Format("Attribute '{0}' of {1} with ID {2} was not a valid number", attributeName, elem.Name, elem.GetAttribute("id")));
			}
		}
				
		private double GetDoubleValueFromAttribute(XmlElement elem, string attributeName)
		{
			double doubleVal = double.NaN;
			string attribValue = elem.GetAttribute(attributeName);
			
			if (attribValue == "INF")
			{
				doubleVal = double.PositiveInfinity;
			}
			else
			{
				try
				{
					return int.Parse(attribValue);
				}
				catch(FormatException)
				{
					throw new FormatException(String.Format("Attribute '{0}' of {1} with ID {2} was not a valid number", attributeName, elem.Name, elem.GetAttribute("id")));
				}
			}
			
			return doubleVal;
		}

		public override void CompleteLoading(IWarFoundryStagedLoadObject obj)
		{			
			LogNotifier.DebugFormat(GetType(), "Complete loading of {0} with ID {1}", obj.GetType().Name, obj.ID);
							
			if (obj is GameSystem)
			{
				CompleteLoading((GameSystem)obj);
			}
			else if (obj is Race)
			{
				CompleteLoading((Race)obj);
			}
		}
		
		public void CompleteLoading(GameSystem system)
		{
			if (system.IsFullyLoaded)
			{
				LogNotifier.DebugFormat(GetType(), "Object of type GameSystem with ID {0} is already fully loaded", system.ID);
				return;
			}
			
			if (system.IsLoading)
			{
				LogNotifier.WarnFormat(GetType(), "Object of type GameSystem with ID {0} is already being loaded", system.ID);
				return;
			}
			
			system.SetAsLoading();
			
			XmlDocument extraData = GetExtraData(system);
			LoadCategoriesForSystem(system, extraData);
			XmlElement statsElem = SelectSingleElement(extraData, "/system:system/system:sysStatsList");
			string defaultStatsID = statsElem.GetAttribute("defaultStats");
			LoadSystemStatsForSystem(system, extraData);
			system.StandardSystemStatsID = defaultStatsID;
			LogNotifier.DebugFormat(GetType(), "Completed loading of GameSystem with ID {0}", system.ID);
			LogNotifier.DebugFormat(GetType(), "GameSystem with ID {0} default stats: {1}", system.ID, system.StandardSystemStatsID);
			system.SetAsFullyLoaded();
		}
		
		private void LoadCategoriesForSystem(GameSystem system, XmlNode elem)
		{
			WarFoundryObject tempObj;
						
			foreach (XmlElement cat in SelectNodes(elem, "/system:system/system:categories/cat:cat"))
			{
				tempObj = CreateObjectFromElement(cat);
				
				if (tempObj is Category)
				{
					system.AddCategory((Category)tempObj);
				}
				else
				{
					LogNotifier.WarnFormat(GetType(), "Object for string {0} was not of type Category", cat.OuterXml);
				}
			}
		}
		
		public void CompleteLoading(Race race)
		{
			if (race.IsFullyLoaded)
			{
				LogNotifier.DebugFormat(GetType(), "Object of type Race with ID {0} is already fully loaded", race.ID);
				return;
			}
			
			if (race.IsLoading)
			{
				LogNotifier.WarnFormat(GetType(), "Object of type Race with ID {0} is already being loaded", race.ID);
				return;
			}
			
			race.SetAsLoading();
			
			XmlDocument extraData = GetExtraData(race);
			
			foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:units/race:unit"))
			{
				UnitType type = CreateUnitTypeFromElement(node, race, race.GameSystem);
				race.AddUnitType(type);
			}
			
			foreach (XmlElement node in SelectNodes(extraData, "/race:race/race:categories/cat:cat"))
			{
				race.AddCategory(CreateCategoryFromElement(node));
			}
							
			foreach (XmlElement node  in SelectNodes(extraData, "/race:race/race:equipment/cat:equipmentItem"))
			{
				EquipmentItem item = CreateEquipmentItemFromElement(node, race);
				race.AddEquipmentItem(item);
			}
							
			foreach (XmlElement node  in SelectNodes(extraData, "/race:race/race:abilities/cat:ability"))
			{
				Ability ability = CreateAbilityFromElement(node, race);
				race.AddAbility(ability);
			}
			
			race.SetAsFullyLoaded();
			LogNotifier.DebugFormat(GetType(), "Completed loading of Race with ID {0}", race.ID);
		}
					
		private WarFoundryObject CreateObjectFromElement(XmlElement elem)
		{
			WarFoundryObject obj = null;
			LogNotifier.DebugFormat(GetType(), "Create object for <{0}>", elem.Name);
			
			if (elem.LocalName.Equals(WarFoundryXmlElementName.CATEGORY_ELEMENT.Value))
			{
				LogNotifier.Debug(GetType(), "Create Category");
				obj = CreateCategoryFromElement(elem);
			}
			else
			{
				LogNotifier.Debug(GetType(), "No match");
			}
			
			return obj;
		}
		
		private Category CreateCategoryFromElement(XmlElement elem)
		{
			string id = elem.GetAttribute("id");
			string name = elem.GetAttribute("name");
			Category cat = new Category(id, name);
			cat.MaximumPercentage = GetIntValueFromAttribute(elem, "maxPercentage");
			cat.MinimumPercentage = GetIntValueFromAttribute(elem, "minPercentage");
			cat.MaximumPoints = GetIntValueFromAttribute(elem, "maxPoints");
			cat.MinimumPoints = GetIntValueFromAttribute(elem, "minPoints");
			return cat;
		}
						
		private UnitType CreateUnitTypeFromElement(XmlElement elem, Race parentRace, GameSystem system)
		{
			string id = elem.GetAttribute("id");
			string name = elem.GetAttribute("typeName");
			UnitType type = new UnitType(id, name, parentRace);
			type.MaxNumber = GetIntValueFromAttribute(elem, "maxNum");
			type.MinNumber = GetIntValueFromAttribute(elem, "minNum");
			type.MaxSize = GetIntValueFromAttribute(elem, "maxSize");
			type.MinSize = GetIntValueFromAttribute(elem, "minSize");
			type.BaseSize = GetIntValueFromAttribute(elem, "baseSize");
			type.CostPerTrooper = GetIntValueFromAttribute(elem, "points");
			type.BaseUnitCost = GetIntValueFromAttribute(elem, "unitPoints");
			string mainCatID = elem.GetAttribute("cat");
			Category cat = parentRace.GetCategory(mainCatID);
			
			if (cat == null)
			{
				throw new InvalidDataException(String.Format("Attribute 'cat' of UnitType {0} (value: {1}) did not reference a valid category", id, mainCatID));
			}
			
			type.MainCategory = cat;
			XmlElement statsElement = SelectSingleElement(elem, "/race:race/race:units/race:unit/race:stats");
			type.UnitStats = ParseUnitStats(statsElement, system);
			//TODO: Add unit requirements
			LogNotifier.Debug(GetType(), "Loaded "+type.Name);
			return type;
		}
		
		private Stats ParseUnitStats(XmlElement elem, GameSystem system)
		{
			List<Stat> statsList = new List<Stat>();
			String statsID = elem.GetAttribute("statSet");
			SystemStats statsSet;
			
			if (statsID == "")
			{
				statsSet = system.StandardSystemStats;
			}
			else
			{
				statsSet = system.GetSystemStatsForID(statsID);
			}
			
			Stats stats = new Stats(statsSet);
			
			foreach (XmlElement stat in elem.ChildNodes)
			{
				String statID = stat.GetAttribute("name");
				StatSlot slot = statsSet[statID];
				
				if (slot!=null)
				{
					statsList.Add(new Stat(slot, stat.InnerText));
				}
				else
				{
					throw new InvalidFileException("The stat "+statID+" was not found in stats set "+statsID);
				}
			}
			
			stats.SetStats(statsList);
			
			return stats;
		}
		
		private void LoadSystemStatsForSystem(GameSystem system, XmlNode elem)
		{
			foreach (XmlElement stats in SelectNodes(elem, "/system:system/system:sysStatsList/system:sysStats"))
			{
				SystemStats sysStats = CreateSystemStatsFromElement(stats);
				system.AddSystemStats(sysStats);
			}
		}
		
		private SystemStats CreateSystemStatsFromElement(XmlElement elem)
		{
			List<StatSlot> slots = new List<StatSlot>();
			string id = elem.GetAttribute("id");	
			
			foreach (XmlElement slot in elem.ChildNodes)
			{
				StatSlot statSlot = new StatSlot(slot.GetAttribute("name"));
				slots.Add(statSlot);
			}
			
			return new SystemStats(id, slots.ToArray());
		}
		
		private EquipmentItem CreateEquipmentItemFromElement(XmlElement elem, Race race)
		{
			string id = elem.GetAttribute("id");
			string name = elem.GetAttribute("name");
			double cost = 0, min = 0, max = 0;
			ArmourType armourType;
			
			try
			{
				cost = GetDoubleValueFromAttribute(elem, "cost");
			}
			catch(FormatException ex)
			{
				throw new InvalidFileException("Attribute 'cost' of equipment item "+id+" was not a valid number", ex);
			}			
			
			try
			{
				armourType = (ArmourType)Enum.Parse(typeof(ArmourType), elem.GetAttribute("armourType"));
			}
			catch(ArgumentException ex)
			{
				throw new InvalidFileException("Attribute 'armourType' of equipment "+id+" was not a valid value from the enumeration");
			}
			
			//TODO: Parse equipment stats if there are any
			
			return new EquipmentItem(id, name, cost, min, max, armourType, race);
		}
		
		private Ability CreateAbilityFromElement(XmlElement elem, Race race)
		{
			string id = elem.GetAttribute("id");
			string name = elem.GetAttribute("name");
			Ability ability = new Ability(id, name);
			XmlNode node = elem.SelectSingleNode("description", GetNamespaceManager());
			ability.Description = (node == null) ? "" : node.InnerText;
			return ability;
		}
	}
}