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; } }