Fixing issue with filename case sensitivity on linux, second try
[castle.git] / MonoRail / Castle.MonoRail.Views.Brail / BooViewEngine.cs
bloba3462142f1cd6e986a2aad1588505b08615c3bb9
1 // Copyright 2004-2007 Castle Project - http://www.castleproject.org/
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle.MonoRail.Views.Brail
17 using System;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Configuration;
21 using System.IO;
22 using System.Reflection;
23 using System.Runtime.Serialization;
24 using System.Text;
25 using System.Threading;
26 using Boo.Lang.Compiler;
27 using Boo.Lang.Compiler.IO;
28 using Boo.Lang.Compiler.Pipelines;
29 using Boo.Lang.Compiler.Steps;
30 using Boo.Lang.Parser;
31 using Castle.Core;
32 using Castle.Core.Logging;
33 using Castle.MonoRail.Framework;
34 using Castle.MonoRail.Framework.Helpers;
36 public class BooViewEngine : ViewEngineBase, IInitializable
38 /// <summary>
39 /// This field holds all the cache of all the
40 /// compiled types (not instances) of all the views that Brail nows of.
41 /// </summary>
42 private Hashtable compilations = Hashtable.Synchronized(
43 new Hashtable(StringComparer.InvariantCultureIgnoreCase));
45 /// <summary>
46 /// used to hold the constructors of types, so we can avoid using
47 /// Activator (which takes a long time
48 /// </summary>
49 private Hashtable constructors = new Hashtable();
51 /// <summary>
52 /// This is used to add a reference to the common scripts for each compiled scripts
53 /// </summary>
54 private Assembly common;
55 private ILogger logger;
56 private static BooViewEngineOptions options;
57 private string baseSavePath;
59 public void Initialize()
61 if (options == null) InitializeConfig();
63 string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
64 Log("Base Directory: " + baseDir);
65 baseSavePath = Path.Combine(baseDir, options.SaveDirectory);
66 Log("Base Save Path: " + baseSavePath);
68 if (options.SaveToDisk && !Directory.Exists(baseSavePath))
70 Directory.CreateDirectory(baseSavePath);
71 Log("Created directory " + baseSavePath);
74 CompileCommonScripts();
76 ViewSourceLoader.ViewChanged += new FileSystemEventHandler(OnViewChanged);
79 public override bool SupportsJSGeneration
81 get { return true; }
84 public override string ViewFileExtension
86 get { return ".brail"; }
89 public override string JSGeneratorFileExtension
91 get { return ".brailjs"; }
94 public override bool HasTemplate(string templateName)
96 if (Path.HasExtension(templateName))
97 return ViewSourceLoader.HasTemplate(templateName);
98 return ViewSourceLoader.HasTemplate(templateName + ViewFileExtension);
101 // Process a template name and output the results to the user
102 // This may throw if an error occured and the user is not local (which would
103 // cause the yellow screen of death)
104 public override void Process(IRailsEngineContext context, Controller controller, string templateName)
106 Process(context.Response.Output, context, controller, templateName);
109 public override void Process(TextWriter output, IRailsEngineContext context, Controller controller,
110 string templateName)
112 Log("Starting to process request for {0}", templateName);
113 string file = templateName + ViewFileExtension;
114 BrailBase view;
115 // Output may be the layout's child output if a layout exists
116 // or the context.Response.Output if the layout is null
117 LayoutViewOutput layoutViewOutput = GetOutput(output, context, controller);
118 // Will compile on first time, then save the assembly on the cache.
119 view = GetCompiledScriptInstance(file, layoutViewOutput.Output, context, controller);
120 controller.PreSendView(view);
121 Log("Executing view {0}", templateName);
122 view.Run();
123 if (layoutViewOutput.Layout != null)
125 layoutViewOutput.Layout.SetParent(view);
126 layoutViewOutput.Layout.Run();
128 Log("Finished executing view {0}", templateName);
129 controller.PostSendView(view);
132 public override void ProcessPartial(TextWriter output, IRailsEngineContext context, Controller controller,
133 string partialName)
135 Log("Generating partial for {0}", partialName);
139 string file = ResolveTemplateName(partialName, ViewFileExtension);
140 BrailBase view = GetCompiledScriptInstance(file, output, context, controller);
141 Log("Executing partial view {0}", partialName);
142 view.Run();
143 Log("Finished executing partial view {0}", partialName);
145 catch (Exception ex)
147 if (Logger != null && Logger.IsErrorEnabled)
149 Logger.Error("Could not generate JS", ex);
152 throw new RailsException("Error generating partial: " + partialName, ex);
156 public override object CreateJSGenerator(IRailsEngineContext context)
158 return new BrailJSGenerator(new PrototypeHelper.JSGenerator(context));
161 public override void GenerateJS(TextWriter output, IRailsEngineContext context, Controller controller,
162 string templateName)
164 Log("Generating JS for {0}", templateName);
168 object generator = CreateJSGenerator(context);
169 AdjustJavascriptContentType(context);
170 string file = ResolveTemplateName(templateName, JSGeneratorFileExtension);
171 BrailBase view = GetCompiledScriptInstance(file,
172 //we use the script just to build the generator, not to output to the user
173 new StringWriter(),
174 context, controller);
175 Log("Executing JS view {0}", templateName);
176 view.AddProperty("page", generator);
177 view.Run();
179 output.WriteLine(generator);
180 Log("Finished executing JS view {0}", templateName);
182 catch (Exception ex)
184 if (Logger != null && Logger.IsErrorEnabled)
186 Logger.Error("Could not generate JS", ex);
189 throw new RailsException("Error generating JS. Template: " + templateName, ex);
193 // Send the contents text directly to the user, only adding the layout if neccecary
194 public override void ProcessContents(IRailsEngineContext context, Controller controller, string contents)
196 LayoutViewOutput layoutViewOutput = GetOutput(controller.Response.Output, context, controller);
197 layoutViewOutput.Output.Write(contents);
198 // here we don't need to pass parameters from the layout to the view,
199 if (layoutViewOutput.Layout != null)
200 layoutViewOutput.Layout.Run();
203 private void OnViewChanged(object sender, FileSystemEventArgs e)
205 if (e.FullPath.IndexOf(options.CommonScriptsDirectory) != -1)
207 Log("Detected a change in commons scripts directory " + options.CommonScriptsDirectory + ", recompiling site");
208 // need to invalidate the entire CommonScripts assembly
209 // not worrying about concurrency here, since it is assumed
210 // that changes here are rare. Note, this force a recompile of the
211 // whole site!
214 WaitForFileToBecomeAvailableForReading(e);
215 CompileCommonScripts();
217 catch (Exception ex)
219 // we failed to recompile the commons scripts directory, but because we are running
220 // on another thread here, and exception would kill the application, so we log it
221 // and continue on. CompileCommonScripts() will only change the global state if it has
222 // successfully compiled the commons scripts directory.
223 Log("Failed to recompile the commons scripts directory! {0}", ex);
225 return;
227 Log("Detected a change in {0}, removing from complied cache", e.Name);
228 // Will cause a recompilation
229 compilations[e.Name] = null;
232 private static void WaitForFileToBecomeAvailableForReading(FileSystemEventArgs e)
234 // We may need to wait while the file is being written and closed to disk
235 int retries = 10;
236 bool successfullyOpenedFile = false;
237 while (retries != 0 && successfullyOpenedFile == false)
239 retries -= 1;
242 using (File.OpenRead(e.FullPath))
244 successfullyOpenedFile = true;
247 catch (IOException)
249 //The file is probably in locked because it is currently being written to,
250 // will wait a while for it to be freed.
251 // again, this isn't something that need to be very robust, it runs on a separate thread
252 // and if it fails, it is not going to do any damage
253 Thread.Sleep(250);
258 // Get configuration options if they exists, if they do not exist, load the default ones
259 // Create directory to save the compiled assemblies if required.
260 // pre-compile the common scripts
261 public override void Service(IServiceProvider serviceProvider)
263 base.Service(serviceProvider);
264 ILoggerFactory loggerFactory = serviceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
265 if (loggerFactory != null)
266 logger = loggerFactory.Create(GetType().Name);
269 /// <summary>
270 /// Resolves the template name into a file name.
271 /// </summary>
272 protected static string ResolveTemplateName(string templateName, string extention)
274 if (Path.HasExtension(templateName))
276 return templateName;
278 else
280 return templateName + extention;
284 // Check if a layout has been defined. If it was, then the layout would be created
285 // and will take over the output, otherwise, the context.Reposne.Output is used,
286 // and layout is null
287 private LayoutViewOutput GetOutput(TextWriter output, IRailsEngineContext context, Controller controller)
289 BrailBase layout = null;
290 if (controller.LayoutName != null)
292 string layoutTemplate = "layouts\\" + controller.LayoutName;
293 string layoutFilename = layoutTemplate + ViewFileExtension;
294 layout = GetCompiledScriptInstance(layoutFilename, output,
295 context, controller);
296 output = layout.ChildOutput = new StringWriter();
298 return new LayoutViewOutput(output, layout);
301 // This takes a filename and return an instance of the view ready to be used.
302 // If the file does not exist, an exception is raised
303 // The cache is checked to see if the file has already been compiled, and it had been
304 // a check is made to see that the compiled instance is newer then the file's modification date.
305 // If the file has not been compiled, or the version on disk is newer than the one in memory, a new
306 // version is compiled.
307 // Finally, an instance is created and returned
308 public BrailBase GetCompiledScriptInstance(string file,
309 TextWriter output,
310 IRailsEngineContext context,
311 Controller controller)
313 bool batch = options.BatchCompile;
314 // normalize filename - replace / or \ to the system path seperator
315 string filename = file.Replace('/', Path.DirectorySeparatorChar)
316 .Replace('\\', Path.DirectorySeparatorChar);
317 Log("Getting compiled instnace of {0}", filename);
318 Type type;
319 if (compilations.ContainsKey(filename))
321 type = (Type)compilations[filename];
322 if (type != null)
324 Log("Got compiled instance of {0} from cache", filename);
325 return CreateBrailBase(context, controller, output, type);
327 // if file is in compilations and the type is null,
328 // this means that we need to recompile. Since this usually means that
329 // the file was changed, we'll set batch to false and procceed to compile just
330 // this file.
331 Log("Cache miss! Need to recompile {0}", filename);
332 batch = false;
334 type = CompileScript(filename, batch);
335 if (type == null)
337 throw new RailsException("Could not find a view with path " + filename);
339 return CreateBrailBase(context, controller, output, type);
342 private BrailBase CreateBrailBase(IRailsEngineContext context, Controller controller, TextWriter output, Type type)
344 ConstructorInfo constructor = (ConstructorInfo)constructors[type];
345 BrailBase self = (BrailBase)FormatterServices.GetUninitializedObject(type);
346 constructor.Invoke(self, new object[] { this, output, context, controller });
347 return self;
350 // Compile a script (or all scripts in a directory), save the compiled result
351 // to the cache and return the compiled type.
352 // If an error occurs in batch compilation, then an attempt is made to compile just the single
353 // request file.
354 public Type CompileScript(string filename, bool batch)
356 IDictionary<ICompilerInput, string> inputs2FileName = GetInput(filename, batch);
357 string name = NormalizeName(filename);
358 Log("Compiling {0} to {1} with batch: {2}", filename, name, batch);
359 CompilationResult result = DoCompile(inputs2FileName.Keys, name);
361 if (result.Context.Errors.Count > 0)
363 if (batch == false)
365 string errors = result.Context.Errors.ToString(true);
366 Log("Failed to compile {0} because {1}", filename, errors);
367 StringBuilder msg = new StringBuilder();
368 msg.Append("Error during compile:")
369 .Append(Environment.NewLine)
370 .Append(errors)
371 .Append(Environment.NewLine);
373 foreach (ICompilerInput input in inputs2FileName.Keys)
375 msg.Append("Input (").Append(input.Name).Append(")")
376 .Append(Environment.NewLine);
377 msg.Append(result.Processor.GetInputCode(input))
378 .Append(Environment.NewLine);
380 throw new RailsException(msg.ToString());
382 //error compiling a batch, let's try a single file
383 return CompileScript(filename, false);
385 Type type;
386 foreach (ICompilerInput input in inputs2FileName.Keys)
388 string viewName = Path.GetFileNameWithoutExtension(input.Name);
389 string typeName = TransformToBrailStep.GetViewTypeName(viewName);
390 type = result.Context.GeneratedAssembly.GetType(typeName);
391 Log("Adding {0} to the cache", type.FullName);
392 compilations[ inputs2FileName[input] ] = type;
393 constructors[type] = type.GetConstructor(new Type[]
395 typeof(BooViewEngine),
396 typeof(TextWriter),
397 typeof(IRailsEngineContext),
398 typeof(Controller)
401 type = (Type)compilations[filename];
402 return type;
405 // If batch compilation is set to true, this would return all the view scripts
406 // in the director (not recursive!)
407 // Otherwise, it would return just the single file
408 private IDictionary<ICompilerInput, string> GetInput(string filename, bool batch)
410 Dictionary<ICompilerInput, string> input2FileName = new Dictionary<ICompilerInput, string>();
411 if (batch == false)
413 input2FileName.Add(CreateInput(filename), filename);
414 return input2FileName;
416 // use the System.IO.Path to get the folder name even though
417 // we are using the ViewSourceLoader to load the actual file
418 string directory = Path.GetDirectoryName(filename);
419 foreach (string file in ViewSourceLoader.ListViews(directory))
421 ICompilerInput input = CreateInput(file);
422 input2FileName.Add(input, file);
424 return input2FileName;
427 // create an input from a resource name
428 public ICompilerInput CreateInput(string name)
430 IViewSource viewSrc = ViewSourceLoader.GetViewSource(name);
431 if (viewSrc == null)
433 throw new RailsException("{0} is not a valid view", name);
435 // I need to do it this way because I can't tell
436 // when to dispose of the stream.
437 // It is not expected that this will be a big problem, the string
438 // will go away after the compile is done with them.
439 using (StreamReader stream = new StreamReader(viewSrc.OpenViewStream()))
441 return new StringInput(name, stream.ReadToEnd());
445 /// <summary>
446 /// Perform the actual compilation of the scripts
447 /// Things to note here:
448 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
449 /// * If a common scripts assembly exist, it is also referenced
450 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
451 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
452 /// this allows to use naked parameters such as (output context.IsLocal) without using
453 /// any special syntax
454 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
455 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
456 /// date and list without accidently using the Boo.Lang.BuiltIn versions
457 /// </summary>
458 /// <param name="files"></param>
459 /// <param name="name"></param>
460 /// <returns></returns>
461 private CompilationResult DoCompile(ICollection<ICompilerInput> files, string name)
463 ICompilerInput[] filesAsArray = new List<ICompilerInput>(files).ToArray();
464 BooCompiler compiler = SetupCompiler(filesAsArray);
465 string filename = Path.Combine(baseSavePath, name);
466 compiler.Parameters.OutputAssembly = filename;
467 // this is here and not in SetupCompiler since CompileCommon is also
468 // using SetupCompiler, and we don't want reference to the old common from the new one
469 if (common != null)
470 compiler.Parameters.References.Add(common);
471 // pre procsssor needs to run before the parser
472 BrailPreProcessor processor = new BrailPreProcessor(this);
473 compiler.Parameters.Pipeline.Insert(0, processor);
474 // inserting the add class step after the parser
475 compiler.Parameters.Pipeline.Insert(2, new TransformToBrailStep(options));
476 compiler.Parameters.Pipeline.Replace(typeof(ProcessMethodBodiesWithDuckTyping), new ReplaceUknownWithParameters());
477 compiler.Parameters.Pipeline.Replace(typeof(ExpandDuckTypedExpressions), new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
478 compiler.Parameters.Pipeline.Replace(typeof(InitializeTypeSystemServices), new InitializeCustomTypeSystem());
479 compiler.Parameters.Pipeline.RemoveAt(compiler.Parameters.Pipeline.Find(typeof(IntroduceGlobalNamespaces)));
481 return new CompilationResult(compiler.Run(), processor);
484 // Return the output filename for the generated assembly
485 // The filename is dependant on whatever we are doing a batch
486 // compile or not, if it's a batch compile, then the directory name
487 // is used, if it's just a single file, we're using the file's name.
488 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
489 // should never include it since I'm converting this to a relative path
490 public string NormalizeName(string filename)
492 string name = filename;
493 name = name.Replace(Path.AltDirectorySeparatorChar, '_');
494 name = name.Replace(Path.DirectorySeparatorChar, '_');
496 return name + "_BrailView.dll";
499 // Compile all the common scripts to a common assemblies
500 // an error in the common scripts would raise an exception.
501 public bool CompileCommonScripts()
503 if (options.CommonScriptsDirectory == null)
504 return false;
506 // the demi.boo is stripped, but GetInput require it.
507 string demiFile = Path.Combine(options.CommonScriptsDirectory, "demi.brail");
508 IDictionary<ICompilerInput, string> inputs = GetInput(demiFile, true);
509 ICompilerInput[] inputsAsArray = new List<ICompilerInput>(inputs.Keys).ToArray();
510 BooCompiler compiler = SetupCompiler(inputsAsArray);
511 string outputFile = Path.Combine(baseSavePath, "CommonScripts.dll");
512 compiler.Parameters.OutputAssembly = outputFile;
513 CompilerContext result = compiler.Run();
514 if (result.Errors.Count > 0)
515 throw new RailsException(result.Errors.ToString(true));
516 common = result.GeneratedAssembly;
517 compilations.Clear();
518 return true;
521 // common setup for the compiler
522 private BooCompiler SetupCompiler(ICompilerInput[] files)
524 BooCompiler compiler = new BooCompiler();
525 compiler.Parameters.Ducky = true;
526 compiler.Parameters.Debug = options.Debug;
527 if (options.SaveToDisk)
528 compiler.Parameters.Pipeline = new CompileToFile();
529 else
530 compiler.Parameters.Pipeline = new CompileToMemory();
531 // replace the normal parser with white space agnostic one.
532 compiler.Parameters.Pipeline.RemoveAt(0);
533 compiler.Parameters.Pipeline.Insert(0, new WSABooParsingStep());
534 foreach (ICompilerInput file in files)
536 compiler.Parameters.Input.Add(file);
538 foreach (Assembly assembly in options.AssembliesToReference)
540 compiler.Parameters.References.Add(assembly);
542 compiler.Parameters.OutputType = CompilerOutputType.Library;
543 return compiler;
546 public string ViewRootDir
548 get { return ViewSourceLoader.ViewRootDir; }
551 public BooViewEngineOptions Options
553 get { return options; }
554 set { options = value; }
557 private static void InitializeConfig()
559 InitializeConfig("brail");
561 if (options == null)
563 InitializeConfig("Brail");
566 if (options == null)
568 options = new BooViewEngineOptions();
572 private static void InitializeConfig(string sectionName)
574 options = ConfigurationManager.GetSection(sectionName) as BooViewEngineOptions;
577 private void Log(string msg, params object[] items)
579 if (logger == null || logger.IsDebugEnabled == false)
580 return;
581 logger.DebugFormat(msg, items);
584 private class LayoutViewOutput
586 private BrailBase layout;
587 private TextWriter output;
589 public LayoutViewOutput(TextWriter output, BrailBase layout)
591 this.layout = layout;
592 this.output = output;
595 public BrailBase Layout
597 get { return layout; }
600 public TextWriter Output
602 get { return output; }
606 private class CompilationResult
608 CompilerContext context;
609 BrailPreProcessor processor;
611 public CompilerContext Context
613 get { return context; }
616 public BrailPreProcessor Processor
618 get { return processor; }
621 public CompilationResult(CompilerContext context, BrailPreProcessor processor)
623 this.context = context;
624 this.processor = processor;
628 public bool ConditionalPreProcessingOnly(string name)
630 return String.Equals(
631 Path.GetExtension(name),
632 JSGeneratorFileExtension,
633 StringComparison.InvariantCultureIgnoreCase);