- Implemented support for view component caching. Just use the attribute
[castle.git] / MonoRail / Castle.MonoRail.Framework.Views.NVelocity / CustomDirectives / AbstractComponentDirective.cs
blob4391b9c0edc77ac4aeeb39356a885cb8e9840ceb
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.Runtime.Parser.Node;
16 using NVelocity.Runtime.Resource;
17 using NVelocity.Runtime.Directive;
18 using NVelocity.Runtime;
19 using NVelocity.Exception;
20 using NVelocity.Runtime.Parser;
21 using Template = NVelocity.Template;
23 namespace Castle.MonoRail.Framework.Views.NVelocity.CustomDirectives
25 using System;
26 using System.Collections;
27 using System.Collections.Generic;
28 using System.Collections.Specialized;
29 using System.IO;
30 using System.Text;
31 using Castle.MonoRail.Framework;
32 using Castle.MonoRail.Framework.Internal;
33 using global::NVelocity.App.Events;
34 using global::NVelocity.Context;
35 using global::NVelocity.Util.Introspection;
37 /// <summary>
38 /// Pendent
39 /// </summary>
40 public abstract class AbstractComponentDirective : Directive, IViewRenderer
42 private readonly IViewComponentFactory viewComponentFactory;
44 private String componentName;
45 private ViewComponent component;
46 private NVelocityViewContextAdapter contextAdapter;
47 private IViewEngine viewEngine;
48 private INode compNameNode;
50 /// <summary>
51 /// Initializes a new instance of the <see cref="AbstractComponentDirective"/> class.
52 /// </summary>
53 /// <param name="viewComponentFactory">The view component factory.</param>
54 /// <param name="viewEngine">The view engine instance</param>
55 public AbstractComponentDirective(IViewComponentFactory viewComponentFactory, IViewEngine viewEngine)
57 this.viewComponentFactory = viewComponentFactory;
58 this.viewEngine = viewEngine;
61 public override void Init(IRuntimeServices rs, IInternalContextAdapter context, INode node)
63 base.Init(rs, context, node);
65 compNameNode = node.GetChild(0);
67 if (compNameNode == null)
69 String message = String.Format("You must specify the component name on the #{0} directive", Name);
70 throw new ViewComponentException(message);
74 public override bool Render(IInternalContextAdapter context, TextWriter writer, INode node)
76 IRailsEngineContext railsContext = MonoRailHttpHandler.CurrentContext;
77 IViewComponentRegistry registry = railsContext.GetService<IViewComponentFactory>().Registry;
78 IViewComponentDescriptorProvider viewDescProvider = railsContext.GetService<IViewComponentDescriptorProvider>();
79 ICacheProvider cacheProvider = railsContext.GetService<ICacheProvider>();
81 componentName = compNameNode.FirstToken.Image;
83 if (componentName == null)
85 String message = String.Format("Could not obtain component name from the #{0} directive", Name);
86 throw new ViewComponentException(message);
89 if (componentName.StartsWith("$"))
91 String nodeContent = compNameNode.Literal.Trim('"', '\'');
92 SimpleNode inlineNode = rsvc.Parse(new StringReader(nodeContent), context.CurrentTemplateName, false);
94 inlineNode.Init(context, rsvc);
95 componentName = (String) Evaluate(inlineNode, context);
98 IDictionary componentParams = CreateParameters(context, node);
100 Type viewComptype = registry.GetViewComponent(componentName);
102 ViewComponentDescriptor descriptor = null;
103 CacheKey key = null;
105 if (viewComptype != null)
107 descriptor = viewDescProvider.Collect(viewComptype);
110 bool isOutputtingToCache = false;
111 ViewComponentCacheBag bag = null;
113 if (descriptor != null && descriptor.IsCacheable)
115 key = descriptor.CacheKeyGenerator.Create(componentName, componentParams, railsContext);
117 if (key == null)
119 throw new MonoRailException("CacheKeyGenerator returned a null CacheKey implementation (Not good at all). " +
120 "Please investigate the implementation: " + descriptor.CacheKeyGenerator.GetType().FullName);
123 ViewComponentCacheBag cachedContent = (ViewComponentCacheBag) cacheProvider.Get(key.ToString());
125 if (cachedContent != null)
127 // Restore entries
129 foreach(KeyValuePair<string, object> pair in cachedContent.ContextEntries)
131 context[pair.Key] = pair.Value;
134 // Render from cache
136 writer.Write(cachedContent.Content);
138 return true;
141 isOutputtingToCache = true;
142 bag = new ViewComponentCacheBag();
145 component = viewComponentFactory.Create(componentName);
147 if (component == null)
149 throw new MonoRailException("ViewComponentFactory returned a null ViewComponent for " + componentName + ". " +
150 "Please investigate the implementation: " + viewComponentFactory.GetType().FullName);
153 ASTDirective directiveNode = (ASTDirective) node;
154 IViewRenderer renderer = (IViewRenderer) directiveNode.Directive;
156 contextAdapter = new NVelocityViewContextAdapter(componentName, node, viewEngine, renderer);
157 contextAdapter.Context = isOutputtingToCache ? new CacheAwareContext(context, bag) : context;
159 ProcessSubSections();
161 INode bodyNode = null;
163 if (node.ChildrenCount > 0)
165 bodyNode = node.GetChild(node.ChildrenCount - 1);
169 TextWriter output = isOutputtingToCache ? bag.CacheWriter : writer;
171 contextAdapter.BodyNode = bodyNode;
172 contextAdapter.ComponentParams = componentParams;
173 contextAdapter.TextWriter = output;
175 const string ViewComponentContextKey = "viewcomponent";
179 context[ViewComponentContextKey] = component;
181 component.Init(railsContext, contextAdapter);
183 component.Render();
185 if (contextAdapter.ViewToRender != null)
187 RenderComponentView(context, contextAdapter.ViewToRender, output, contextAdapter);
190 if (isOutputtingToCache)
192 // Save output
194 cacheProvider.Store(key.ToString(), bag);
196 // Output to correct writer
198 writer.Write(bag.Content);
201 finally
203 context.Remove(ViewComponentContextKey);
206 return true;
209 public bool RenderComponentView(IInternalContextAdapter context, String viewToRender, TextWriter writer, NVelocityViewContextAdapter contextAdapter)
211 viewToRender = viewToRender + NVelocityViewEngine.TemplateExtension;
213 CheckTemplateStack(context);
215 String encoding = ExtractEncoding(context);
217 Template template = GetTemplate(viewToRender, encoding);
219 return RenderView(context, viewToRender, template, writer);
222 protected virtual void ProcessSubSections()
226 protected virtual IDictionary CreateParameters(IInternalContextAdapter context, INode node)
228 int childrenCount = node.ChildrenCount;
230 if (childrenCount > 1)
232 INode lastNode = node.GetChild(childrenCount - 1);
234 if (lastNode.Type == ParserTreeConstants.BLOCK)
236 childrenCount--;
239 if (childrenCount > 1)
241 IDictionary dict = ProcessFirstParam(node, context, childrenCount);
243 if (dict != null)
245 return dict;
247 else if (childrenCount > 2)
249 return ProcessRemainingParams(childrenCount, node, context);
254 return new Hashtable(0);
257 protected string ComponentName
259 get { return componentName; }
262 protected ViewComponent Component
264 get { return component; }
267 protected NVelocityViewContextAdapter ContextAdapter
269 get { return contextAdapter; }
272 private IDictionary ProcessRemainingParams(int childrenCount, INode node, IInternalContextAdapter context)
274 IDictionary entries = new HybridDictionary(true);
276 for(int i = 2; i < childrenCount; i++)
278 INode paramNode = node.GetChild(i);
280 string nodeContent = paramNode.Literal.TrimStart('"', '\'').TrimEnd('"', '\'');
282 string[] parts = nodeContent.Split(new char[] {'='}, 2, StringSplitOptions.RemoveEmptyEntries);
284 if (parts.Length == 2 && parts[1].IndexOf("$") != -1)
286 SimpleNode inlineNode = rsvc.Parse(new StringReader(parts[1]), context.CurrentTemplateName, false);
288 inlineNode.Init(context, rsvc);
290 entries[parts[0]] = Evaluate(inlineNode, context);
292 else if (parts.Length == 2)
294 entries[parts[0]] = parts[1];
296 else
298 entries[parts[0]] = String.Empty;
302 return entries;
305 private object Evaluate(SimpleNode inlineNode, IInternalContextAdapter context)
307 ArrayList values = new ArrayList();
309 for(int i=0; i < inlineNode.ChildrenCount; i++)
311 INode node = inlineNode.GetChild(i);
313 if (node.Type == ParserTreeConstants.TEXT)
315 values.Add(((ASTText)node).Text);
317 else
319 values.Add(node.Value(context));
323 if (values.Count == 0)
325 return null;
327 else if (values.Count == 1)
329 return values[0];
331 else
333 StringBuilder sb = new StringBuilder();
334 foreach(object value in values)
336 sb.Append(value);
338 return sb.ToString();
342 /// <summary>
343 /// Processes the first param.
344 /// first param can either be the literal string 'with' which means the user
345 /// is using the syntax #blockcomponent(ComponentName with "param1=value1" "param2=value2")
346 /// or it could be a dictionary string like:
347 /// #blockcomponent(ComponentName "#{ param1='value1', param2='value2' }")
348 /// anything different than that will throw an exception
349 /// </summary>
350 /// <param name="node">The node.</param>
351 /// <param name="context">The context.</param>
352 /// <param name="childrenCount">The children count.</param>
353 /// <returns></returns>
354 private IDictionary ProcessFirstParam(INode node, IInternalContextAdapter context, int childrenCount)
356 INode withNode = node.GetChild(1);
358 String withName = withNode.FirstToken.Image;
360 if (!"with".Equals(withName))
362 IDictionary dict = withNode.Value(context) as IDictionary;
364 if (dict != null)
366 if (childrenCount > 2)
368 String message = String.Format("A #{0} directive with a dictionary " +
369 "string parameter cannot have extra params - component {0}", componentName);
370 throw new ViewComponentException(message);
372 return dict;
374 else
376 String message = String.Format("A #{0} directive with parameters must use " +
377 "the keyword 'with' - component {0}", componentName);
378 throw new ViewComponentException(message);
382 return null;
385 private bool RenderView(IInternalContextAdapter context,
386 String viewToRender, Template template, TextWriter writer)
390 context.PushCurrentTemplateName(viewToRender);
391 ((SimpleNode) template.Data).Render(context, writer);
393 catch(Exception e)
395 if (e is MethodInvocationException)
397 throw;
400 return false;
402 finally
404 context.PopCurrentTemplateName();
407 return true;
410 private Template GetTemplate(String viewToRender, String encoding)
412 return rsvc.GetTemplate(viewToRender, encoding);
415 private String ExtractEncoding(IInternalContextAdapter context)
417 Resource current = context.CurrentResource;
419 String encoding = null;
421 if (current != null)
423 encoding = current.Encoding;
425 else
427 encoding = (String) rsvc.GetProperty(RuntimeConstants.INPUT_ENCODING);
430 return encoding;
433 private void CheckTemplateStack(IInternalContextAdapter context)
435 Object[] templateStack = context.TemplateNameStack;
437 if (templateStack.Length >= rsvc.GetInt(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, 20))
439 StringBuilder path = new StringBuilder();
441 for(int i = 0; i < templateStack.Length; ++i)
443 path.Append(" > " + templateStack[i]);
446 throw new Exception("Max recursion depth reached (" + templateStack.Length + ")" + " File stack:" + path);
451 public class CacheAwareContext : IInternalContextAdapter
453 private readonly IInternalContextAdapter context;
454 private readonly ViewComponentCacheBag bag;
456 public CacheAwareContext(IInternalContextAdapter context, ViewComponentCacheBag bag)
458 this.context = context;
459 this.bag = bag;
462 object IInternalContextAdapter.Remove(object key)
464 if (bag.ContextEntries.ContainsKey(key.ToString()))
466 bag.ContextEntries.Remove(key.ToString());
468 return context.Remove(key);
471 public void PushCurrentTemplateName(string s)
473 context.PushCurrentTemplateName(s);
476 public void PopCurrentTemplateName()
478 context.PopCurrentTemplateName();
481 public IntrospectionCacheData ICacheGet(object key)
483 return context.ICacheGet(key);
486 public void ICachePut(object key, IntrospectionCacheData o)
488 context.ICachePut(key, o);
491 public string CurrentTemplateName
493 get { return context.CurrentTemplateName; }
496 public object[] TemplateNameStack
498 get { return context.TemplateNameStack; }
501 public Resource CurrentResource
503 get { return context.CurrentResource; }
504 set { context.CurrentResource = value; }
507 public object Put(string key, object value)
509 bag.ContextEntries[key] = value;
511 return context.Put(key, value);
514 public object Get(string key)
516 return context.Get(key);
519 public bool ContainsKey(object key)
521 return context.ContainsKey(key);
524 object IContext.Remove(object key)
526 if (bag.ContextEntries.ContainsKey(key.ToString()))
528 bag.ContextEntries.Remove(key.ToString());
531 return context.Remove(key);
534 public IContext InternalUserContext
536 get { return context.InternalUserContext; }
539 public IInternalContextAdapter BaseContext
541 get { return context.BaseContext; }
544 public EventCartridge AttachEventCartridge(EventCartridge ec)
546 return context.AttachEventCartridge(ec);
549 public EventCartridge EventCartridge
551 get { return context.EventCartridge; }
554 public bool Contains(object key)
556 return context.Contains(key);
559 public void Add(object key, object value)
561 bag.ContextEntries[key.ToString()] = value;
562 context.Add(key, value);
565 public void Clear()
567 context.Clear();
570 IDictionaryEnumerator IDictionary.GetEnumerator()
572 return context.GetEnumerator();
575 public void Remove(object key)
577 if (bag.ContextEntries.ContainsKey(key.ToString()))
579 bag.ContextEntries.Remove(key.ToString());
582 ((IDictionary) context).Remove(key);
585 public object this[object key]
587 get { return context[key]; }
590 bag.ContextEntries[key.ToString()] = value;
591 context[key] = value;
595 public ICollection Keys
597 get { return ((IDictionary) context).Keys; }
600 object[] IContext.Keys
602 get { return ((IContext)context).Keys; }
605 public ICollection Values
607 get { return context.Values; }
610 public bool IsReadOnly
612 get { return context.IsReadOnly; }
615 public bool IsFixedSize
617 get { return context.IsFixedSize; }
620 public void CopyTo(Array array, int index)
622 context.CopyTo(array, index);
625 public int Count
627 get { return ((ICollection) context).Count; }
630 public object SyncRoot
632 get { return context.SyncRoot; }
635 public bool IsSynchronized
637 get { return context.IsSynchronized; }
640 public IEnumerator GetEnumerator()
642 return ((IEnumerable) context).GetEnumerator();