1 // Copyright 2004-2007 Castle Project - http://www.castleproject.org/
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
7 // http://www.apache.org/licenses/LICENSE-2.0
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
18 using System
.Collections
;
19 using System
.Collections
.Generic
;
20 using System
.Configuration
;
22 using System
.Reflection
;
23 using System
.Runtime
.Serialization
;
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
;
32 using Framework
.Helpers
;
36 public class BooViewEngine
: ViewEngineBase
, IInitializable
38 private static BooViewEngineOptions options
;
41 /// This field holds all the cache of all the
42 /// compiled types (not instances) of all the views that Brail nows of.
44 private readonly Hashtable compilations
= Hashtable
.Synchronized(
45 new Hashtable(StringComparer
.InvariantCultureIgnoreCase
));
47 private string baseSavePath
;
50 /// This is used to add a reference to the common scripts for each compiled scripts
52 private Assembly common
;
55 /// used to hold the constructors of types, so we can avoid using
56 /// Activator (which takes a long time
58 private readonly Hashtable constructors
= new Hashtable();
60 private ILogger logger
;
62 public override bool SupportsJSGeneration
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
);
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
,
124 Log("Starting to process request for {0}", templateName
);
125 string file
= templateName
+ ViewFileExtension
;
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
);
142 HandleException(templateName
, view
, e
);
145 if (layoutViewOutput
.Layout
!= null)
147 layoutViewOutput
.Layout
.SetParent(view
);
150 layoutViewOutput
.Layout
.Run();
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());
169 throw new RailsException(msg
, e
);
172 public override void ProcessPartial(
173 TextWriter output
, IRailsEngineContext context
, IController controller
,
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
);
184 Log("Finished executing partial view {0}", partialName
);
188 if (Logger
!= null && Logger
.IsErrorEnabled
)
190 Logger
.Error("Could not generate JS", ex
);
193 throw new RailsException("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
,
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
216 context
, controller
);
217 Log("Executing JS view {0}", templateName
);
218 view
.AddProperty("page", generator
);
221 output
.WriteLine(generator
);
222 Log("Finished executing JS view {0}", templateName
);
226 if (Logger
!= null && Logger
.IsErrorEnabled
)
228 Logger
.Error("Could not generate JS", ex
);
231 throw new RailsException("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
262 WaitForFileToBecomeAvailableForReading(e
);
263 CompileCommonScripts();
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
);
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
286 bool successfullyOpenedFile
= false;
287 while (retries
!= 0 && successfullyOpenedFile
== false)
292 using (File
.OpenRead(e
.FullPath
))
294 successfullyOpenedFile
= true;
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
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().Name
);
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
);
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
350 public BrailBase
GetCompiledScriptInstance(
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
);
362 if (compilations
.ContainsKey(filename
))
364 type
= (Type
)compilations
[filename
];
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
374 Log("Cache miss! Need to recompile {0}", filename
);
377 type
= CompileScript(filename
, batch
);
380 throw new RailsException("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 }
);
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
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)
408 string errors
= result
.Context
.Errors
.ToString(true);
409 Log("Failed to compile {0} because {1}", filename
, errors
);
410 StringBuilder msg
= new StringBuilder();
411 msg
.Append("Error during compile:")
412 .Append(Environment
.NewLine
)
414 .Append(Environment
.NewLine
);
416 foreach (ICompilerInput input
in inputs2FileName
.Keys
)
418 msg
.Append("Input (").Append(input
.Name
).Append(")")
419 .Append(Environment
.NewLine
);
420 msg
.Append(result
.Processor
.GetInputCode(input
))
421 .Append(Environment
.NewLine
);
423 throw new RailsException(msg
.ToString());
425 //error compiling a batch, let's try a single file
426 return CompileScript(filename
, false);
429 foreach (ICompilerInput input
in inputs2FileName
.Keys
)
431 string viewName
= Path
.GetFileNameWithoutExtension(input
.Name
);
432 string typeName
= TransformToBrailStep
.GetViewTypeName(viewName
);
433 type
= result
.Context
.GeneratedAssembly
.GetType(typeName
);
434 Log("Adding {0} to the cache", type
.FullName
);
435 compilations
[inputs2FileName
[input
]] = type
;
436 constructors
[type
] = type
.GetConstructor(new Type
[]
438 typeof (BooViewEngine
),
440 typeof (IRailsEngineContext
),
444 type
= (Type
)compilations
[filename
];
448 // If batch compilation is set to true, this would return all the view scripts
449 // in the director (not recursive!)
450 // Otherwise, it would return just the single file
451 private IDictionary
<ICompilerInput
, string> GetInput(string filename
, bool batch
)
453 Dictionary
<ICompilerInput
, string> input2FileName
= new Dictionary
<ICompilerInput
, string>();
456 input2FileName
.Add(CreateInput(filename
), filename
);
457 return input2FileName
;
459 // use the System.IO.Path to get the folder name even though
460 // we are using the ViewSourceLoader to load the actual file
461 string directory
= Path
.GetDirectoryName(filename
);
462 foreach (string file
in ViewSourceLoader
.ListViews(directory
))
464 ICompilerInput input
= CreateInput(file
);
465 input2FileName
.Add(input
, file
);
467 return input2FileName
;
470 // create an input from a resource name
471 public ICompilerInput
CreateInput(string name
)
473 IViewSource viewSrc
= ViewSourceLoader
.GetViewSource(name
);
476 throw new RailsException("{0} is not a valid view", name
);
478 // I need to do it this way because I can't tell
479 // when to dispose of the stream.
480 // It is not expected that this will be a big problem, the string
481 // will go away after the compile is done with them.
482 using (StreamReader stream
= new StreamReader(viewSrc
.OpenViewStream()))
484 return new StringInput(name
, stream
.ReadToEnd());
489 /// Perform the actual compilation of the scripts
490 /// Things to note here:
491 /// * The generated assembly reference the Castle.MonoRail.MonoRailBrail and Castle.MonoRail.Framework assemblies
492 /// * If a common scripts assembly exist, it is also referenced
493 /// * The AddBrailBaseClassStep compiler step is added - to create a class from the view's code
494 /// * The ProcessMethodBodiesWithDuckTyping is replaced with ReplaceUknownWithParameters
495 /// this allows to use naked parameters such as (output context.IsLocal) without using
496 /// any special syntax
497 /// * The FixTryGetParameterConditionalChecks is run afterward, to transform "if ?Error" to "if not ?Error isa IgnoreNull"
498 /// * The ExpandDuckTypedExpressions is replace with a derived step that allows the use of Dynamic Proxy assemblies
499 /// * The IntroduceGlobalNamespaces step is removed, to allow to use common variables such as
500 /// date and list without accidently using the Boo.Lang.BuiltIn versions
502 /// <param name="files"></param>
503 /// <param name="name"></param>
504 /// <returns></returns>
505 private CompilationResult
DoCompile(IEnumerable
<ICompilerInput
> files
, string name
)
507 ICompilerInput
[] filesAsArray
= new List
<ICompilerInput
>(files
).ToArray();
508 BooCompiler compiler
= SetupCompiler(filesAsArray
);
509 string filename
= Path
.Combine(baseSavePath
, name
);
510 compiler
.Parameters
.OutputAssembly
= filename
;
511 // this is here and not in SetupCompiler since CompileCommon is also
512 // using SetupCompiler, and we don't want reference to the old common from the new one
514 compiler
.Parameters
.References
.Add(common
);
515 // pre procsssor needs to run before the parser
516 BrailPreProcessor processor
= new BrailPreProcessor(this);
517 compiler
.Parameters
.Pipeline
.Insert(0, processor
);
518 // inserting the add class step after the parser
519 compiler
.Parameters
.Pipeline
.Insert(2, new TransformToBrailStep(options
));
520 compiler
.Parameters
.Pipeline
.Replace(typeof(ProcessMethodBodiesWithDuckTyping
),
521 new ReplaceUknownWithParameters());
522 compiler
.Parameters
.Pipeline
.Replace(typeof(ExpandDuckTypedExpressions
),
523 new ExpandDuckTypedExpressions_WorkaroundForDuplicateVirtualMethods());
524 compiler
.Parameters
.Pipeline
.Replace(typeof(InitializeTypeSystemServices
),
525 new InitializeCustomTypeSystem());
526 compiler
.Parameters
.Pipeline
.InsertBefore(typeof(ReplaceUknownWithParameters
),
527 new FixTryGetParameterConditionalChecks());
528 compiler
.Parameters
.Pipeline
.RemoveAt(compiler
.Parameters
.Pipeline
.Find(typeof(IntroduceGlobalNamespaces
)));
530 return new CompilationResult(compiler
.Run(), processor
);
533 // Return the output filename for the generated assembly
534 // The filename is dependant on whatever we are doing a batch
535 // compile or not, if it's a batch compile, then the directory name
536 // is used, if it's just a single file, we're using the file's name.
537 // '/' and '\' are replaced with '_', I'm not handling ':' since the path
538 // should never include it since I'm converting this to a relative path
539 public string NormalizeName(string filename
)
541 string name
= filename
;
542 name
= name
.Replace(Path
.AltDirectorySeparatorChar
, '_');
543 name
= name
.Replace(Path
.DirectorySeparatorChar
, '_');
545 return name
+ "_BrailView.dll";
548 // Compile all the common scripts to a common assemblies
549 // an error in the common scripts would raise an exception.
550 public bool CompileCommonScripts()
552 if (options
.CommonScriptsDirectory
== null)
555 // the demi.boo is stripped, but GetInput require it.
556 string demiFile
= Path
.Combine(options
.CommonScriptsDirectory
, "demi.brail");
557 IDictionary
<ICompilerInput
, string> inputs
= GetInput(demiFile
, true);
558 ICompilerInput
[] inputsAsArray
= new List
<ICompilerInput
>(inputs
.Keys
).ToArray();
559 BooCompiler compiler
= SetupCompiler(inputsAsArray
);
560 string outputFile
= Path
.Combine(baseSavePath
, "CommonScripts.dll");
561 compiler
.Parameters
.OutputAssembly
= outputFile
;
562 CompilerContext result
= compiler
.Run();
563 if (result
.Errors
.Count
> 0)
564 throw new RailsException(result
.Errors
.ToString(true));
565 common
= result
.GeneratedAssembly
;
566 compilations
.Clear();
570 // common setup for the compiler
571 private static BooCompiler
SetupCompiler(IEnumerable
<ICompilerInput
> files
)
573 BooCompiler compiler
= new BooCompiler();
574 compiler
.Parameters
.Ducky
= true;
575 compiler
.Parameters
.Debug
= options
.Debug
;
576 if (options
.SaveToDisk
)
577 compiler
.Parameters
.Pipeline
= new CompileToFile();
579 compiler
.Parameters
.Pipeline
= new CompileToMemory();
580 // replace the normal parser with white space agnostic one.
581 compiler
.Parameters
.Pipeline
.RemoveAt(0);
582 compiler
.Parameters
.Pipeline
.Insert(0, new WSABooParsingStep());
583 foreach (ICompilerInput file
in files
)
585 compiler
.Parameters
.Input
.Add(file
);
587 foreach (Assembly assembly
in options
.AssembliesToReference
)
589 compiler
.Parameters
.References
.Add(assembly
);
591 compiler
.Parameters
.OutputType
= CompilerOutputType
.Library
;
595 private static void InitializeConfig()
597 InitializeConfig("brail");
601 InitializeConfig("Brail");
606 options
= new BooViewEngineOptions();
610 private static void InitializeConfig(string sectionName
)
612 options
= ConfigurationManager
.GetSection(sectionName
) as BooViewEngineOptions
;
615 private void Log(string msg
, params object[] items
)
617 if (logger
== null || logger
.IsDebugEnabled
== false)
619 logger
.DebugFormat(msg
, items
);
622 public bool ConditionalPreProcessingOnly(string name
)
624 return String
.Equals(
625 Path
.GetExtension(name
),
626 JSGeneratorFileExtension
,
627 StringComparison
.InvariantCultureIgnoreCase
);
630 #region Nested type: CompilationResult
632 private class CompilationResult
634 private readonly CompilerContext context
;
635 private readonly BrailPreProcessor processor
;
637 public CompilationResult(CompilerContext context
, BrailPreProcessor processor
)
639 this.context
= context
;
640 this.processor
= processor
;
643 public CompilerContext Context
645 get { return context; }
648 public BrailPreProcessor Processor
650 get { return processor; }
656 #region Nested type: LayoutViewOutput
658 private class LayoutViewOutput
660 private readonly BrailBase layout
;
661 private readonly TextWriter output
;
663 public LayoutViewOutput(TextWriter output
, BrailBase layout
)
665 this.layout
= layout
;
666 this.output
= output
;
669 public BrailBase Layout
671 get { return layout; }
674 public TextWriter Output
676 get { return output; }