Fixing a bug with the new FixTryGetParameterConditionalChecks - which would always...
[castle.git] / MonoRail / Castle.MonoRail.Views.Brail / BooViewEngine.cs
blob98b653fd6013fdc8d48276a1b2f9680cc40c85df
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.Logging;
32 using Castle.MonoRail.Framework.Helpers;
33 using Core;
34 using Framework;
36 public class BooViewEngine : ViewEngineBase, IInitializable
38 private static BooViewEngineOptions options;
39 private string baseSavePath;
41 /// <summary>
42 /// This is used to add a reference to the common scripts for each compiled scripts
43 /// </summary>
44 private Assembly common;
46 /// <summary>
47 /// This field holds all the cache of all the
48 /// compiled types (not instances) of all the views that Brail nows of.
49 /// </summary>
50 private readonly Hashtable compilations = Hashtable.Synchronized(
51 new Hashtable(StringComparer.InvariantCultureIgnoreCase));
53 /// <summary>
54 /// used to hold the constructors of types, so we can avoid using
55 /// Activator (which takes a long time
56 /// </summary>
57 private Hashtable constructors = new Hashtable();
59 private ILogger logger;
61 public override bool SupportsJSGeneration
63 get { return true; }
66 public override string ViewFileExtension
68 get { return ".brail"; }
71 public override string JSGeneratorFileExtension
73 get { return ".brailjs"; }
76 public string ViewRootDir
78 get { return ViewSourceLoader.ViewRootDir; }
81 public BooViewEngineOptions Options
83 get { return options; }
84 set { options = value; }
87 #region IInitializable Members
89 public void Initialize()
91 if (options == null) InitializeConfig();
93 string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
94 Log("Base Directory: " + baseDir);
95 baseSavePath = Path.Combine(baseDir, options.SaveDirectory);
96 Log("Base Save Path: " + baseSavePath);
98 if (options.SaveToDisk && !Directory.Exists(baseSavePath))
100 Directory.CreateDirectory(baseSavePath);
101 Log("Created directory " + baseSavePath);
104 CompileCommonScripts();
106 ViewSourceLoader.ViewChanged += new FileSystemEventHandler(OnViewChanged);
109 #endregion
111 public override bool HasTemplate(string templateName)
113 if (Path.HasExtension(templateName))
114 return ViewSourceLoader.HasTemplate(templateName);
115 return ViewSourceLoader.HasTemplate(templateName + ViewFileExtension);
118 // Process a template name and output the results to the user
119 // This may throw if an error occured and the user is not local (which would
120 // cause the yellow screen of death)
121 public override void Process(IRailsEngineContext context, Controller controller, string templateName)
123 Process(context.Response.Output, context, controller, templateName);
126 public override void Process(TextWriter output, IRailsEngineContext context, Controller controller,
127 string templateName)
129 Log("Starting to process request for {0}", templateName);
130 string file = templateName + ViewFileExtension;
131 BrailBase view;
132 // Output may be the layout's child output if a layout exists
133 // or the context.Response.Output if the layout is null
134 LayoutViewOutput layoutViewOutput = GetOutput(output, context, controller);
135 // Will compile on first time, then save the assembly on the cache.
136 view = GetCompiledScriptInstance(file, layoutViewOutput.Output, context, controller);
137 controller.PreSendView(view);
138 Log("Executing view {0}", templateName);
139 view.Run();
140 if (layoutViewOutput.Layout != null)
142 layoutViewOutput.Layout.SetParent(view);
143 layoutViewOutput.Layout.Run();
145 Log("Finished executing view {0}", templateName);
146 controller.PostSendView(view);
149 public override void ProcessPartial(TextWriter output, IRailsEngineContext context, Controller controller,
150 string partialName)
152 Log("Generating partial for {0}", partialName);
156 string file = ResolveTemplateName(partialName, ViewFileExtension);
157 BrailBase view = GetCompiledScriptInstance(file, output, context, controller);
158 Log("Executing partial view {0}", partialName);
159 view.Run();
160 Log("Finished executing partial view {0}", partialName);
162 catch (Exception ex)
164 if (Logger != null && Logger.IsErrorEnabled)
166 Logger.Error("Could not generate JS", ex);
169 throw new RailsException("Error generating partial: " + partialName, ex);
173 public override object CreateJSGenerator(IRailsEngineContext context)
175 return new BrailJSGenerator(new PrototypeHelper.JSGenerator(context));
178 public override void GenerateJS(TextWriter output, IRailsEngineContext context, Controller controller,
179 string templateName)
181 Log("Generating JS for {0}", templateName);
185 object generator = CreateJSGenerator(context);
186 AdjustJavascriptContentType(context);
187 string file = ResolveTemplateName(templateName, JSGeneratorFileExtension);
188 BrailBase view = GetCompiledScriptInstance(file,
189 //we use the script just to build the generator, not to output to the user
190 new StringWriter(),
191 context, controller);
192 Log("Executing JS view {0}", templateName);
193 view.AddProperty("page", generator);
194 view.Run();
196 output.WriteLine(generator);
197 Log("Finished executing JS view {0}", templateName);
199 catch (Exception ex)
201 if (Logger != null && Logger.IsErrorEnabled)
203 Logger.Error("Could not generate JS", ex);
206 throw new RailsException("Error generating JS. Template: " + templateName, ex);
210 // Send the contents text directly to the user, only adding the layout if neccecary
211 public override void ProcessContents(IRailsEngineContext context, Controller controller, string contents)
213 LayoutViewOutput layoutViewOutput = GetOutput(controller.Response.Output, context, controller);
214 layoutViewOutput.Output.Write(contents);
215 // here we don't need to pass parameters from the layout to the view,
216 if (layoutViewOutput.Layout != null)
217 layoutViewOutput.Layout.Run();
220 private void OnViewChanged(object sender, FileSystemEventArgs e)
222 if (e.FullPath.IndexOf(options.CommonScriptsDirectory) != -1)
224 Log("Detected a change in commons scripts directory " + options.CommonScriptsDirectory + ", recompiling site");
225 // need to invalidate the entire CommonScripts assembly
226 // not worrying about concurrency here, since it is assumed
227 // that changes here are rare. Note, this force a recompile of the
228 // whole site!
231 WaitForFileToBecomeAvailableForReading(e);
232 CompileCommonScripts();
234 catch (Exception ex)
236 // we failed to recompile the commons scripts directory, but because we are running
237 // on another thread here, and exception would kill the application, so we log it
238 // and continue on. CompileCommonScripts() will only change the global state if it has
239 // successfully compiled the commons scripts directory.
240 Log("Failed to recompile the commons scripts directory! {0}", ex);
242 return;
244 Log("Detected a change in {0}, removing from complied cache", e.Name);
245 // Will cause a recompilation
246 compilations[e.FullPath] = null;
249 private static void WaitForFileToBecomeAvailableForReading(FileSystemEventArgs e)
251 // We may need to wait while the file is being written and closed to disk
252 int retries = 10;
253 bool successfullyOpenedFile = false;
254 while (retries != 0 && successfullyOpenedFile == false)
256 retries -= 1;
259 using (File.OpenRead(e.FullPath))
261 successfullyOpenedFile = true;
264 catch (IOException)
266 //The file is probably in locked because it is currently being written to,
267 // will wait a while for it to be freed.
268 // again, this isn't something that need to be very robust, it runs on a separate thread
269 // and if it fails, it is not going to do any damage
270 Thread.Sleep(250);
275 // Get configuration options if they exists, if they do not exist, load the default ones
276 // Create directory to save the compiled assemblies if required.
277 // pre-compile the common scripts
278 public override void Service(IServiceProvider serviceProvider)
280 base.Service(serviceProvider);
281 ILoggerFactory loggerFactory = serviceProvider.GetService(typeof (ILoggerFactory)) as ILoggerFactory;
282 if (loggerFactory != null)
283 logger = loggerFactory.Create(GetType().Name);
286 /// <summary>
287 /// Resolves the template name into a file name.
288 /// </summary>
289 protected static string ResolveTemplateName(string templateName, string extention)
291 if (Path.HasExtension(templateName))
293 return templateName;
295 else
297 return templateName + extention;
301 // Check if a layout has been defined. If it was, then the layout would be created
302 // and will take over the output, otherwise, the context.Reposne.Output is used,
303 // and layout is null
304 private LayoutViewOutput GetOutput(TextWriter output, IRailsEngineContext context, Controller controller)
306 BrailBase layout = null;
307 if (controller.LayoutName != null)
309 string layoutTemplate = "layouts\\" + controller.LayoutName;
310 string layoutFilename = layoutTemplate + ViewFileExtension;
311 layout = GetCompiledScriptInstance(layoutFilename, output,
312 context, controller);
313 output = layout.ChildOutput = new StringWriter();
315 return new LayoutViewOutput(output, layout);
318 // This takes a filename and return an instance of the view ready to be used.
319 // If the file does not exist, an exception is raised
320 // The cache is checked to see if the file has already been compiled, and it had been
321 // a check is made to see that the compiled instance is newer then the file's modification date.
322 // If the file has not been compiled, or the version on disk is newer than the one in memory, a new
323 // version is compiled.
324 // Finally, an instance is created and returned
325 public BrailBase GetCompiledScriptInstance(string file,
326 TextWriter output,
327 IRailsEngineContext context,
328 Controller controller)
330 bool batch = options.BatchCompile;
331 // normalize filename - replace / or \ to the system path seperator
332 string filename = file.Replace('/', Path.DirectorySeparatorChar)
333 .Replace('\\', Path.DirectorySeparatorChar);
334 Log("Getting compiled instnace of {0}", filename);
335 Type type;
336 if (compilations.ContainsKey(filename))
338 type = (Type) compilations[filename];
339 if (type != null)
341 Log("Got compiled instance of {0} from cache", filename);
342 return CreateBrailBase(context, controller, output, type);
344 // if file is in compilations and the type is null,
345 // this means that we need to recompile. Since this usually means that
346 // the file was changed, we'll set batch to false and procceed to compile just
347 // this file.
348 Log("Cache miss! Need to recompile {0}", filename);
349 batch = false;
351 type = CompileScript(filename, batch);
352 if (type == null)
354 throw new RailsException("Could not find a view with path " + filename);
356 return CreateBrailBase(context, controller, output, type);
359 private BrailBase CreateBrailBase(IRailsEngineContext context, Controller controller, TextWriter output, Type type)
361 ConstructorInfo constructor = (ConstructorInfo) constructors[type];
362 BrailBase self = (BrailBase) FormatterServices.GetUninitializedObject(type);
363 constructor.Invoke(self, new object[] {this, output, context, controller});
364 return self;
367 // Compile a script (or all scripts in a directory), save the compiled result
368 // to the cache and return the compiled type.
369 // If an error occurs in batch compilation, then an attempt is made to compile just the single
370 // request file.
371 public Type CompileScript(string filename, bool batch)
373 IDictionary<ICompilerInput, string> inputs2FileName = GetInput(filename, batch);
374 string name = NormalizeName(filename);
375 Log("Compiling {0} to {1} with batch: {2}", filename, name, batch);
376 CompilationResult result = DoCompile(inputs2FileName.Keys, name);
378 if (result.Context.Errors.Count > 0)
380 if (batch == false)
382 string errors = result.Context.Errors.ToString(true);
383 Log("Failed to compile {0} because {1}", filename, errors);
384 StringBuilder msg = new StringBuilder();
385 msg.Append("Error during compile:")
386 .Append(Environment.NewLine)
387 .Append(errors)
388 .Append(Environment.NewLine);
390 foreach (ICompilerInput input in inputs2FileName.Keys)
392 msg.Append("Input (").Append(input.Name).Append(")")
393 .Append(Environment.NewLine);
394 msg.Append(result.Processor.GetInputCode(input))
395 .Append(Environment.NewLine);
397 throw new RailsException(msg.ToString());
399 //error compiling a batch, let's try a single file
400 return CompileScript(filename, false);
402 Type type;
403 foreach (ICompilerInput input in inputs2FileName.Keys)
405 string viewName = Path.GetFileNameWithoutExtension(input.Name);
406 string typeName = TransformToBrailStep.GetViewTypeName(viewName);
407 type = result.Context.GeneratedAssembly.GetType(typeName);
408 Log("Adding {0} to the cache", type.FullName);
409 compilations[inputs2FileName[input]] = type;
410 constructors[type] = type.GetConstructor(new Type[]
412 typeof (BooViewEngine),
413 typeof (TextWriter),
414 typeof (IRailsEngineContext),
415 typeof (Controller)
418 type = (Type) compilations[filename];
419 return type;
422 // If batch compilation is set to true, this would return all the view scripts
423 // in the director (not recursive!)
424 // Otherwise, it would return just the single file
425 private IDictionary<ICompilerInput, string> GetInput(string filename, bool batch)
427 Dictionary<ICompilerInput, string> input2FileName = new Dictionary<ICompilerInput, string>();
428 if (batch == false)
430 input2FileName.Add(CreateInput(filename), filename);
431 return input2FileName;
433 // use the System.IO.Path to get the folder name even though
434 // we are using the ViewSourceLoader to load the actual file
435 string directory = Path.GetDirectoryName(filename);
436 foreach (string file in ViewSourceLoader.ListViews(directory))
438 ICompilerInput input = CreateInput(file);
439 input2FileName.Add(input, file);
441 return input2FileName;
444 // create an input from a resource name
445 public ICompilerInput CreateInput(string name)
447 IViewSource viewSrc = ViewSourceLoader.GetViewSource(name);
448 if (viewSrc == null)
450 throw new RailsException("{0} is not a valid view", name);
452 // I need to do it this way because I can't tell
453 // when to dispose of the stream.
454 // It is not expected that this will be a big problem, the string
455 // will go away after the compile is done with them.
456 using (StreamReader stream = new StreamReader(viewSrc.OpenViewStream()))
458 return new StringInput(name, stream.ReadToEnd());
462 /// <summary>
463 /// Perform the actual compilation of the scripts
464 /// Things to note here:
465 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
466 /// * If a common scripts assembly exist, it is also referenced
467 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
468 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
469 /// this allows to use naked parameters such as (output context.IsLocal) without using
470 /// any special syntax
471 /// * The FixTryGetParameterConditionalChecks is run afterward, to transform "if ?Error" to "if not ?Error isa IgnoreNull"
472 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
473 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
474 /// date and list without accidently using the Boo.Lang.BuiltIn versions
475 /// </summary>
476 /// <param name="files"></param>
477 /// <param name="name"></param>
478 /// <returns></returns>
479 private CompilationResult DoCompile(ICollection<ICompilerInput> files, string name)
481 ICompilerInput[] filesAsArray = new List<ICompilerInput>(files).ToArray();
482 BooCompiler compiler = SetupCompiler(filesAsArray);
483 string filename = Path.Combine(baseSavePath, name);
484 compiler.Parameters.OutputAssembly = filename;
485 // this is here and not in SetupCompiler since CompileCommon is also
486 // using SetupCompiler, and we don't want reference to the old common from the new one
487 if (common != null)
488 compiler.Parameters.References.Add(common);
489 // pre procsssor needs to run before the parser
490 BrailPreProcessor processor = new BrailPreProcessor(this);
491 compiler.Parameters.Pipeline.Insert(0, processor);
492 // inserting the add class step after the parser
493 compiler.Parameters.Pipeline.Insert(2, new TransformToBrailStep(options));
494 compiler.Parameters.Pipeline.Replace(typeof (ProcessMethodBodiesWithDuckTyping),
495 new ReplaceUknownWithParameters());
496 compiler.Parameters.Pipeline.Replace(typeof (ExpandDuckTypedExpressions),
497 new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
498 compiler.Parameters.Pipeline.Replace(typeof (InitializeTypeSystemServices),
499 new InitializeCustomTypeSystem());
500 compiler.Parameters.Pipeline.InsertBefore(typeof (ReplaceUknownWithParameters),
501 new FixTryGetParameterConditionalChecks());
502 compiler.Parameters.Pipeline.RemoveAt(compiler.Parameters.Pipeline.Find(typeof (IntroduceGlobalNamespaces)));
504 return new CompilationResult(compiler.Run(), processor);
507 // Return the output filename for the generated assembly
508 // The filename is dependant on whatever we are doing a batch
509 // compile or not, if it's a batch compile, then the directory name
510 // is used, if it's just a single file, we're using the file's name.
511 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
512 // should never include it since I'm converting this to a relative path
513 public string NormalizeName(string filename)
515 string name = filename;
516 name = name.Replace(Path.AltDirectorySeparatorChar, '_');
517 name = name.Replace(Path.DirectorySeparatorChar, '_');
519 return name + "_BrailView.dll";
522 // Compile all the common scripts to a common assemblies
523 // an error in the common scripts would raise an exception.
524 public bool CompileCommonScripts()
526 if (options.CommonScriptsDirectory == null)
527 return false;
529 // the demi.boo is stripped, but GetInput require it.
530 string demiFile = Path.Combine(options.CommonScriptsDirectory, "demi.brail");
531 IDictionary<ICompilerInput, string> inputs = GetInput(demiFile, true);
532 ICompilerInput[] inputsAsArray = new List<ICompilerInput>(inputs.Keys).ToArray();
533 BooCompiler compiler = SetupCompiler(inputsAsArray);
534 string outputFile = Path.Combine(baseSavePath, "CommonScripts.dll");
535 compiler.Parameters.OutputAssembly = outputFile;
536 CompilerContext result = compiler.Run();
537 if (result.Errors.Count > 0)
538 throw new RailsException(result.Errors.ToString(true));
539 common = result.GeneratedAssembly;
540 compilations.Clear();
541 return true;
544 // common setup for the compiler
545 private static BooCompiler SetupCompiler(ICompilerInput[] files)
547 BooCompiler compiler = new BooCompiler();
548 compiler.Parameters.Ducky = true;
549 compiler.Parameters.Debug = options.Debug;
550 if (options.SaveToDisk)
551 compiler.Parameters.Pipeline = new CompileToFile();
552 else
553 compiler.Parameters.Pipeline = new CompileToMemory();
554 // replace the normal parser with white space agnostic one.
555 compiler.Parameters.Pipeline.RemoveAt(0);
556 compiler.Parameters.Pipeline.Insert(0, new WSABooParsingStep());
557 foreach (ICompilerInput file in files)
559 compiler.Parameters.Input.Add(file);
561 foreach (Assembly assembly in options.AssembliesToReference)
563 compiler.Parameters.References.Add(assembly);
565 compiler.Parameters.OutputType = CompilerOutputType.Library;
566 return compiler;
569 private static void InitializeConfig()
571 InitializeConfig("brail");
573 if (options == null)
575 InitializeConfig("Brail");
578 if (options == null)
580 options = new BooViewEngineOptions();
584 private static void InitializeConfig(string sectionName)
586 options = ConfigurationManager.GetSection(sectionName) as BooViewEngineOptions;
589 private void Log(string msg, params object[] items)
591 if (logger == null || logger.IsDebugEnabled == false)
592 return;
593 logger.DebugFormat(msg, items);
596 public bool ConditionalPreProcessingOnly(string name)
598 return String.Equals(
599 Path.GetExtension(name),
600 JSGeneratorFileExtension,
601 StringComparison.InvariantCultureIgnoreCase);
604 #region Nested type: CompilationResult
606 private class CompilationResult
608 private CompilerContext context;
609 private BrailPreProcessor processor;
611 public CompilationResult(CompilerContext context, BrailPreProcessor processor)
613 this.context = context;
614 this.processor = processor;
617 public CompilerContext Context
619 get { return context; }
622 public BrailPreProcessor Processor
624 get { return processor; }
628 #endregion
630 #region Nested type: LayoutViewOutput
632 private class LayoutViewOutput
634 private BrailBase layout;
635 private TextWriter output;
637 public LayoutViewOutput(TextWriter output, BrailBase layout)
639 this.layout = layout;
640 this.output = output;
643 public BrailBase Layout
645 get { return layout; }
648 public TextWriter Output
650 get { return output; }
654 #endregion