MR-223: AmbiguousMatch exception when querying properties from NHibernate-enabled...
[castle.git] / MonoRail / Castle.MonoRail.Views.Brail / BooViewEngine.cs
blob70fefebbf1a7ba0a74f405a66ab051d5428b52db
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.Configuration;
20 using System.IO;
21 using System.Reflection;
22 using System.Runtime.Serialization;
23 using System.Text;
24 using Boo.Lang.Compiler;
25 using Boo.Lang.Compiler.IO;
26 using Boo.Lang.Compiler.Pipelines;
27 using Boo.Lang.Compiler.Steps;
28 using Boo.Lang.Parser;
29 using Castle.Core;
30 using Castle.Core.Logging;
31 using Castle.MonoRail.Framework;
32 using Castle.MonoRail.Framework.Helpers;
34 public class BooViewEngine : ViewEngineBase, IInitializable
36 /// <summary>
37 /// This field holds all the cache of all the
38 /// compiled types (not instances) of all the views that Brail nows of.
39 /// </summary>
40 private Hashtable compilations = Hashtable.Synchronized(
41 new Hashtable(
42 #if DOTNET2
43 StringComparer.InvariantCultureIgnoreCase
44 #else
45 CaseInsensitiveHashCodeProvider.Default,
46 CaseInsensitiveComparer.Default
47 #endif
48 ));
50 /// <summary>
51 /// used to hold the constructors of types, so we can avoid using
52 /// Activator (which takes a long time
53 /// </summary>
54 private Hashtable constructors = new Hashtable();
56 /// <summary>
57 /// This is used to add a reference to the common scripts for each compiled scripts
58 /// </summary>
59 private Assembly common;
60 private ILogger logger;
61 private static BooViewEngineOptions options;
62 private string baseSavePath;
64 public void Initialize()
66 if (options == null) InitializeConfig();
68 string baseDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
69 Log("Base Directory: " + baseDir);
70 baseSavePath = Path.Combine(baseDir, options.SaveDirectory);
71 Log("Base Save Path: " + baseSavePath);
73 if (options.SaveToDisk && !Directory.Exists(baseSavePath))
75 Directory.CreateDirectory(baseSavePath);
76 Log("Created directory " + baseSavePath);
79 CompileCommonScripts();
81 ViewSourceLoader.ViewChanged += new FileSystemEventHandler(OnViewChanged);
84 public override bool SupportsJSGeneration
86 get { return true; }
89 public override string ViewFileExtension
91 get { return ".brail"; }
94 public override string JSGeneratorFileExtension
96 get { return ".brailjs"; }
99 public override bool HasTemplate(string templateName)
101 if(Path.HasExtension(templateName))
102 return ViewSourceLoader.HasTemplate(templateName);
103 return ViewSourceLoader.HasTemplate(templateName + ViewFileExtension);
106 // Process a template name and output the results to the user
107 // This may throw if an error occured and the user is not local (which would
108 // cause the yellow screen of death)
109 public override void Process(IRailsEngineContext context, Controller controller, string templateName)
111 Process(context.Response.Output, context, controller, templateName);
114 public override void Process(TextWriter output, IRailsEngineContext context, Controller controller,
115 string templateName)
117 Log("Starting to process request for {0}", templateName);
118 string file = templateName + ViewFileExtension;
119 BrailBase view;
120 // Output may be the layout's child output if a layout exists
121 // or the context.Response.Output if the layout is null
122 LayoutViewOutput layoutViewOutput = GetOutput(output, context, controller);
123 // Will compile on first time, then save the assembly on the cache.
124 view = GetCompiledScriptInstance(file, layoutViewOutput.Output, context, controller);
125 controller.PreSendView(view);
126 Log("Executing view {0}", templateName);
127 view.Run();
128 if (layoutViewOutput.Layout != null)
130 layoutViewOutput.Layout.SetParent(view);
131 layoutViewOutput.Layout.Run();
133 Log("Finished executing view {0}", templateName);
134 controller.PostSendView(view);
137 public override void ProcessPartial(TextWriter output, IRailsEngineContext context, Controller controller,
138 string partialName)
140 Log("Generating partial for {0}", partialName);
144 string file = ResolveTemplateName(partialName, ViewFileExtension);
145 BrailBase view = GetCompiledScriptInstance(file, output, context, controller);
146 Log("Executing partial view {0}", partialName);
147 view.Run();
148 Log("Finished executing partial view {0}", partialName);
150 catch (Exception ex)
152 if (Logger != null && Logger.IsErrorEnabled)
154 Logger.Error("Could not generate JS", ex);
157 throw new RailsException("Error generating partial: " + partialName, ex);
161 public override object CreateJSGenerator(IRailsEngineContext context)
163 return new BrailJSGenerator(new PrototypeHelper.JSGenerator(context));
166 public override void GenerateJS(TextWriter output, IRailsEngineContext context, Controller controller,
167 string templateName)
169 Log("Generating JS for {0}", templateName);
173 object generator = CreateJSGenerator(context);
174 AdjustJavascriptContentType(context);
175 string file = ResolveTemplateName(templateName, JSGeneratorFileExtension);
176 BrailBase view = GetCompiledScriptInstance(file,
177 //we use the script just to build the generator, not to output to the user
178 new StringWriter(),
179 context, controller);
180 Log("Executing JS view {0}", templateName);
181 view.AddProperty("page", generator);
182 view.Run();
184 output.WriteLine(generator);
185 Log("Finished executing JS view {0}", templateName);
187 catch (Exception ex)
189 if (Logger!=null && Logger.IsErrorEnabled)
191 Logger.Error("Could not generate JS", ex);
194 throw new RailsException("Error generating JS. Template: " + templateName, ex);
198 // Send the contents text directly to the user, only adding the layout if neccecary
199 public override void ProcessContents(IRailsEngineContext context, Controller controller, string contents)
201 LayoutViewOutput layoutViewOutput = GetOutput(controller.Response.Output, context, controller);
202 layoutViewOutput.Output.Write(contents);
203 // here we don't need to pass parameters from the layout to the view,
204 if (layoutViewOutput.Layout != null)
205 layoutViewOutput.Layout.Run();
208 private void OnViewChanged(object sender, FileSystemEventArgs e)
210 if (e.FullPath.IndexOf(options.CommonScriptsDirectory) != -1)
212 Log("Detected a change in commons scripts directory " + options.CommonScriptsDirectory + ", recompiling site");
213 // need to invalidate the entire CommonScripts assembly
214 // not worrying about concurrency here, since it is assumed
215 // that changes here are rare. Note, this force a recompile of the
216 // whole site.
217 CompileCommonScripts();
218 return;
220 Log("Detected a change in {0}, removing from complied cache", e.Name);
221 // Will cause a recompilation
222 compilations[e.Name] = null;
225 // Get configuration options if they exists, if they do not exist, load the default ones
226 // Create directory to save the compiled assemblies if required.
227 // pre-compile the common scripts
228 public override void Service(IServiceProvider serviceProvider)
230 base.Service(serviceProvider);
231 ILoggerFactory loggerFactory = serviceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
232 if (loggerFactory != null)
233 logger = loggerFactory.Create(GetType().Name);
236 /// <summary>
237 /// Resolves the template name into a file name.
238 /// </summary>
239 protected static string ResolveTemplateName(string templateName, string extention)
241 if (Path.HasExtension(templateName))
243 return templateName;
245 else
247 return templateName + extention;
251 // Check if a layout has been defined. If it was, then the layout would be created
252 // and will take over the output, otherwise, the context.Reposne.Output is used,
253 // and layout is null
254 private LayoutViewOutput GetOutput(TextWriter output, IRailsEngineContext context, Controller controller)
256 BrailBase layout = null;
257 if (controller.LayoutName != null)
259 string layoutTemplate = "layouts\\" + controller.LayoutName;
260 string layoutFilename = layoutTemplate + ViewFileExtension;
261 layout = GetCompiledScriptInstance(layoutFilename, output,
262 context, controller);
263 output = layout.ChildOutput = new StringWriter();
265 return new LayoutViewOutput(output, layout);
268 // This takes a filename and return an instance of the view ready to be used.
269 // If the file does not exist, an exception is raised
270 // The cache is checked to see if the file has already been compiled, and it had been
271 // a check is made to see that the compiled instance is newer then the file's modification date.
272 // If the file has not been compiled, or the version on disk is newer than the one in memory, a new
273 // version is compiled.
274 // Finally, an instance is created and returned
275 public BrailBase GetCompiledScriptInstance(string file,
276 TextWriter output,
277 IRailsEngineContext context,
278 Controller controller)
280 bool batch = options.BatchCompile;
281 // normalize filename - replace / or \ to the system path seperator
282 string filename = file.Replace('/', Path.DirectorySeparatorChar)
283 .Replace('\\', Path.DirectorySeparatorChar);
284 Log("Getting compiled instnace of {0}", filename);
285 Type type;
286 if (compilations.ContainsKey(filename))
288 type = (Type) compilations[filename];
289 if (type != null)
291 Log("Got compiled instance of {0} from cache",filename);
292 return CreateBrailBase(context, controller, output, type);
294 // if file is in compilations and the type is null,
295 // this means that we need to recompile. Since this usually means that
296 // the file was changed, we'll set batch to false and procceed to compile just
297 // this file.
298 Log("Cache miss! Need to recompile {0}", filename);
299 batch = false;
301 type = CompileScript(filename, batch);
302 if (type == null)
304 throw new RailsException("Could not find a view with path " + filename);
306 return CreateBrailBase(context, controller, output, type);
309 private BrailBase CreateBrailBase(IRailsEngineContext context, Controller controller, TextWriter output, Type type)
311 ConstructorInfo constructor = (ConstructorInfo) constructors[type];
312 BrailBase self = (BrailBase) FormatterServices.GetUninitializedObject(type);
313 constructor.Invoke(self, new object[] {this, output, context, controller});
314 return self;
317 // Compile a script (or all scripts in a directory), save the compiled result
318 // to the cache and return the compiled type.
319 // If an error occurs in batch compilation, then an attempt is made to compile just the single
320 // request file.
321 public Type CompileScript(string filename, bool batch)
323 ICompilerInput[] inputs = GetInput(filename, batch);
324 string name = NormalizeName(filename);
325 Log("Compiling {0} to {1} with batch: {2}", filename, name, batch);
326 CompilationResult result = DoCompile(inputs, name);
328 if (result.Context.Errors.Count > 0)
330 if (batch == false)
332 string errors = result.Context.Errors.ToString(true);
333 Log("Failed to compile {0} because {1}", filename, errors);
334 StringBuilder msg = new StringBuilder();
335 msg.Append("Error during compile:")
336 .Append(Environment.NewLine)
337 .Append(errors)
338 .Append(Environment.NewLine);
340 foreach (ICompilerInput input in inputs)
342 msg.Append("Input (").Append(input.Name).Append(")")
343 .Append(Environment.NewLine);
344 msg.Append(result.Processor.GetInputCode(input))
345 .Append(Environment.NewLine);
347 throw new RailsException(msg.ToString());
349 //error compiling a batch, let's try a single file
350 return CompileScript(filename, false);
352 Type type;
353 foreach (ICompilerInput input in inputs)
355 string typeName = Path.GetFileNameWithoutExtension(input.Name) + "_BrailView";
356 type = result.Context.GeneratedAssembly.GetType(typeName);
357 Log("Adding {0} to the cache", type.FullName);
358 compilations[input.Name] = type;
359 constructors[type] = type.GetConstructor(new Type[]
361 typeof(BooViewEngine),
362 typeof(TextWriter),
363 typeof(IRailsEngineContext),
364 typeof(Controller)
367 type = (Type) compilations[filename];
368 return type;
371 // If batch compilation is set to true, this would return all the view scripts
372 // in the director (not recursive!)
373 // Otherwise, it would return just the single file
374 private ICompilerInput[] GetInput(string filename, bool batch)
376 if (batch == false)
377 return new ICompilerInput[] { CreateInput(filename) };
378 ArrayList inputs = new ArrayList();
379 // use the System.IO.Path to get the folder name even though
380 // we are using the ViewSourceLoader to load the actual file
381 string directory = Path.GetDirectoryName(filename);
382 foreach(string file in ViewSourceLoader.ListViews(directory))
384 inputs.Add(CreateInput(file));
386 return (ICompilerInput[])inputs.ToArray(typeof(ICompilerInput));
389 // create an input from a resource name
390 public ICompilerInput CreateInput(string name)
392 IViewSource viewSrc = ViewSourceLoader.GetViewSource(name);
393 if (viewSrc == null)
395 throw new RailsException("{0} is not a valid view", name);
397 // I need to do it this way because I can't tell
398 // when to dispose of the stream.
399 // It is not expected that this will be a big problem, the string
400 // will go away after the compile is done with them.
401 using(StreamReader stream = new StreamReader(viewSrc.OpenViewStream()))
403 return new StringInput(name, stream.ReadToEnd());
407 /// <summary>
408 /// Perform the actual compilation of the scripts
409 /// Things to note here:
410 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
411 /// * If a common scripts assembly exist, it is also referenced
412 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
413 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
414 /// this allows to use naked parameters such as (output context.IsLocal) without using
415 /// any special syntax
416 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
417 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
418 /// date and list without accidently using the Boo.Lang.BuiltIn versions
419 /// </summary>
420 /// <param name="files"></param>
421 /// <param name="name"></param>
422 /// <returns></returns>
423 private CompilationResult DoCompile(ICompilerInput[] files, string name)
425 BooCompiler compiler = SetupCompiler(files);
426 string filename = Path.Combine(baseSavePath, name);
427 compiler.Parameters.OutputAssembly = filename;
428 // this is here and not in SetupCompiler since CompileCommon is also
429 // using SetupCompiler, and we don't want reference to the old common from the new one
430 if (common != null)
431 compiler.Parameters.References.Add(common);
432 // pre procsssor needs to run before the parser
433 BrailPreProcessor processor = new BrailPreProcessor(this);
434 compiler.Parameters.Pipeline.Insert(0, processor);
435 // inserting the add class step after the parser
436 compiler.Parameters.Pipeline.Insert(2, new TransformToBrailStep());
437 compiler.Parameters.Pipeline.Replace(typeof(ProcessMethodBodiesWithDuckTyping), new ReplaceUknownWithParameters());
438 compiler.Parameters.Pipeline.Replace(typeof(ExpandDuckTypedExpressions), new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
439 compiler.Parameters.Pipeline.Replace(typeof(InitializeTypeSystemServices), new InitializeCustomTypeSystem());
440 compiler.Parameters.Pipeline.RemoveAt(compiler.Parameters.Pipeline.Find(typeof(IntroduceGlobalNamespaces)));
442 return new CompilationResult(compiler.Run(), processor);
445 // Return the output filename for the generated assembly
446 // The filename is dependant on whatever we are doing a batch
447 // compile or not, if it's a batch compile, then the directory name
448 // is used, if it's just a single file, we're using the file's name.
449 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
450 // should never include it since I'm converting this to a relative path
451 public string NormalizeName(string filename)
453 string name = filename;
454 name = name.Replace(Path.AltDirectorySeparatorChar, '_');
455 name = name.Replace(Path.DirectorySeparatorChar, '_');
457 return name + "_BrailView.dll";
460 // Compile all the common scripts to a common assemblies
461 // an error in the common scripts would raise an exception.
462 public bool CompileCommonScripts()
464 if (options.CommonScriptsDirectory == null)
465 return false;
467 // the demi.boo is stripped, but GetInput require it.
468 string demiFile = Path.Combine(options.CommonScriptsDirectory, "demi.brail");
469 ICompilerInput[] inputs = GetInput(demiFile, true);
470 BooCompiler compiler = SetupCompiler(inputs);
471 string outputFile = Path.Combine(baseSavePath, "CommonScripts.dll");
472 compiler.Parameters.OutputAssembly = outputFile;
473 CompilerContext result = compiler.Run();
474 if (result.Errors.Count > 0)
475 throw new RailsException(result.Errors.ToString(true));
476 common = result.GeneratedAssembly;
477 compilations.Clear();
478 return true;
481 // common setup for the compiler
482 private BooCompiler SetupCompiler(ICompilerInput[] files)
484 BooCompiler compiler = new BooCompiler();
485 compiler.Parameters.Ducky = true;
486 compiler.Parameters.Debug = options.Debug;
487 if (options.SaveToDisk)
488 compiler.Parameters.Pipeline = new CompileToFile();
489 else
490 compiler.Parameters.Pipeline = new CompileToMemory();
491 // replace the normal parser with white space agnostic one.
492 compiler.Parameters.Pipeline.RemoveAt(0);
493 compiler.Parameters.Pipeline.Insert(0, new WSABooParsingStep());
494 foreach (ICompilerInput file in files)
496 compiler.Parameters.Input.Add(file);
498 foreach(Assembly assembly in options.AssembliesToReference)
500 compiler.Parameters.References.Add(assembly);
502 compiler.Parameters.OutputType = CompilerOutputType.Library;
503 return compiler;
506 public string ViewRootDir
508 get { return ViewSourceLoader.ViewRootDir; }
511 public BooViewEngineOptions Options
513 get { return options; }
516 private static void InitializeConfig()
518 InitializeConfig("brail");
520 if (options == null)
522 InitializeConfig("Brail");
525 if (options == null)
527 options = new BooViewEngineOptions();
531 private static void InitializeConfig(string sectionName)
533 #if DOTNET2
534 options = ConfigurationManager.GetSection(sectionName) as BooViewEngineOptions;
535 #else
536 options = System.Configuration.ConfigurationSettings.GetConfig(sectionName) as BooViewEngineOptions;
537 #endif
540 private void Log(string msg, params object[] items)
542 if (logger == null || logger.IsDebugEnabled == false)
543 return;
544 logger.DebugFormat(msg, items);
547 private class LayoutViewOutput
549 private BrailBase layout;
550 private TextWriter output;
552 public LayoutViewOutput(TextWriter output, BrailBase layout)
554 this.layout = layout;
555 this.output = output;
558 public BrailBase Layout
560 get { return layout; }
563 public TextWriter Output
565 get { return output; }
569 private class CompilationResult
571 CompilerContext context;
572 BrailPreProcessor processor;
574 public CompilerContext Context
576 get { return context; }
579 public BrailPreProcessor Processor
581 get { return processor; }
584 public CompilationResult(CompilerContext context, BrailPreProcessor processor)
586 this.context = context;
587 this.processor = processor;
591 public bool ConditionalPreProcessingOnly(string name)
593 return Path.GetExtension(name) == JSGeneratorFileExtension;