view API/AbstractWarFoundryLoader.cs @ 370:077e9be48438

Re #346: Add requirement schema support * Pass unit test with a specific case for one requirement - needs extensibility
author IBBoard <dev@ibboard.co.uk>
date Mon, 13 Jun 2011 15:15:04 +0000
parents 3c4a6403a88c
children 71fceea2725b
line wrap: on
line source

// This file (AbstractWarFoundryLoader.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 and distributed, without warranty, under the GNU Affero GPL license, either version 3 of the License or (at your option) any later version. Please see COPYING for more information and the full license.

using System;
using System.Collections.Generic;
using System.IO;
using IBBoard.Collections;
using IBBoard.IO;
using IBBoard.Logging;
using IBBoard.WarFoundry.API.Factories;
using IBBoard.WarFoundry.API.Objects;

namespace IBBoard.WarFoundry.API
{
	/// <summary>
	/// The base abstract implementation of a WarFoundry file loader
	/// </summary>
	public abstract class AbstractWarFoundryLoader
	{
		private ICollection<DirectoryInfo> directories;
		private ICollection<INativeWarFoundryFactory> factories;
		private ICollection<INonNativeWarFoundryFactory> nonNativeFactories;
		private Dictionary<IWarFoundryFactory, SimpleSet<IWarFoundryObject>> loadedObjects;

		public delegate void FileLoadingCompleteDelegate(List<FileLoadFailure> failures);

		public event MethodInvoker FileLoadingStarted;

		public event FileLoadingCompleteDelegate FileLoadingFinished;

		protected AbstractWarFoundryLoader()
		{
			directories = new List<DirectoryInfo>();
			factories = new List<INativeWarFoundryFactory>();
			nonNativeFactories = new List<INonNativeWarFoundryFactory>();
			loadedObjects = new Dictionary<IWarFoundryFactory,SimpleSet<IWarFoundryObject>>();
		}
		
		/// <summary>
		/// Adds a directory to the collection of directories that will be searched for WarFoundry data files.
		/// </summary>
		/// <param name="directory">
		/// The <see cref="DirectoryInfo"/> to add to the list for searching for data files
		/// </param>
		public void AddLoadDirectory(DirectoryInfo directory)
		{
			if (!directories.Contains(directory))
			{
				directories.Add(directory);
			}
		}
		
		/// <summary>
		/// Removes a directory from the collection of directories that will be searched for WarFoundry data files.
		/// </summary>
		/// <param name="directory">
		/// A <see cref="DirectoryInfo"/>
		/// </param>
		public void RemoveLoadDirectory(DirectoryInfo directory)
		{
			if (directories.Contains(directory))
			{
				directories.Remove(directory);
			}
		}
		
		/// <summary>
		/// Registers a <see cref="INativeWarFoundryFactory"/> as a factory that can parse native data files.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INativeWarFoundryFactory"/> to register to parse native data files.
		/// </param>
		public virtual void RegisterFactory(INativeWarFoundryFactory factory)
		{
			if (!factories.Contains(factory))
			{
				factories.Add(factory);
			}
		}
		
		/// <summary>
		/// Unregisters a <see cref="INativeWarFoundryFactory"/> so that it will no longer be used to try to parse native data files.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INativeWarFoundryFactory"/> to remove from the collection of factories that are used to try to parse native data files.
		/// </param>
		public virtual void UnregisterFactory(INativeWarFoundryFactory factory)
		{
			if (factories.Contains(factory))
			{
				factories.Remove(factory);
			}
		}
		
		/// <summary>
		/// Registers a <see cref="INonNativeWarFoundryFactory"/> so that it will be used to try to parse non-native data files from other applications.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INonNativeWarFoundryFactory"/> to register to parse non-native data files.
		/// </param>
		public virtual void RegisterNonNativeFactory(INonNativeWarFoundryFactory factory)
		{
			if (!nonNativeFactories.Contains(factory))
			{
				nonNativeFactories.Add(factory);
			}
		}
		
		/// <summary>
		/// Unregisters a <see cref="INonNativeWarFoundryFactory"/> so that it will no longer be used to try to parse non-native data files from other applications.
		/// </summary>
		/// <param name="factory">
		/// The <see cref="INonNativeWarFoundryFactory"/> to remove from the collection of factories that are used to try to parse non-native data files.
		/// </param>
		public virtual void UnregisterNonNativeFactory(INonNativeWarFoundryFactory factory)
		{
			if (nonNativeFactories.Contains(factory))
			{
				nonNativeFactories.Remove(factory);
			}
		}
		
		/// <summary>
		/// Loads all of the data files in the registered directories.
		/// </summary>
		/// <returns>
		/// A <see cref="List"/> of <see cref="FileLoadFailure"/> for files that failed to load
		/// </returns>
		public List<FileLoadFailure> LoadFiles()
		{
			PrepareForFileLoad();
			Dictionary<FileInfo, IWarFoundryFactory> loadableRaces = new Dictionary<FileInfo, IWarFoundryFactory>();
			Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems = new Dictionary<FileInfo, IWarFoundryFactory>();
			List<FileLoadFailure> failedLoads = FillLoadableFiles(loadableRaces, loadableGameSystems);
			failedLoads.AddRange(LoadGameSystems(loadableGameSystems));
			failedLoads.AddRange(LoadRaces(loadableRaces));
			OnFileLoadingFinished(failedLoads);
			FinishFileLoad();
			return failedLoads;
		}

		private void OnFileLoadingFinished(List<FileLoadFailure> failures)
		{
			if (FileLoadingFinished != null)
			{
				FileLoadingFinished(failures);
			}
		}

		protected virtual void PrepareForFileLoad()
		{
			//Do nothing special
		}

		protected virtual void FinishFileLoad()
		{
			//Do nothing special
		}

		private List<FileLoadFailure> FillLoadableFiles(Dictionary<FileInfo, IWarFoundryFactory> loadableRaces, Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems)
		{			
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			
			foreach (DirectoryInfo directory in directories)
			{
				directory.Refresh();

				if (directory.Exists)
				{
					List<FileLoadFailure> directoryFails = FillLoadableFilesForDirectory(loadableRaces, loadableGameSystems, directory);
					fails.AddRange(directoryFails);
				}
				else
				{
					LogNotifier.WarnFormat(GetType(), "Load for {0} failed because directory didn't exist", directory.FullName);
				}
			}
			
			return fails;
		}

		private List<FileLoadFailure> FillLoadableFilesForDirectory(Dictionary<FileInfo, IWarFoundryFactory> loadableRaces, Dictionary<FileInfo, IWarFoundryFactory> loadableGameSystems, DirectoryInfo directory)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			LogNotifier.Debug(GetType(), "Load from " + directory.FullName);
		
			foreach (FileInfo file in directory.GetFiles())
			{
				IWarFoundryFactory factory = GetGameSystemLoadingFactoryForFile(file);
				
				if (factory != null)
				{
					loadableGameSystems.Add(file, factory);
				}
				else
				{
					factory = GetRaceLoadingFactoryForFile(file);
	
					if (factory != null)
					{
						loadableRaces.Add(file, factory);
					}
					else
					{
						FileLoadFailure failure = new FileLoadFailure(file, "File not handled as a Race or Game System definition: {0}", "FileNotHandled");
						fails.Add(failure);
						LogNotifier.Info(GetType(), failure.Message);
					}
				}
			}
			
			foreach (DirectoryInfo subdir in directory.GetDirectories())
			{
				fails.AddRange(FillLoadableFilesForDirectory(loadableRaces, loadableGameSystems, subdir));
			}

			return fails;
		}

		private IWarFoundryFactory GetGameSystemLoadingFactoryForFile(FileInfo file)
		{
			IWarFoundryFactory loadingFactory = null;
			
			foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
			{
				if (factory.CanHandleFileAsGameSystem(file))
				{
					loadingFactory = factory;
					break;
				}
			}

			if (loadingFactory == null)
			{
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileAsGameSystem(file))
					{
						loadingFactory = factory;
						break;
					}
				}
			}

			return loadingFactory;
		}

		private IWarFoundryFactory GetRaceLoadingFactoryForFile(FileInfo file)
		{
			IWarFoundryFactory loadingFactory = null;
			
			foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
			{
				if (factory.CanHandleFileAsRace(file))
				{
					loadingFactory = factory;
					break;
				}
			}

			if (loadingFactory == null)
			{
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileAsRace(file))
					{
						loadingFactory = factory;
						break;
					}
				}
			}

			return loadingFactory;
		}

		private List<FileLoadFailure> LoadGameSystems(Dictionary<FileInfo, IWarFoundryFactory> gameSystemFiles)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();

			
			foreach (FileInfo file in gameSystemFiles.Keys)
			{
				FileLoadFailure failure = null;
				
				try
				{
					bool loaded = LoadObject(file, gameSystemFiles[file]);
	
					if (!loaded)
					{
						failure = new FileLoadFailure(file, "FileLoadFailed", "Failed to load {0} as GameSystem using {1}");
					}
				}
				catch (Exception ex)
				{
					failure = new FileLoadFailure(file, null, ex.Message, null, ex);
				}
						
				if (failure != null)
				{
					fails.Add(failure);
					LogNotifier.Warn(GetType(), failure.Message, failure.Exception);
				}
			}
			
			return fails;
		}

		private List<FileLoadFailure> LoadRaces(Dictionary<FileInfo, IWarFoundryFactory> raceFiles)
		{
			List<FileLoadFailure> fails = new List<FileLoadFailure>();
			
			foreach (FileInfo file in raceFiles.Keys)
			{
				FileLoadFailure failure = null;
				
				try
				{
					bool loaded = LoadObject(file, raceFiles[file]);
	
					if (!loaded)
					{
						failure = new FileLoadFailure(file, "FileLoadFailed", "Failed to load {0} as Race using {1}");
					}
				}
				catch (Exception ex)
				{
					failure = new FileLoadFailure(file, null, ex.Message, null, ex);
				}
						
				if (failure != null)
				{
					fails.Add(failure);
					LogNotifier.Warn(GetType(), failure.Message, failure.Exception);
				}
			}
			
			return fails;
		}

		private bool LoadObject(FileInfo file, IWarFoundryFactory factory)
		{
			LogNotifier.DebugFormat(GetType(), "Loading {0} using {1}", file.FullName, factory.GetType().Name);
			factory.RaceLoaded+= StoreRace;
			factory.GameSystemLoaded+= StoreGameSystem;
			ICollection<IWarFoundryObject> objects = factory.CreateObjectsFromFile(file);
			return objects.Count > 0;
		}
		
		
		/// <summary>
		/// Loads a single file through the registered WarFoundryFactories, if a factory exists that supports the file format.
		/// </summary>
		/// <param name="file">
		/// A <see cref="FileInfo"/> for the file to attempt to load
		/// </param>
		/// <returns>
		/// An ICollection of IWarFoundryObjects loaded from <code>file</code>
		/// </returns>
		public ICollection<IWarFoundryObject> LoadFile(FileInfo file)
		{
			ICollection<IWarFoundryObject> objs = null;
			IWarFoundryFactory loadFactory = null;
			
			try
			{
				objs = LoadFileWithNonNativeFactories(file, out loadFactory);
				
				if (objs == null)
				{
					objs = LoadFileWithNativeFactories(file, out loadFactory);
				}
			}
			catch (InvalidFileException ex)
			{
				LogNotifier.Error(GetType(), file.FullName + " failed to load", ex);
			}
				
			if (objs != null)
			{
				AddLoadedObjects(objs, loadFactory);
			}
			else
			{
				objs = new List<IWarFoundryObject>();
			}

			return objs;
		}

		private ICollection<IWarFoundryObject> LoadFileWithNonNativeFactories(FileInfo file, out IWarFoundryFactory loadFactory)
		{
			ICollection<IWarFoundryObject> objs = null;
			loadFactory = null;
			
			if (nonNativeFactories.Count > 0)
			{
				LogNotifier.Debug(GetType(), "Attempting to load " + file.FullName + " as a non-native file");
				
				foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
				{
					bool canLoad = factory.CanHandleFileFormat(file);
					LogNotifier.Debug(GetType(), "Load using " + factory.GetType().FullName + "? " + (canLoad ? "yes" : "no"));
					
					if (canLoad)
					{
						objs = factory.CreateObjectsFromFile(file);
						
						if (objs != null)
						{
							loadFactory = factory;
							break;
						}
					}			         
				}
			}
			
			return objs;
		}

		private ICollection<IWarFoundryObject> LoadFileWithNativeFactories(FileInfo file, out IWarFoundryFactory loadFactory)
		{
			ICollection<IWarFoundryObject> objs = null;
			loadFactory = null;
			
			if (factories.Count > 0)
			{
				LogNotifier.Debug(GetType(), "Attempting to load " + file.FullName + " as native file");
						
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileFormat(file))
					{
						objs = factory.CreateObjectsFromFile(file);
						
						if (objs != null)
						{
							loadFactory = factory;
							break;
						}
					}
				}
			}
			
			return objs;
		}

		private void AddLoadedObjects(ICollection<IWarFoundryObject> loadedObjs, IWarFoundryFactory factory)
		{
			SimpleSet<IWarFoundryObject> objs;
			loadedObjects.TryGetValue(factory, out objs);
			
			if (objs == null)
			{
				objs = new SimpleSet<IWarFoundryObject>();
				loadedObjects.Add(factory, objs);
			}
				
			objs.AddRange(loadedObjs);
			StoreObjects(loadedObjs);
		}

		private void StoreObjects(ICollection<IWarFoundryObject> loadedObjects)
		{
			foreach (IWarFoundryObject loadedObject in loadedObjects)
			{
				if (loadedObject is GameSystem)
				{
					StoreGameSystem((GameSystem)loadedObject);
				}
				else if (loadedObject is Race)
				{
					StoreRace((Race)loadedObject);
				}
			}
		}

		protected void StoreGameSystem(GameSystem system)
		{
			GameSystem existingSystem = GetExistingSystemForSystem(system);
			
			if (existingSystem != null)
			{				
				if (!system.Equals(existingSystem))
				{
					//TODO: Raise an event to say we got a different duplicate
					//We can't just fail, because failing is for completely unhandled files, not for objects in a file
				}
			}
			else
			{
				DoStoreGameSystem(system);
			}
		}
		
		/// <summary>
		/// Gets a game system that has already been loaded that duplicates the supplied game system's ID, if one exists.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> to find pre-existing duplicates of
		/// </param>
		/// <returns>
		/// <code>null</code> if no existing duplicate exists, else the duplicate <see cref="GameSystem"/>
		/// </returns>
		protected abstract GameSystem GetExistingSystemForSystem(GameSystem system);
		
		/// <summary>
		/// Stores a GameSystem in the loader's relevant storage structure
		/// </summary>
		/// <param name="system">
		/// The loaded <see cref="GameSystem"/> to store
		/// </param>
		protected abstract void DoStoreGameSystem(GameSystem system);

		protected void StoreRace(Race race)
		{
			if (race.GameSystem == null)
			{
				throw new InvalidOperationException("Race cannot have null game system. Game system should be loaded before race.");
			}
			
			DoStoreRace(race);
		}
		
		/// <summary>
		/// Performs the implementation specific storage of a race
		/// </summary>
		/// <param name="race">
		/// The <see cref="Race"/> to store
		/// </param>
		protected abstract void DoStoreRace(Race race);
		
		/// <summary>
		/// Gets all <see cref="GameSystem"/>s that are currently available, determined by those that can be loaded with the current <see cref="IWarFoundryFactory"/>s. 
		/// </summary>
		/// <returns>
		/// An array of <see cref="GameSystem"/>s that are currently available.
		/// </returns>
		public abstract GameSystem[] GetGameSystems();

		/// <summary>
		/// Gets a single <see cref="GameSystem"/> with a given ID. 
		/// </summary>
		/// <param name="systemID">
		/// The ID of the <see cref="GameSystem"/> to get, as a <see cref="System.String"/>.
		/// </param>
		/// <returns>
		/// The <see cref="GameSystem"/> with the given ID, or <code>null</code> if one doesn't exist.
		/// </returns>
		public abstract GameSystem GetGameSystem(string systemID);

		/// <summary>
		/// Removes a loaded <see cref="GameSystem"/>. Used when a GameSystem fails to complete loading
		/// </summary>
		/// <param name="system">The GameSystem to remove</param>
		protected internal abstract void RemoveGameSystem(GameSystem system);

		/// <summary>
		/// Gets an array of the races for the specified <see cref="GameSystem"/>.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> to get the available races for.
		/// </param>
		/// <returns>
		/// An array of <see cref="Race"/>s for the <see cref="GameSystem"/>
		/// </returns>
		public abstract Race[] GetRaces(GameSystem system);

		/// <summary>
		/// Gets a single race for a given <see cref="GameSystem"/> by ID of the race.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// A <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <returns>
		/// A <see cref="Race"/> with the specified ID from the <see cref="GameSystem"/>, or <code>null</code> if one doesn't exist.
		/// </returns>
		public abstract Race GetRace(GameSystem system, string raceID);

		/// <summary>
		/// Gets a single race for a given <see cref="GameSystem"/> by the race's ID and sub-race ID.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> that the race is part of.
		/// </param>
		/// <param name="raceID">
		/// The <see cref="System.String"/> ID for the race to load.
		/// </param>
		/// <param name="raceSubID">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="Race"/>
		/// </returns>
		public abstract Race GetRace(GameSystem system, string raceID, string raceSubID);

		protected internal abstract void RemoveRace(Race race);

		/// <summary>
		/// Gets the IDs of all of the game systems currently available.
		/// </summary>
		/// <returns>
		/// An array of <see cref="System.String"/>s representing the IDs of the game systems.
		/// </returns>
		public virtual string[] GetGameSystemIDs()
		{
			GameSystem[] systems = GetGameSystems();
			return GetWarFoundryObjectIDs(systems);
		}

		protected string[] GetWarFoundryObjectIDs(WarFoundryObject[] objs)
		{
			int objCount = objs.Length;
			string[] keys = new string[objCount];

			for (int i = 0; i < objCount; i++)
			{
				keys[i] = objs[i].ID;
			}

			return keys;
		}
		
		/// <summary>
		/// Gets the IDs of all of the races of a specified game system.
		/// </summary>
		/// <param name="system">
		/// The <see cref="GameSystem"/> to get the available races for.
		/// </param>
		/// <returns>
		/// An array of <see cref="System.String"/>s representing the IDs of the races of the specified game system.
		/// </returns>
		public virtual string[] GetSystemRaceIDs(GameSystem system)
		{
			Race[] races = GetRaces(system);
			return GetWarFoundryObjectIDs(races);
		}

		public Army LoadArmy(FileInfo file)
		{
			IWarFoundryFactory factory = GetArmyLoadingFactoryForFile(file);			
			Army loadedArmy = null;
			
			if (factory != null)
			{
				ICollection<IWarFoundryObject> objs = factory.CreateObjectsFromFile(file);
								
				if (objs.Count == 1)
				{
					foreach (IWarFoundryObject systemCount in objs)
					{
						if (systemCount is Army)
						{
							loadedArmy = (Army)systemCount;
						}
					}
				}
			}
			
			return loadedArmy;
		}

		private IWarFoundryFactory GetArmyLoadingFactoryForFile(FileInfo file)
		{
			IWarFoundryFactory loadingFactory = null;
			
			foreach (INonNativeWarFoundryFactory factory in nonNativeFactories)
			{
				if (factory.CanHandleFileAsArmy(file))
				{
					loadingFactory = factory;
					break;
				}
			}

			if (loadingFactory == null)
			{
				foreach (INativeWarFoundryFactory factory in factories)
				{
					if (factory.CanHandleFileAsArmy(file))
					{
						loadingFactory = factory;
						break;
					}
				}
			}

			return loadingFactory;
		}
	}
}