Added a way to override message definition using external resources file.
[castle.git] / MonoRail / Castle.MonoRail.Framework.Views.NVelocity / NVelocityViewEngine.cs
blob61ea964add8254a682178fb3e9fd667056244fd0
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 using NVelocity;
16 using NVelocity.App;
17 using NVelocity.Context;
18 using NVelocity.Runtime;
20 namespace Castle.MonoRail.Framework.Views.NVelocity
22 using System;
23 using System.IO;
24 using System.Collections;
25 using Castle.Core;
26 using Castle.MonoRail.Framework.Helpers;
27 using Castle.MonoRail.Framework.Views.NVelocity.JSGeneration;
28 using Commons.Collections;
30 /// <summary>
31 /// Implements a view engine using the popular Velocity syntax.
32 /// <para>
33 /// For details on the syntax, check the VTL Reference Guide
34 /// http://jakarta.apache.org/velocity/docs/vtl-reference-guide.html
35 /// </para>
36 /// </summary>
37 public class NVelocityViewEngine : ViewEngineBase, IInitializable
39 internal const String TemplateExtension = ".vm";
40 internal const String JsTemplateExtension = ".njs";
42 internal const String ServiceProvider = "service.provider";
44 private IServiceProvider provider;
46 protected VelocityEngine velocity = new VelocityEngine();
48 /// <summary>
49 /// Creates a new <see cref="NVelocityViewEngine"/> instance.
50 /// </summary>
51 public NVelocityViewEngine()
55 #region IInitializable implementation
57 public void Initialize()
59 ExtendedProperties props = new ExtendedProperties();
61 if (ViewSourceLoader.HasTemplate("nvelocity.properties"))
63 using(Stream stream = ViewSourceLoader.GetViewSource("nvelocity.properties").OpenViewStream())
65 props.Load(stream);
69 // Set up a custom directive manager
70 props.SetProperty("directive.manager",
71 "Castle.MonoRail.Framework.Views.NVelocity.CustomDirectiveManager; Castle.MonoRail.Framework.Views.NVelocity");
73 InitializeVelocityProperties(props);
75 velocity.SetApplicationAttribute("service.provider", provider);
77 velocity.Init(props);
80 #endregion
82 #region IServiceEnabledComponent implementation
84 public override void Service(IServiceProvider provider)
86 base.Service(provider);
88 this.provider = provider;
91 #endregion
93 #region IViewEngine implementation
95 /// <summary>
96 /// Gets a value indicating whether the view engine
97 /// support the generation of JS.
98 /// </summary>
99 /// <value>
100 /// <c>true</c> if JS generation is supported; otherwise, <c>false</c>.
101 /// </value>
102 public override bool SupportsJSGeneration
104 get { return true; }
107 /// <summary>
108 /// Gets the view file extension.
109 /// </summary>
110 /// <value>The view file extension.</value>
111 public override string ViewFileExtension
113 get { return ".vm"; }
116 /// <summary>
117 /// Gets the JS generator file extension.
118 /// </summary>
119 /// <value>The JS generator file extension.</value>
120 public override string JSGeneratorFileExtension
122 get { return ".njs"; }
125 /// <summary>
126 /// Evaluates whether the specified template exists.
127 /// </summary>
128 /// <param name="templateName"></param>
129 /// <returns><c>true</c> if it exists</returns>
130 public override bool HasTemplate(String templateName)
132 return ViewSourceLoader.HasTemplate(ResolveTemplateName(templateName));
135 /// <summary>
136 /// Processes the specified context.
137 /// </summary>
138 /// <param name="context">The context.</param>
139 /// <param name="controller">The controller.</param>
140 /// <param name="viewName">Name of the view.</param>
141 public override void Process(IRailsEngineContext context, Controller controller, String viewName)
143 IContext ctx = CreateContext(context, controller);
147 AdjustContentType(context);
149 bool hasLayout = controller.LayoutName != null;
151 TextWriter writer;
153 if (hasLayout)
155 // Because we are rendering within a layout we need to catch it first
156 writer = new StringWriter();
158 else
160 // No layout so render direct to the output
161 writer = context.Response.Output;
164 String view = ResolveTemplateName(viewName);
166 Template template = velocity.GetTemplate(view);
168 PreSendView(controller, template);
170 BeforeMerge(velocity, template, ctx);
171 template.Merge(ctx, writer);
173 PostSendView(controller, template);
175 if (hasLayout)
177 String contents = (writer as StringWriter).GetStringBuilder().ToString();
178 ProcessLayout(contents, controller, ctx, context);
181 catch(Exception ex)
183 if (Logger.IsErrorEnabled)
185 Logger.Error("Could not render view", ex);
188 throw;
192 /// <summary>
193 /// Processes the view - using the templateName to obtain the correct template
194 /// and writes the results to the System.TextWriter. No layout is applied!
195 /// </summary>
196 public override void Process(TextWriter output, IRailsEngineContext context, Controller controller, String viewName)
198 IContext ctx = CreateContext(context, controller);
200 AdjustContentType(context);
202 String view = ResolveTemplateName(viewName);
206 Template template = velocity.GetTemplate(view);
208 BeforeMerge(velocity, template, ctx);
209 template.Merge(ctx, output);
211 catch(Exception ex)
213 if (Logger.IsErrorEnabled)
215 Logger.Error("Could not render view", ex);
218 throw new RailsException("Could not render view: " + view, ex);
222 public override void ProcessPartial(TextWriter output, IRailsEngineContext context,
223 Controller controller, string partialName)
225 IContext ctx = CreateContext(context, controller);
226 String view = ResolveTemplateName(partialName);
230 Template template = velocity.GetTemplate(view);
231 template.Merge(ctx, output);
233 catch(Exception ex)
235 if (Logger.IsErrorEnabled)
237 Logger.Error("Could not render partial", ex);
240 throw new RailsException("Could not render partial " + view, ex);
244 public override object CreateJSGenerator(IRailsEngineContext context)
246 return new JSGeneratorDuck(new PrototypeHelper.JSGenerator(context));
249 public override void GenerateJS(TextWriter output, IRailsEngineContext context, Controller controller,
250 string templateName)
252 IContext ctx = CreateContext(context, controller);
254 object generator = CreateJSGenerator(context);
256 ctx.Put("page", generator);
258 AdjustJavascriptContentType(context);
260 String view = ResolveJSTemplateName(templateName);
264 Template template = velocity.GetTemplate(view);
266 StringWriter writer = new StringWriter();
268 template.Merge(ctx, writer);
270 output.WriteLine(generator);
272 catch(Exception ex)
274 if (Logger.IsErrorEnabled)
276 Logger.Error("Could not generate JS", ex);
279 throw new RailsException("Error generating JS. Template " + templateName, ex);
283 public override void ProcessContents(IRailsEngineContext context, Controller controller, String contents)
285 IContext ctx = CreateContext(context, controller);
286 AdjustContentType(context);
288 bool hasLayout = controller.LayoutName != null;
290 if (hasLayout)
292 ProcessLayout(contents, controller, ctx, context);
294 else
296 context.Response.Output.Write(contents);
300 #endregion
302 /// <summary>
303 /// Initializes basic velocity properties. The main purpose of this method is to
304 /// allow this logic to be overrided.
305 /// </summary>
306 /// <param name="props">The <see cref="ExtendedProperties"/> collection to populate.</param>
307 protected virtual void InitializeVelocityProperties(ExtendedProperties props)
309 velocity.SetApplicationAttribute(RuntimeConstants.RESOURCE_MANAGER_CLASS,
310 new CustomResourceManager(ViewSourceLoader));
312 LoadMacros(props);
315 /// <summary>
316 /// Resolves the template name into a velocity template file name.
317 /// </summary>
318 protected string ResolveTemplateName(string templateName)
320 if (Path.HasExtension(templateName))
322 return templateName;
324 else
326 return templateName + TemplateExtension;
330 /// <summary>
331 /// Resolves the template name into a velocity JS template file name.
332 /// </summary>
333 protected string ResolveJSTemplateName(string templateName)
335 if (Path.HasExtension(templateName))
337 return templateName;
339 else
341 return templateName + JsTemplateExtension;
345 /// <summary>
346 /// Resolves the template name into a velocity template file name.
347 /// </summary>
348 protected string ResolveTemplateName(string area, string templateName)
350 return String.Format("{0}{1}{2}",
351 area, Path.DirectorySeparatorChar,
352 ResolveTemplateName(templateName));
355 protected virtual void BeforeMerge(VelocityEngine velocityEngine, Template template, IContext context)
359 private void ProcessLayout(String contents, Controller controller, IContext ctx, IRailsEngineContext context)
361 String layout = ResolveTemplateName(TemplateKeys.LayoutPath, controller.LayoutName);
365 ctx.Put(TemplateKeys.ChildContent, contents);
367 Template template = velocity.GetTemplate(layout);
369 BeforeMerge(velocity, template, ctx);
370 template.Merge(ctx, context.Response.Output);
372 catch(Exception ex)
374 if (context.Request.IsLocal)
376 SendErrorDetails(ex, context.Response.Output);
377 return;
379 else
381 throw new RailsException("Could not obtain layout: " + layout, ex);
386 private IContext CreateContext(IRailsEngineContext context, Controller controller)
388 #if DOTNET2
389 Hashtable innerContext = new Hashtable(StringComparer.InvariantCultureIgnoreCase);
390 #else
391 Hashtable innerContext = new Hashtable(CaseInsensitiveHashCodeProvider.Default, CaseInsensitiveComparer.Default);
392 #endif
394 innerContext.Add(TemplateKeys.Controller, controller);
395 innerContext.Add(TemplateKeys.Context, context);
396 innerContext.Add(TemplateKeys.Request, context.Request);
397 innerContext.Add(TemplateKeys.Response, context.Response);
398 innerContext.Add(TemplateKeys.Session, context.Session);
400 if (controller.Resources != null)
402 foreach(String key in controller.Resources.Keys)
404 innerContext[key] = controller.Resources[key];
408 foreach(String key in context.Params.AllKeys)
410 if (key == null) continue; // Nasty bug?
411 object value = context.Params[key];
412 if (value == null) continue;
413 innerContext[key] = value;
416 #if DOTNET2
417 // list from : http://msdn2.microsoft.com/en-us/library/hfa3fa08.aspx
418 object[] builtInHelpers =
419 new object[]
421 new StaticAccessorHelper<Byte>(),
422 new StaticAccessorHelper<SByte>(),
423 new StaticAccessorHelper<Int16>(),
424 new StaticAccessorHelper<Int32>(),
425 new StaticAccessorHelper<Int64>(),
426 new StaticAccessorHelper<UInt16>(),
427 new StaticAccessorHelper<UInt32>(),
428 new StaticAccessorHelper<UInt64>(),
429 new StaticAccessorHelper<Single>(),
430 new StaticAccessorHelper<Double>(),
431 new StaticAccessorHelper<Boolean>(),
432 new StaticAccessorHelper<Char>(),
433 new StaticAccessorHelper<Decimal>(),
434 new StaticAccessorHelper<String>(),
435 new StaticAccessorHelper<Guid>(),
436 new StaticAccessorHelper<DateTime>()
439 foreach(object helper in builtInHelpers)
441 innerContext[helper.GetType().GetGenericArguments()[0].Name] = helper;
443 #endif
445 foreach(object key in controller.Helpers.Keys)
447 innerContext[key] = controller.Helpers[key];
450 // Adding flash as a collection and each individual item
452 if (context.Flash != null)
454 innerContext[Flash.FlashKey] = context.Flash;
456 foreach(DictionaryEntry entry in context.Flash)
458 if (entry.Value == null) continue;
459 innerContext[entry.Key] = entry.Value;
463 if (controller.PropertyBag != null)
465 foreach(DictionaryEntry entry in controller.PropertyBag)
467 if (entry.Value == null) continue;
468 innerContext[entry.Key] = entry.Value;
472 innerContext[TemplateKeys.SiteRoot] = context.ApplicationPath;
474 return new VelocityContext(innerContext);
477 private void SendErrorDetails(Exception ex, TextWriter writer)
479 writer.WriteLine("<pre>");
480 writer.WriteLine(ex);
481 writer.WriteLine("</pre>");
484 private void LoadMacros(ExtendedProperties props)
486 String[] macros = ViewSourceLoader.ListViews("macros");
488 ArrayList macroList = new ArrayList(macros);
490 if (macroList.Count > 0)
492 object libPropValue = props.GetProperty(RuntimeConstants.VM_LIBRARY);
494 if (libPropValue is ICollection)
496 macroList.AddRange((ICollection) libPropValue);
498 else if (libPropValue is string)
500 macroList.Add(libPropValue);
503 props.AddProperty(RuntimeConstants.VM_LIBRARY, macroList);
506 props.AddProperty(RuntimeConstants.VM_LIBRARY_AUTORELOAD, true);