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
.Configuration
;
21 using System
.Reflection
;
22 using System
.Runtime
.Serialization
;
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
;
30 using Castle
.Core
.Logging
;
31 using Castle
.MonoRail
.Framework
;
32 using Castle
.MonoRail
.Framework
.Helpers
;
34 public class BooViewEngine
: ViewEngineBase
, IInitializable
37 /// This field holds all the cache of all the
38 /// compiled types (not instances) of all the views that Brail nows of.
40 private Hashtable compilations
= Hashtable
.Synchronized(
43 StringComparer
.InvariantCultureIgnoreCase
45 CaseInsensitiveHashCodeProvider
.Default
,
46 CaseInsensitiveComparer
.Default
51 /// used to hold the constructors of types, so we can avoid using
52 /// Activator (which takes a long time
54 private Hashtable constructors
= new Hashtable();
57 /// This is used to add a reference to the common scripts for each compiled scripts
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
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
,
117 Log("Starting to process request for {0}", templateName
);
118 string file
= templateName
+ ViewFileExtension
;
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
);
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
,
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
);
148 Log("Finished executing partial view {0}", partialName
);
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
,
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
179 context
, controller
);
180 Log("Executing JS view {0}", templateName
);
181 view
.AddProperty("page", generator
);
184 output
.WriteLine(generator
);
185 Log("Finished executing JS view {0}", templateName
);
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
217 CompileCommonScripts();
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
);
237 /// Resolves the template name into a file name.
239 protected static string ResolveTemplateName(string templateName
, string extention
)
241 if (Path
.HasExtension(templateName
))
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
,
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
);
286 if (compilations
.ContainsKey(filename
))
288 type
= (Type
) compilations
[filename
];
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
298 Log("Cache miss! Need to recompile {0}", filename
);
301 type
= CompileScript(filename
, batch
);
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}
);
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
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)
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
)
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);
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
),
363 typeof(IRailsEngineContext
),
367 type
= (Type
) compilations
[filename
];
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
)
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
);
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());
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
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
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)
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();
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();
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
;
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");
522 InitializeConfig("Brail");
527 options
= new BooViewEngineOptions();
531 private static void InitializeConfig(string sectionName
)
534 options
= ConfigurationManager
.GetSection(sectionName
) as BooViewEngineOptions
;
536 options
= System
.Configuration
.ConfigurationSettings
.GetConfig(sectionName
) as BooViewEngineOptions
;
540 private void Log(string msg
, params object[] items
)
542 if (logger
== null || logger
.IsDebugEnabled
== false)
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
;