using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Chernobyl.Collections.Generic.Event;
using Chernobyl.Plugin;
using Chernobyl.Reflection.Template;
using Chernobyl.Reflection.Template.CodeDom;
using Chernobyl.Resources;
using Microsoft.CSharp;
namespace Chernobyl.Reflection.Resources
{
///
/// An that uses System.CodeDOM
/// to generate the code responsible for creating component information. This
/// accepts the
/// if an
/// with a specific name is needed from an content
/// file.
///
public class CodeDomContentResourceProcessor : ResourceProcessor
{
///
/// Initializes a new instance of the
/// class.
///
/// The instance that
/// gives and takes services.
public CodeDomContentResourceProcessor(IEventCollection services)
{
Services = services;
BaseComponentNamespace = "Chernobyl.Content.Scripts";
Provider = new CSharpCodeProvider();
_contentPluginFactory = new CSharpScriptPluginFactory(Services) { Predicate = type => type.IsPublic };
// On Windows, we want case-insensitive comparisons but not on Unix
// where case sensitivity is expected.
if (Environment.OSVersion.Platform != PlatformID.Unix)
_componentCache = new Dictionary>(StringComparer.OrdinalIgnoreCase);
else
_componentCache = new Dictionary>();
}
///
/// Should process the stream or resource that is passed to it. A
/// resource processor must call its next resource processor (provided
/// it isn't null) if it is unable to generate the resource from the
/// passed in stream. If it is able to
/// generate it, then it should do so, invoke the
/// method, and NOT call the next resource
/// processor. If the next resource processor is null, then the resource
/// processor should throw an exception stating that the resource could
/// not be generated from the stream because no resource processor
/// existed that could perform the task.
///
/// The callback that will be invoked when the
/// resource has been loaded.
/// The stream to load the resource from.
/// Options for the loading or processing of the
/// resource.
public override void From(ResourceProcessCallback callback, Stream readFrom, params IOption[] options)
{
FileStream fileStream = readFrom as FileStream;
if (fileStream != null)
{
// get the timestamp of the created file.
DateTime componentFileWriteTime = File.GetLastWriteTime(fileStream.Name);
// If the Chernobyl assembly is changed, there is a chance that the
// code that auto generates the scripts could have been changed.
// So we will get the last write time of the Chernobyl assembly (dll/lib)
// and use it to decide whether to regenerate the content scripts.
string chernobylAssemblyPath = Assembly.GetExecutingAssembly().CodeBase;
UriBuilder chernobylAssemblyUri = new UriBuilder(chernobylAssemblyPath);
DateTime chernobylAssemblyWriteTime = File.GetLastWriteTime(chernobylAssemblyUri.Path);
// get the filepath of the script
string scriptFilePath = Path.ChangeExtension(fileStream.Name, ".cs");
DateTime scriptFileWriteTime = File.GetLastWriteTime(scriptFilePath);
// We are going to recreate the script file if one of the following
// are true:
// 1) The script file does not exist
// 2) The content file's last write time is newer than the script file
// 3) The Chernobyl assembly's last write time is newer than the script file
if (File.Exists(scriptFilePath) == false ||
componentFileWriteTime >= scriptFileWriteTime ||
chernobylAssemblyWriteTime >= scriptFileWriteTime)
{
// the content file is newer, we'll need to (re)create the
// script file; we'll use a callback to achieve this
ResourceProcessCallback createScriptCallback =
result =>
{
// now generate the code
CodeCompileUnit script = new CodeCompileUnit();
// create the namespace, import namespaces into it, and add it to the
// compile unit
CodeNamespace ns = new CodeNamespace(BaseComponentNamespace + "." + Path.GetFileNameWithoutExtension(scriptFilePath));
ns.Imports.Add(new CodeNamespaceImport("System"));
ns.Imports.Add(new CodeNamespaceImport("Chernobyl.Reflection.Template"));
script.Namespaces.Add(ns);
// create a code type declaration from the instance passed
// in and add it to the namespace
CodeDomInstance instance = (CodeDomInstance)result.Resource;
instance.TypeDeclaration.TypeAttributes = TypeAttributes.Public;
ns.Types.Add(instance.TypeDeclaration);
// write the script to the script file. Make
// sure the code is properly indented using an
// IndentedTextWriter
using (StreamWriter streamWriter = new StreamWriter(scriptFilePath, false))
using (IndentedTextWriter textWriter = new IndentedTextWriter(streamWriter, " "))
{
// generate source code using the code provider and then
// close the text writer
Provider.GenerateCodeFromCompileUnit(script, textWriter, new CodeGeneratorOptions());
textWriter.Close();
}
IComponent resource = CompileScriptsAndCreateComponent(scriptFilePath, options);
callback(new ResourceProcessResult(resource, null, result.CompletedSynchronously, true));
};
// have a processor the content file; we will process
// the result and create a script file from it using the
// callback we created above
CheckedNextFrom(createScriptCallback, readFrom, options);
}
else
{
IComponent resource = CompileScriptsAndCreateComponent(scriptFilePath, options);
callback(new ResourceProcessResult(resource, null, true, true));
}
}
else
CheckedNextFrom(callback, readFrom, options);
}
///
/// Writes the resource to the stream specified.
///
/// The resource to write to the stream.
/// The stream to have the resource written to.
/// Options for the writing or processing of the resource.
public override void To(IComponent resource, Stream writeTo, params IOption[] options)
{
throw new NotImplementedException();
}
///
/// Attempts to compile the script file that was generated or read in.
/// Only the first class we received will be public so we are just going
/// to have our predicate search for public classes.
///
/// The path to the script file to compile.
/// The processing options. This method looks for
/// the option to locate the component to
/// return, otherwise, the first component found will be returned.
/// Thrown if the resource could not
/// be located from the scripts.
/// The component created from the script.
IComponent CompileScriptsAndCreateComponent(string scriptFilePath, IEnumerable options)
{
// We use Path.GetFullPath(String) to ensure the names stored
// and compared to in the map are all full paths. This avoids
// any issues where you reference the same file but with different
// string values. Additionally, we cut out any trailing slashes
// as Path.GetFullPath(String) doesn't seem to standardize on
// trailing slashes very well. See here for more details:
// http://stackoverflow.com/questions/2281531/how-can-i-compare-directory-paths-in-c
String properScriptFilePath = Path.GetFullPath(scriptFilePath).TrimEnd('\\');
// Check the cache for the root component, if it's not there we'll
// load it from the script and add it to the cache.
IEnumerable rootComponents;
if(_componentCache.TryGetValue(properScriptFilePath, out rootComponents) == false)
{
// HACK: this code assumes that the Create method will invoke the
// callback immediately but this is not gauranteed to happen. Fix
// this code.
var creation = _contentPluginFactory.Create(properScriptFilePath.ToEnumerable());
rootComponents = creation.Cast();
_componentCache.Add(properScriptFilePath, rootComponents);
}
// If we were provided a name for the component, we'll need to locate
// it based off that name.
IComponent resource = null;
ResourceName resourceName = options.OfType().FirstOrDefault();
if (resourceName == null)
resource = rootComponents.First();
else
{
// Locate the IComponent based off the name in the ResourceName.
using (IEnumerator componentEnumerable = rootComponents.GetEnumerator())
{
while (componentEnumerable.MoveNext() == true && resource == null)
{
IComponent component = componentEnumerable.Current;
if (component.Name == resourceName.Name)
resource = component;
else
resource = component.FirstOrDefault(comp => comp.Name == resourceName.Name);
}
}
}
// Now check for error conditions.
if (resource == null)
{
if(resourceName == null)
throw new ResourceNotFoundException("Failed to locate a resource in " +
"the compiled script file \"" + scriptFilePath + "\".");
throw new ResourceNotFoundException("Failed to locate the resource \"" +
resourceName.Name + "\" in the compiled script file \"" + scriptFilePath + "\".");
}
return resource;
}
///
/// The that generates the script. By
/// default, this provider generates C# code ( ).
///
CodeDomProvider Provider { get; set; }
///
/// The base namespace that the code of generated scripts is placed into.
/// By default, the namespace is "Chernobyl.Content.Scripts". Note that,
/// the namespace of the content being loaded is
///
public string BaseComponentNamespace { get; set; }
///
/// Holds the services that will be injected into or taken from the
/// created content instance.
///
IEventCollection Services { get; set; }
///
/// The that compiles the generated scripts.
///
CSharpScriptPluginFactory _contentPluginFactory;
///
/// Stores the root s that have already loaded.
/// The key is the script file that was compiled and loaded to get the
/// .
///
IDictionary> _componentCache;
}
}