- Fixed MR-84
[castle.git] / MonoRail / Castle.MonoRail.Views.Brail / BooViewEngine.cs
blobd3a21052c2e46f099eede2bc0242ab090f4527d6
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 Core.Logging;
32 using Framework.Helpers;
33 using Core;
34 using Framework;
36 public class BooViewEngine : ViewEngineBase, IInitializable
38 private static BooViewEngineOptions options;
40 /// <summary>
41 /// This field holds all the cache of all the
42 /// compiled types (not instances) of all the views that Brail nows of.
43 /// </summary>
44 private readonly Hashtable compilations = Hashtable.Synchronized(
45 new Hashtable(StringComparer.InvariantCultureIgnoreCase));
47 private string baseSavePath;
49 /// <summary>
50 /// This is used to add a reference to the common scripts for each compiled scripts
51 /// </summary>
52 private Assembly common;
54 /// <summary>
55 /// used to hold the constructors of types, so we can avoid using
56 /// Activator (which takes a long time
57 /// </summary>
58 private readonly Hashtable constructors = new Hashtable();
60 private ILogger logger;
62 public override bool SupportsJSGeneration
64 get { return true; }
67 public override string ViewFileExtension
69 get { return ".brail"; }
72 public override string JSGeneratorFileExtension
74 get { return ".brailjs"; }
77 public string ViewRootDir
79 get { return ViewSourceLoader.ViewRootDir; }
82 public BooViewEngineOptions Options
84 get { return options; }
85 set { options = value; }
88 #region IInitializable Members
90 public void Initialize()
92 if (options == null) InitializeConfig();
94 string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
95 Log("Base Directory: " + baseDir);
96 baseSavePath = Path.Combine(baseDir, options.SaveDirectory);
97 Log("Base Save Path: " + baseSavePath);
99 if (options.SaveToDisk && !Directory.Exists(baseSavePath))
101 Directory.CreateDirectory(baseSavePath);
102 Log("Created directory " + baseSavePath);
105 CompileCommonScripts();
107 ViewSourceLoader.ViewChanged += new FileSystemEventHandler(OnViewChanged);
110 #endregion
112 // Process a template name and output the results to the user
113 // This may throw if an error occured and the user is not local (which would
114 // cause the yellow screen of death)
115 public override void Process(IRailsEngineContext context, IController controller, string templateName)
117 Process(context.Response.Output, context, controller, templateName);
120 public override void Process(
121 TextWriter output, IRailsEngineContext context, IController controller,
122 string templateName)
124 Log("Starting to process request for {0}", templateName);
125 string file = templateName + ViewFileExtension;
126 BrailBase view;
127 // Output may be the layout's child output if a layout exists
128 // or the context.Response.Output if the layout is null
129 LayoutViewOutput layoutViewOutput = GetOutput(output, context, controller);
130 // Will compile on first time, then save the assembly on the cache.
131 view = GetCompiledScriptInstance(file, layoutViewOutput.Output, context, controller);
132 controller.PreSendView(view);
134 Log("Executing view {0}", templateName);
138 view.Run();
140 catch (Exception e)
142 HandleException(templateName, view, e);
145 if (layoutViewOutput.Layout != null)
147 layoutViewOutput.Layout.SetParent(view);
150 layoutViewOutput.Layout.Run();
152 catch (Exception e)
154 HandleException(controller.LayoutName, layoutViewOutput.Layout, e);
157 Log("Finished executing view {0}", templateName);
158 controller.PostSendView(view);
161 private void HandleException(string templateName, BrailBase view, Exception e)
163 StringBuilder sb = new StringBuilder();
164 sb.Append("Exception on process view: ").AppendLine(templateName);
165 sb.Append("Last accessed variable: ").Append(view.LastVariableAccessed);
166 string msg = sb.ToString();
167 sb.Append("Exception: ").AppendLine(e.ToString());
168 Log(msg);
169 throw new MonoRailException(msg, e);
172 public override void ProcessPartial(
173 TextWriter output, IRailsEngineContext context, IController controller,
174 string partialName)
176 Log("Generating partial for {0}", partialName);
180 string file = ResolveTemplateName(partialName);
181 BrailBase view = GetCompiledScriptInstance(file, output, context, controller);
182 Log("Executing partial view {0}", partialName);
183 view.Run();
184 Log("Finished executing partial view {0}", partialName);
186 catch (Exception ex)
188 if (Logger != null && Logger.IsErrorEnabled)
190 Logger.Error("Could not generate JS", ex);
193 throw new MonoRailException("Error generating partial: " + partialName, ex);
197 public override object CreateJSGenerator(IRailsEngineContext context)
199 return new BrailJSGenerator(new PrototypeHelper.JSGenerator(context));
202 public override void GenerateJS(
203 TextWriter output, IRailsEngineContext context, IController controller,
204 string templateName)
206 Log("Generating JS for {0}", templateName);
210 object generator = CreateJSGenerator(context);
211 AdjustJavascriptContentType(context);
212 string file = ResolveJSTemplateName(templateName);
213 BrailBase view = GetCompiledScriptInstance(file,
214 //we use the script just to build the generator, not to output to the user
215 new StringWriter(),
216 context, controller);
217 Log("Executing JS view {0}", templateName);
218 view.AddProperty("page", generator);
219 view.Run();
221 output.WriteLine(generator);
222 Log("Finished executing JS view {0}", templateName);
224 catch (Exception ex)
226 if (Logger != null && Logger.IsErrorEnabled)
228 Logger.Error("Could not generate JS", ex);
231 throw new MonoRailException("Error generating JS. Template: " + templateName, ex);
235 // Send the contents text directly to the user, only adding the layout if neccecary
236 public override void ProcessContents(IRailsEngineContext context, IController controller, string contents)
238 LayoutViewOutput layoutViewOutput = GetOutput(controller.Response.Output, context, controller);
239 layoutViewOutput.Output.Write(contents);
240 // here we don't need to pass parameters from the layout to the view,
241 if (layoutViewOutput.Layout != null)
242 layoutViewOutput.Layout.Run();
245 private void OnViewChanged(object sender, FileSystemEventArgs e)
247 string path = e.FullPath.Substring(ViewRootDir.Length);
248 if (path.Length > 0 && (path[0] == Path.DirectorySeparatorChar ||
249 path[0] == Path.AltDirectorySeparatorChar))
251 path = path.Substring(1);
253 if (path.IndexOf(options.CommonScriptsDirectory) != -1)
255 Log("Detected a change in commons scripts directory " + options.CommonScriptsDirectory + ", recompiling site");
256 // need to invalidate the entire CommonScripts assembly
257 // not worrying about concurrency here, since it is assumed
258 // that changes here are rare. Note, this force a recompile of the
259 // whole site!
262 WaitForFileToBecomeAvailableForReading(e);
263 CompileCommonScripts();
265 catch (Exception ex)
267 // we failed to recompile the commons scripts directory, but because we are running
268 // on another thread here, and exception would kill the application, so we log it
269 // and continue on. CompileCommonScripts() will only change the global state if it has
270 // successfully compiled the commons scripts directory.
271 Log("Failed to recompile the commons scripts directory! {0}", ex);
274 else
276 Log("Detected a change in {0}, removing from complied cache", e.Name);
277 // Will cause a recompilation
278 compilations[path] = null;
282 private static void WaitForFileToBecomeAvailableForReading(FileSystemEventArgs e)
284 // We may need to wait while the file is being written and closed to disk
285 int retries = 10;
286 bool successfullyOpenedFile = false;
287 while (retries != 0 && successfullyOpenedFile == false)
289 retries -= 1;
292 using (File.OpenRead(e.FullPath))
294 successfullyOpenedFile = true;
297 catch (IOException)
299 //The file is probably in locked because it is currently being written to,
300 // will wait a while for it to be freed.
301 // again, this isn't something that need to be very robust, it runs on a separate thread
302 // and if it fails, it is not going to do any damage
303 Thread.Sleep(250);
308 internal void SetViewSourceLoader(IViewSourceLoader loader)
310 this.ViewSourceLoader = loader;
313 // Get configuration options if they exists, if they do not exist, load the default ones
314 // Create directory to save the compiled assemblies if required.
315 // pre-compile the common scripts
316 public override void Service(IServiceProvider serviceProvider)
318 base.Service(serviceProvider);
319 ILoggerFactory loggerFactory = serviceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
320 if (loggerFactory != null)
321 logger = loggerFactory.Create(GetType());
324 // Check if a layout has been defined. If it was, then the layout would be created
325 // and will take over the output, otherwise, the context.Reposne.Output is used,
326 // and layout is null
327 private LayoutViewOutput GetOutput(TextWriter output, IRailsEngineContext context, IController controller)
329 BrailBase layout = null;
330 if (controller.LayoutName != null)
332 string layoutTemplate = "layouts\\" + controller.LayoutName;
333 string layoutFilename = layoutTemplate + ViewFileExtension;
334 layout = GetCompiledScriptInstance(layoutFilename, output,
335 context, controller);
336 output = layout.ChildOutput = new StringWriter();
338 return new LayoutViewOutput(output, layout);
341 /// <summary>
342 /// This takes a filename and return an instance of the view ready to be used.
343 /// If the file does not exist, an exception is raised
344 /// The cache is checked to see if the file has already been compiled, and it had been
345 /// a check is made to see that the compiled instance is newer then the file's modification date.
346 /// If the file has not been compiled, or the version on disk is newer than the one in memory, a new
347 /// version is compiled.
348 /// Finally, an instance is created and returned
349 /// </summary>
350 public BrailBase GetCompiledScriptInstance(
351 string file,
352 TextWriter output,
353 IRailsEngineContext context,
354 IController controller)
356 bool batch = options.BatchCompile;
357 // normalize filename - replace / or \ to the system path seperator
358 string filename = file.Replace('/', Path.DirectorySeparatorChar)
359 .Replace('\\', Path.DirectorySeparatorChar);
360 Log("Getting compiled instnace of {0}", filename);
361 Type type;
362 if (compilations.ContainsKey(filename))
364 type = (Type)compilations[filename];
365 if (type != null)
367 Log("Got compiled instance of {0} from cache", filename);
368 return CreateBrailBase(context, controller, output, type);
370 // if file is in compilations and the type is null,
371 // this means that we need to recompile. Since this usually means that
372 // the file was changed, we'll set batch to false and procceed to compile just
373 // this file.
374 Log("Cache miss! Need to recompile {0}", filename);
375 batch = false;
377 type = CompileScript(filename, batch);
378 if (type == null)
380 throw new MonoRailException("Could not find a view with path " + filename);
382 return CreateBrailBase(context, controller, output, type);
385 private BrailBase CreateBrailBase(IRailsEngineContext context, IController controller, TextWriter output, Type type)
387 ConstructorInfo constructor = (ConstructorInfo)constructors[type];
388 BrailBase self = (BrailBase)FormatterServices.GetUninitializedObject(type);
389 constructor.Invoke(self, new object[] { this, output, context, controller });
390 return self;
393 // Compile a script (or all scripts in a directory), save the compiled result
394 // to the cache and return the compiled type.
395 // If an error occurs in batch compilation, then an attempt is made to compile just the single
396 // request file.
397 public Type CompileScript(string filename, bool batch)
399 IDictionary<ICompilerInput, string> inputs2FileName = GetInput(filename, batch);
400 string name = NormalizeName(filename);
401 Log("Compiling {0} to {1} with batch: {2}", filename, name, batch);
402 CompilationResult result = DoCompile(inputs2FileName.Keys, name);
404 if (result.Context.Errors.Count > 0)
406 if (batch == false)
408 RaiseCompilationException(filename, inputs2FileName, result);
410 //error compiling a batch, let's try a single file
411 return CompileScript(filename, false);
413 Type type;
414 foreach (ICompilerInput input in inputs2FileName.Keys)
416 string viewName = Path.GetFileNameWithoutExtension(input.Name);
417 string typeName = TransformToBrailStep.GetViewTypeName(viewName);
418 type = result.Context.GeneratedAssembly.GetType(typeName);
419 Log("Adding {0} to the cache", type.FullName);
420 compilations[inputs2FileName[input]] = type;
421 constructors[type] = type.GetConstructor(new Type[]
423 typeof (BooViewEngine),
424 typeof (TextWriter),
425 typeof (IRailsEngineContext),
426 typeof (Controller)
429 type = (Type)compilations[filename];
430 return type;
433 private void RaiseCompilationException(string filename, IDictionary<ICompilerInput, string> inputs2FileName, CompilationResult result)
435 string errors = result.Context.Errors.ToString(true);
436 Log("Failed to compile {0} because {1}", filename, errors);
437 StringBuilder msg = new StringBuilder();
438 msg.Append("Error during compile:")
439 .Append(Environment.NewLine)
440 .Append(errors)
441 .Append(Environment.NewLine);
443 foreach (ICompilerInput input in inputs2FileName.Keys)
445 msg.Append("Input (").Append(input.Name).Append(")")
446 .Append(Environment.NewLine);
447 msg.Append(result.Processor.GetInputCode(input))
448 .Append(Environment.NewLine);
450 throw new MonoRailException(msg.ToString());
453 // If batch compilation is set to true, this would return all the view scripts
454 // in the director (not recursive!)
455 // Otherwise, it would return just the single file
456 private IDictionary<ICompilerInput, string> GetInput(string filename, bool batch)
458 Dictionary<ICompilerInput, string> input2FileName = new Dictionary<ICompilerInput, string>();
459 if (batch == false)
461 input2FileName.Add(CreateInput(filename), filename);
462 return input2FileName;
464 // use the System.IO.Path to get the folder name even though
465 // we are using the ViewSourceLoader to load the actual file
466 string directory = Path.GetDirectoryName(filename);
467 foreach (string file in ViewSourceLoader.ListViews(directory))
469 ICompilerInput input = CreateInput(file);
470 input2FileName.Add(input, file);
472 return input2FileName;
475 // create an input from a resource name
476 public ICompilerInput CreateInput(string name)
478 IViewSource viewSrc = ViewSourceLoader.GetViewSource(name);
479 if (viewSrc == null)
481 throw new MonoRailException("{0} is not a valid view", name);
483 // I need to do it this way because I can't tell
484 // when to dispose of the stream.
485 // It is not expected that this will be a big problem, the string
486 // will go away after the compile is done with them.
487 using (StreamReader stream = new StreamReader(viewSrc.OpenViewStream()))
489 return new StringInput(name, stream.ReadToEnd());
493 /// <summary>
494 /// Perform the actual compilation of the scripts
495 /// Things to note here:
496 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
497 /// * If a common scripts assembly exist, it is also referenced
498 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
499 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
500 /// this allows to use naked parameters such as (output context.IsLocal) without using
501 /// any special syntax
502 /// * The FixTryGetParameterConditionalChecks is run afterward, to transform "if ?Error" to "if not ?Error isa IgnoreNull"
503 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
504 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
505 /// date and list without accidently using the Boo.Lang.BuiltIn versions
506 /// </summary>
507 /// <param name="files"></param>
508 /// <param name="name"></param>
509 /// <returns></returns>
510 private CompilationResult DoCompile(IEnumerable<ICompilerInput> files, string name)
512 ICompilerInput[] filesAsArray = new List<ICompilerInput>(files).ToArray();
513 BooCompiler compiler = SetupCompiler(filesAsArray);
514 string filename = Path.Combine(baseSavePath, name);
515 compiler.Parameters.OutputAssembly = filename;
516 // this is here and not in SetupCompiler since CompileCommon is also
517 // using SetupCompiler, and we don't want reference to the old common from the new one
518 if (common != null)
519 compiler.Parameters.References.Add(common);
520 // pre procsssor needs to run before the parser
521 BrailPreProcessor processor = new BrailPreProcessor(this);
522 compiler.Parameters.Pipeline.Insert(0, processor);
523 // inserting the add class step after the parser
524 compiler.Parameters.Pipeline.Insert(2, new TransformToBrailStep(options));
525 compiler.Parameters.Pipeline.Replace(typeof(ProcessMethodBodiesWithDuckTyping),
526 new ReplaceUknownWithParameters());
527 compiler.Parameters.Pipeline.Replace(typeof(ExpandDuckTypedExpressions),
528 new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
529 compiler.Parameters.Pipeline.Replace(typeof(InitializeTypeSystemServices),
530 new InitializeCustomTypeSystem());
531 compiler.Parameters.Pipeline.InsertBefore(typeof(ReplaceUknownWithParameters),
532 new FixTryGetParameterConditionalChecks());
533 compiler.Parameters.Pipeline.RemoveAt(compiler.Parameters.Pipeline.Find(typeof(IntroduceGlobalNamespaces)));
535 return new CompilationResult(compiler.Run(), processor);
538 // Return the output filename for the generated assembly
539 // The filename is dependant on whatever we are doing a batch
540 // compile or not, if it's a batch compile, then the directory name
541 // is used, if it's just a single file, we're using the file's name.
542 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
543 // should never include it since I'm converting this to a relative path
544 public string NormalizeName(string filename)
546 string name = filename;
547 name = name.Replace(Path.AltDirectorySeparatorChar, '_');
548 name = name.Replace(Path.DirectorySeparatorChar, '_');
550 return name + "_BrailView.dll";
553 // Compile all the common scripts to a common assemblies
554 // an error in the common scripts would raise an exception.
555 public bool CompileCommonScripts()
557 if (options.CommonScriptsDirectory == null)
558 return false;
560 // the demi.boo is stripped, but GetInput require it.
561 string demiFile = Path.Combine(options.CommonScriptsDirectory, "demi.brail");
562 IDictionary<ICompilerInput, string> inputs = GetInput(demiFile, true);
563 ICompilerInput[] inputsAsArray = new List<ICompilerInput>(inputs.Keys).ToArray();
564 BooCompiler compiler = SetupCompiler(inputsAsArray);
565 string outputFile = Path.Combine(baseSavePath, "CommonScripts.dll");
566 compiler.Parameters.OutputAssembly = outputFile;
567 CompilerContext result = compiler.Run();
568 if (result.Errors.Count > 0)
569 throw new MonoRailException(result.Errors.ToString(true));
570 common = result.GeneratedAssembly;
571 compilations.Clear();
572 return true;
575 // common setup for the compiler
576 private static BooCompiler SetupCompiler(IEnumerable<ICompilerInput> files)
578 BooCompiler compiler = new BooCompiler();
579 compiler.Parameters.Ducky = true;
580 compiler.Parameters.Debug = options.Debug;
581 if (options.SaveToDisk)
582 compiler.Parameters.Pipeline = new CompileToFile();
583 else
584 compiler.Parameters.Pipeline = new CompileToMemory();
585 // replace the normal parser with white space agnostic one.
586 compiler.Parameters.Pipeline.RemoveAt(0);
587 compiler.Parameters.Pipeline.Insert(0, new WSABooParsingStep());
588 foreach (ICompilerInput file in files)
590 compiler.Parameters.Input.Add(file);
592 foreach (Assembly assembly in options.AssembliesToReference)
594 compiler.Parameters.References.Add(assembly);
596 compiler.Parameters.OutputType = CompilerOutputType.Library;
597 return compiler;
600 private static void InitializeConfig()
602 InitializeConfig("brail");
604 if (options == null)
606 InitializeConfig("Brail");
609 if (options == null)
611 options = new BooViewEngineOptions();
615 private static void InitializeConfig(string sectionName)
617 options = ConfigurationManager.GetSection(sectionName) as BooViewEngineOptions;
620 private void Log(string msg, params object[] items)
622 if (logger == null || logger.IsDebugEnabled == false)
623 return;
624 logger.DebugFormat(msg, items);
627 public bool ConditionalPreProcessingOnly(string name)
629 return String.Equals(
630 Path.GetExtension(name),
631 JSGeneratorFileExtension,
632 StringComparison.InvariantCultureIgnoreCase);
635 #region Nested type: CompilationResult
637 private class CompilationResult
639 private readonly CompilerContext context;
640 private readonly BrailPreProcessor processor;
642 public CompilationResult(CompilerContext context, BrailPreProcessor processor)
644 this.context = context;
645 this.processor = processor;
648 public CompilerContext Context
650 get { return context; }
653 public BrailPreProcessor Processor
655 get { return processor; }
659 #endregion
661 #region Nested type: LayoutViewOutput
663 private class LayoutViewOutput
665 private readonly BrailBase layout;
666 private readonly TextWriter output;
668 public LayoutViewOutput(TextWriter output, BrailBase layout)
670 this.layout = layout;
671 this.output = output;
674 public BrailBase Layout
676 get { return layout; }
679 public TextWriter Output
681 get { return output; }
685 #endregion