More working tests.
[castle.git] / MonoRail / Castle.MonoRail.Views.Brail / BooViewEngine.cs
blob6f12daee3ffa82889d88de026c2936e1490de1f8
1 // Copyright 2004-2008 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.CompilerServices;
24 using System.Runtime.Serialization;
25 using System.Text;
26 using System.Threading;
27 using System.Web;
28 using Boo.Lang.Compiler;
29 using Boo.Lang.Compiler.IO;
30 using Boo.Lang.Compiler.Pipelines;
31 using Boo.Lang.Compiler.Steps;
32 using Boo.Lang.Parser;
33 using Castle.Core.Logging;
34 using Castle.MonoRail.Framework.Test;
35 using Core;
36 using Framework;
38 public class BooViewEngine : ViewEngineBase, IInitializable
40 private static BooViewEngineOptions options;
42 /// <summary>
43 /// This field holds all the cache of all the
44 /// compiled types (not instances) of all the views that Brail nows of.
45 /// </summary>
46 private readonly Hashtable compilations = Hashtable.Synchronized(
47 new Hashtable(StringComparer.InvariantCultureIgnoreCase));
49 /// <summary>
50 /// used to hold the constructors of types, so we can avoid using
51 /// Activator (which takes a long time
52 /// </summary>
53 private readonly Hashtable constructors = new Hashtable();
55 private string baseSavePath;
57 /// <summary>
58 /// This is used to add a reference to the common scripts for each compiled scripts
59 /// </summary>
60 private Assembly common;
62 private ILogger logger;
64 public override bool SupportsJSGeneration
66 get { return true; }
69 public override string ViewFileExtension
71 get { return ".brail"; }
74 public override string JSGeneratorFileExtension
76 get { return ".brailjs"; }
79 public string ViewRootDir
81 get { return ViewSourceLoader.ViewRootDir; }
84 public BooViewEngineOptions Options
86 get { return options; }
87 set { options = value; }
90 #region IInitializable Members
92 public void Initialize()
94 if (options == null) InitializeConfig();
96 string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
97 Log("Base Directory: " + baseDir);
98 baseSavePath = Path.Combine(baseDir, options.SaveDirectory);
99 Log("Base Save Path: " + baseSavePath);
101 if (options.SaveToDisk && !Directory.Exists(baseSavePath))
103 Directory.CreateDirectory(baseSavePath);
104 Log("Created directory " + baseSavePath);
107 CompileCommonScripts();
109 ViewSourceLoader.ViewChanged += OnViewChanged;
112 #endregion
114 // Process a template name and output the results to the user
115 // This may throw if an error occured and the user is not local (which would
116 // cause the yellow screen of death)
117 public override void Process(String templateName, TextWriter output, IEngineContext context, IController controller,
118 IControllerContext controllerContext)
120 Log("Starting to process request for {0}", templateName);
121 string file = templateName + ViewFileExtension;
122 BrailBase view;
123 // Output may be the layout's child output if a layout exists
124 // or the context.Response.Output if the layout is null
125 LayoutViewOutput layoutViewOutput = GetOutput(output, context, controller, controllerContext);
126 // Will compile on first time, then save the assembly on the cache.
127 view = GetCompiledScriptInstance(file, layoutViewOutput.Output, context, controller, controllerContext);
128 if (controller != null)
129 controller.PreSendView(view);
131 Log("Executing view {0}", templateName);
135 view.Run();
137 catch (Exception e)
139 HandleException(templateName, view, e);
142 if (layoutViewOutput.Layout != null)
144 layoutViewOutput.Layout.SetParent(view);
148 layoutViewOutput.Layout.Run();
150 catch (Exception e)
152 HandleException(controllerContext.LayoutNames[0], layoutViewOutput.Layout, e);
155 Log("Finished executing view {0}", templateName);
156 if (controller != null)
157 controller.PostSendView(view);
160 public override void Process(string templateName, string layoutName, TextWriter output,
161 IDictionary<string, object> parameters)
163 ControllerContext controllerContext = new ControllerContext();
164 if (layoutName != null)
166 controllerContext.LayoutNames = new string[] { layoutName };
168 foreach (KeyValuePair<string, object> pair in parameters)
170 controllerContext.PropertyBag[pair.Key] = pair.Value;
172 Process(templateName, output, null, null, controllerContext);
175 public override void ProcessPartial(string partialName, TextWriter output, IEngineContext context,
176 IController controller, IControllerContext controllerContext)
178 Log("Generating partial for {0}", partialName);
182 string file = ResolveTemplateName(partialName);
183 BrailBase view = GetCompiledScriptInstance(file, output, context, controller, controllerContext);
184 Log("Executing partial view {0}", partialName);
185 view.Run();
186 Log("Finished executing partial view {0}", partialName);
188 catch (Exception ex)
190 if (Logger != null && Logger.IsErrorEnabled)
192 Logger.Error("Could not generate JS", ex);
195 throw new MonoRailException("Error generating partial: " + partialName, ex);
199 public override object CreateJSGenerator(JSCodeGeneratorInfo generatorInfo, IEngineContext context,
200 IController controller,
201 IControllerContext controllerContext)
203 return new BrailJSGenerator(generatorInfo.CodeGenerator, generatorInfo.LibraryGenerator,
204 generatorInfo.Extensions, generatorInfo.ElementExtensions);
207 public override void GenerateJS(string templateName, TextWriter output, JSCodeGeneratorInfo generatorInfo,
208 IEngineContext context, IController controller, IControllerContext controllerContext)
210 Log("Generating JS for {0}", templateName);
214 object generator = CreateJSGenerator(generatorInfo, context, controller, controllerContext);
215 AdjustJavascriptContentType(context);
216 string file = ResolveJSTemplateName(templateName);
217 BrailBase view = GetCompiledScriptInstance(file,
218 //we use the script just to build the generator, not to output to the user
219 new StringWriter(),
220 context, controller, controllerContext);
221 Log("Executing JS view {0}", templateName);
222 view.AddProperty("page", generator);
223 view.Run();
225 output.WriteLine(generator);
226 Log("Finished executing JS view {0}", templateName);
228 catch (Exception ex)
230 if (Logger != null && Logger.IsErrorEnabled)
232 Logger.Error("Could not generate JS", ex);
235 throw new MonoRailException("Error generating JS. Template: " + templateName, ex);
239 /// <summary>
240 /// Wraps the specified content in the layout using the
241 /// context to output the result.
242 /// </summary>
243 /// <param name="contents"></param>
244 /// <param name="context"></param>
245 /// <param name="controller"></param>
246 /// <param name="controllerContext"></param>
247 public override void RenderStaticWithinLayout(String contents, IEngineContext context, IController controller,
248 IControllerContext controllerContext)
250 LayoutViewOutput layoutViewOutput = GetOutput(context.Response.Output, context, controller, controllerContext);
251 layoutViewOutput.Output.Write(contents);
252 // here we don't need to pass parameters from the layout to the view,
253 if (layoutViewOutput.Layout != null)
255 layoutViewOutput.Layout.Run();
259 private void HandleException(string templateName, BrailBase view, Exception e)
261 StringBuilder sb = new StringBuilder();
262 sb.Append("Exception on process view: ").AppendLine(templateName);
263 sb.Append("Last accessed variable: ").Append(view.LastVariableAccessed);
264 string msg = sb.ToString();
265 sb.Append("Exception: ").AppendLine(e.ToString());
266 Log(msg);
267 throw new MonoRailException(msg, e);
270 private void OnViewChanged(object sender, FileSystemEventArgs e)
273 if (Path.GetExtension(e.FullPath).IndexOf(this.ViewFileExtension) == -1 &&
274 Path.GetExtension(e.FullPath).IndexOf(this.JSGeneratorFileExtension) == -1)
276 return;//early return since only watching view extensions and jsgenerator extensions
278 string path = e.FullPath.Substring(ViewRootDir.Length);
279 if (path.Length > 0 && (path[0] == Path.DirectorySeparatorChar ||
280 path[0] == Path.AltDirectorySeparatorChar))
282 path = path.Substring(1);
284 if (path.IndexOf(options.CommonScriptsDirectory) != -1)
286 Log("Detected a change in commons scripts directory " + options.CommonScriptsDirectory + ", recompiling site");
287 // need to invalidate the entire CommonScripts assembly
288 // not worrying about concurrency here, since it is assumed
289 // that changes here are rare. Note, this force a recompile of the
290 // whole site!
293 WaitForFileToBecomeAvailableForReading(e);
294 CompileCommonScripts();
296 catch (Exception ex)
298 // we failed to recompile the commons scripts directory, but because we are running
299 // on another thread here, and exception would kill the application, so we log it
300 // and continue on. CompileCommonScripts() will only change the global state if it has
301 // successfully compiled the commons scripts directory.
302 Log("Failed to recompile the commons scripts directory! {0}", ex);
305 else
307 Log("Detected a change in {0}, removing from complied cache", e.Name);
308 // Will cause a recompilation
309 compilations[path] = null;
313 private static void WaitForFileToBecomeAvailableForReading(FileSystemEventArgs e)
315 // We may need to wait while the file is being written and closed to disk
316 int retries = 10;
317 bool successfullyOpenedFile = false;
318 while (retries != 0 && successfullyOpenedFile == false)
320 retries -= 1;
323 using (File.OpenRead(e.FullPath))
325 successfullyOpenedFile = true;
328 catch (IOException)
330 //The file is probably in locked because it is currently being written to,
331 // will wait a while for it to be freed.
332 // again, this isn't something that need to be very robust, it runs on a separate thread
333 // and if it fails, it is not going to do any damage
334 Thread.Sleep(250);
339 public void SetViewSourceLoader(IViewSourceLoader loader)
341 ViewSourceLoader = loader;
344 // Get configuration options if they exists, if they do not exist, load the default ones
345 // Create directory to save the compiled assemblies if required.
346 // pre-compile the common scripts
347 public override void Service(IServiceProvider serviceProvider)
349 base.Service(serviceProvider);
350 ILoggerFactory loggerFactory = serviceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
351 if (loggerFactory != null)
352 logger = loggerFactory.Create(GetType());
355 // Check if a layout has been defined. If it was, then the layout would be created
356 // and will take over the output, otherwise, the context.Reposne.Output is used,
357 // and layout is null
358 private LayoutViewOutput GetOutput(TextWriter output, IEngineContext context, IController controller,
359 IControllerContext controllerContext)
361 BrailBase layout = null;
362 if (controllerContext.LayoutNames != null && controllerContext.LayoutNames.Length != 0)
364 string layoutTemplate = controllerContext.LayoutNames[0];
365 if (layoutTemplate.StartsWith("/") == false)
367 layoutTemplate = "layouts\\" + layoutTemplate;
369 string layoutFilename = layoutTemplate + ViewFileExtension;
370 layout = GetCompiledScriptInstance(layoutFilename, output,
371 context, controller, controllerContext);
372 output = layout.ChildOutput = new StringWriter();
374 return new LayoutViewOutput(output, layout);
377 /// <summary>
378 /// This takes a filename and return an instance of the view ready to be used.
379 /// If the file does not exist, an exception is raised
380 /// The cache is checked to see if the file has already been compiled, and it had been
381 /// a check is made to see that the compiled instance is newer then the file's modification date.
382 /// If the file has not been compiled, or the version on disk is newer than the one in memory, a new
383 /// version is compiled.
384 /// Finally, an instance is created and returned
385 /// </summary>
386 public BrailBase GetCompiledScriptInstance(
387 string file,
388 TextWriter output,
389 IEngineContext context,
390 IController controller, IControllerContext controllerContext)
392 bool batch = options.BatchCompile;
394 // normalize filename - replace / or \ to the system path seperator
395 string filename = file.Replace('/', Path.DirectorySeparatorChar)
396 .Replace('\\', Path.DirectorySeparatorChar);
398 Log("Getting compiled instnace of {0}", filename);
400 Type type;
402 if (compilations.ContainsKey(filename))
404 type = (Type)compilations[filename];
405 if (type != null)
407 Log("Got compiled instance of {0} from cache", filename);
408 return CreateBrailBase(context, controller, controllerContext, output, type);
410 // if file is in compilations and the type is null,
411 // this means that we need to recompile. Since this usually means that
412 // the file was changed, we'll set batch to false and procceed to compile just
413 // this file.
414 Log("Cache miss! Need to recompile {0}", filename);
415 batch = false;
418 type = CompileScript(filename, batch);
420 if (type == null)
422 throw new MonoRailException("Could not find a view with path " + filename);
425 return CreateBrailBase(context, controller, controllerContext, output, type);
428 private BrailBase CreateBrailBase(IEngineContext context, IController controller, IControllerContext controllerContext,
429 TextWriter output, Type type)
431 ConstructorInfo constructor = (ConstructorInfo)constructors[type];
432 BrailBase self = (BrailBase)FormatterServices.GetUninitializedObject(type);
433 constructor.Invoke(self, new object[] { this, output, context, controller, controllerContext });
434 return self;
437 // Compile a script (or all scripts in a directory), save the compiled result
438 // to the cache and return the compiled type.
439 // If an error occurs in batch compilation, then an attempt is made to compile just the single
440 // request file.
441 [MethodImpl(MethodImplOptions.Synchronized)]
442 public Type CompileScript(string filename, bool batch)
444 IDictionary<ICompilerInput, string> inputs2FileName = GetInput(filename, batch);
445 string name = NormalizeName(filename);
446 Log("Compiling {0} to {1} with batch: {2}", filename, name, batch);
447 CompilationResult result = DoCompile(inputs2FileName.Keys, name);
449 if (result.Context.Errors.Count > 0)
451 if (batch == false)
453 RaiseCompilationException(filename, inputs2FileName, result);
455 //error compiling a batch, let's try a single file
456 return CompileScript(filename, false);
458 Type type;
459 foreach (ICompilerInput input in inputs2FileName.Keys)
461 string viewName = Path.GetFileNameWithoutExtension(input.Name);
462 string typeName = TransformToBrailStep.GetViewTypeName(viewName);
463 type = result.Context.GeneratedAssembly.GetType(typeName);
464 Log("Adding {0} to the cache", type.FullName);
465 constructors[type] = type.GetConstructor(new Type[]
467 typeof(BooViewEngine),
468 typeof(TextWriter),
469 typeof(IEngineContext),
470 typeof(IController),
471 typeof(IControllerContext)
473 compilations[inputs2FileName[input]] = type;
475 type = (Type)compilations[filename];
476 return type;
479 private void RaiseCompilationException(string filename, IDictionary<ICompilerInput, string> inputs2FileName,
480 CompilationResult result)
482 string errors = result.Context.Errors.ToString(true);
483 Log("Failed to compile {0} because {1}", filename, errors);
484 StringBuilder code = new StringBuilder();
485 foreach (ICompilerInput input in inputs2FileName.Keys)
487 code.AppendLine()
488 .Append(result.Processor.GetInputCode(input))
489 .AppendLine();
491 throw new HttpParseException("Error compiling Brail code",
492 result.Context.Errors[0],
493 filename,
494 code.ToString(), result.Context.Errors[0].LexicalInfo.Line);
497 // If batch compilation is set to true, this would return all the view scripts
498 // in the director (not recursive!)
499 // Otherwise, it would return just the single file
500 private IDictionary<ICompilerInput, string> GetInput(string filename, bool batch)
502 Dictionary<ICompilerInput, string> input2FileName = new Dictionary<ICompilerInput, string>();
503 if (batch == false)
505 input2FileName.Add(CreateInput(filename), filename);
506 return input2FileName;
508 // use the System.IO.Path to get the folder name even though
509 // we are using the ViewSourceLoader to load the actual file
510 string directory = Path.GetDirectoryName(filename);
511 foreach (string file in ViewSourceLoader.ListViews(directory, this.ViewFileExtension, this.JSGeneratorFileExtension))
513 ICompilerInput input = CreateInput(file);
514 input2FileName.Add(input, file);
516 return input2FileName;
519 // create an input from a resource name
520 public ICompilerInput CreateInput(string name)
522 IViewSource viewSrc = ViewSourceLoader.GetViewSource(name);
523 if (viewSrc == null)
525 throw new MonoRailException("{0} is not a valid view", name);
527 // I need to do it this way because I can't tell
528 // when to dispose of the stream.
529 // It is not expected that this will be a big problem, the string
530 // will go away after the compile is done with them.
531 using (StreamReader stream = new StreamReader(viewSrc.OpenViewStream()))
533 return new StringInput(name, stream.ReadToEnd());
537 /// <summary>
538 /// Perform the actual compilation of the scripts
539 /// Things to note here:
540 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
541 /// * If a common scripts assembly exist, it is also referenced
542 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
543 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
544 /// this allows to use naked parameters such as (output context.IsLocal) without using
545 /// any special syntax
546 /// * The FixTryGetParameterConditionalChecks is run afterward, to transform "if ?Error" to "if not ?Error isa IgnoreNull"
547 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
548 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
549 /// date and list without accidently using the Boo.Lang.BuiltIn versions
550 /// </summary>
551 /// <param name="files"></param>
552 /// <param name="name"></param>
553 /// <returns></returns>
554 private CompilationResult DoCompile(IEnumerable<ICompilerInput> files, string name)
556 ICompilerInput[] filesAsArray = new List<ICompilerInput>(files).ToArray();
557 BooCompiler compiler = SetupCompiler(filesAsArray);
558 string filename = Path.Combine(baseSavePath, name);
559 compiler.Parameters.OutputAssembly = filename;
560 // this is here and not in SetupCompiler since CompileCommon is also
561 // using SetupCompiler, and we don't want reference to the old common from the new one
562 if (common != null)
563 compiler.Parameters.References.Add(common);
564 // pre procsssor needs to run before the parser
565 BrailPreProcessor processor = new BrailPreProcessor(this);
566 compiler.Parameters.Pipeline.Insert(0, processor);
567 // inserting the add class step after the parser
568 compiler.Parameters.Pipeline.Insert(2, new TransformToBrailStep(options));
569 compiler.Parameters.Pipeline.Replace(typeof(ProcessMethodBodiesWithDuckTyping),
570 new ReplaceUknownWithParameters());
571 compiler.Parameters.Pipeline.Replace(typeof(ExpandDuckTypedExpressions),
572 new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
573 compiler.Parameters.Pipeline.Replace(typeof(InitializeTypeSystemServices),
574 new InitializeCustomTypeSystem());
575 compiler.Parameters.Pipeline.InsertBefore(typeof(ReplaceUknownWithParameters),
576 new FixTryGetParameterConditionalChecks());
577 compiler.Parameters.Pipeline.RemoveAt(compiler.Parameters.Pipeline.Find(typeof(IntroduceGlobalNamespaces)));
579 return new CompilationResult(compiler.Run(), processor);
582 // Return the output filename for the generated assembly
583 // The filename is dependant on whatever we are doing a batch
584 // compile or not, if it's a batch compile, then the directory name
585 // is used, if it's just a single file, we're using the file's name.
586 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
587 // should never include it since I'm converting this to a relative path
588 public string NormalizeName(string filename)
590 string name = filename;
591 name = name.Replace(Path.AltDirectorySeparatorChar, '_');
592 name = name.Replace(Path.DirectorySeparatorChar, '_');
594 return name + "_BrailView.dll";
597 // Compile all the common scripts to a common assemblies
598 // an error in the common scripts would raise an exception.
599 public bool CompileCommonScripts()
601 if (options.CommonScriptsDirectory == null)
602 return false;
604 // the demi.boo is stripped, but GetInput require it.
605 string demiFile = Path.Combine(options.CommonScriptsDirectory, "demi.brail");
606 IDictionary<ICompilerInput, string> inputs = GetInput(demiFile, true);
607 ICompilerInput[] inputsAsArray = new List<ICompilerInput>(inputs.Keys).ToArray();
608 BooCompiler compiler = SetupCompiler(inputsAsArray);
609 string outputFile = Path.Combine(baseSavePath, "CommonScripts.dll");
610 compiler.Parameters.OutputAssembly = outputFile;
611 CompilerContext result = compiler.Run();
612 if (result.Errors.Count > 0)
613 throw new MonoRailException(result.Errors.ToString(true));
614 common = result.GeneratedAssembly;
615 compilations.Clear();
616 return true;
619 // common setup for the compiler
620 private static BooCompiler SetupCompiler(IEnumerable<ICompilerInput> files)
622 BooCompiler compiler = new BooCompiler();
623 compiler.Parameters.Ducky = true;
624 compiler.Parameters.Debug = options.Debug;
625 if (options.SaveToDisk)
626 compiler.Parameters.Pipeline = new CompileToFile();
627 else
628 compiler.Parameters.Pipeline = new CompileToMemory();
629 // replace the normal parser with white space agnostic one.
630 compiler.Parameters.Pipeline.RemoveAt(0);
631 compiler.Parameters.Pipeline.Insert(0, new WSABooParsingStep());
632 foreach (ICompilerInput file in files)
634 compiler.Parameters.Input.Add(file);
636 foreach (Assembly assembly in options.AssembliesToReference)
638 compiler.Parameters.References.Add(assembly);
640 compiler.Parameters.OutputType = CompilerOutputType.Library;
641 return compiler;
644 private static void InitializeConfig()
646 InitializeConfig("brail");
648 if (options == null)
650 InitializeConfig("Brail");
653 if (options == null)
655 options = new BooViewEngineOptions();
659 private static void InitializeConfig(string sectionName)
661 options = ConfigurationManager.GetSection(sectionName) as BooViewEngineOptions;
664 private void Log(string msg, params object[] items)
666 if (logger == null || logger.IsDebugEnabled == false)
667 return;
668 logger.DebugFormat(msg, items);
671 public bool ConditionalPreProcessingOnly(string name)
673 return String.Equals(
674 Path.GetExtension(name),
675 JSGeneratorFileExtension,
676 StringComparison.InvariantCultureIgnoreCase);
679 #region Nested type: CompilationResult
681 private class CompilationResult
683 private readonly CompilerContext context;
684 private readonly BrailPreProcessor processor;
686 public CompilationResult(CompilerContext context, BrailPreProcessor processor)
688 this.context = context;
689 this.processor = processor;
692 public CompilerContext Context
694 get { return context; }
697 public BrailPreProcessor Processor
699 get { return processor; }
703 #endregion
705 #region Nested type: LayoutViewOutput
707 private class LayoutViewOutput
709 private readonly BrailBase layout;
710 private readonly TextWriter output;
712 public LayoutViewOutput(TextWriter output, BrailBase layout)
714 this.layout = layout;
715 this.output = output;
718 public BrailBase Layout
720 get { return layout; }
723 public TextWriter Output
725 get { return output; }
729 #endregion