Searching for view components should be case insensitive.
[castle.git] / MonoRail / Castle.MonoRail.Framework / ControllerLifecycleExecutor.cs
blob703da2bfb6ba54712aac2d36e0fd27ce33fef951
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.Framework
17 using System;
18 using System.Collections;
19 using System.Collections.Specialized;
20 using System.IO;
21 using System.Reflection;
22 using System.Threading;
23 using Castle.Core;
24 using Castle.Core.Logging;
25 using Castle.MonoRail.Framework.Helpers;
26 using Castle.MonoRail.Framework.Internal;
28 /// <summary>
29 /// Default implementation of <see cref="IControllerLifecycleExecutor"/>
30 /// <para>
31 /// Handles the whole controller lifecycle in a request.
32 /// </para>
33 /// </summary>
34 public class ControllerLifecycleExecutor : IControllerLifecycleExecutor, IServiceEnabledComponent
36 /// <summary>
37 /// Key for the executor instance on <c>Context.Items</c>
38 /// </summary>
39 public const String ExecutorEntry = "mr.executor";
41 private readonly Controller controller;
42 private readonly IRailsEngineContext context;
44 private ControllerMetaDescriptor metaDescriptor;
45 private IServiceProvider serviceProvider;
46 private ILogger logger = NullLogger.Instance;
48 /// <summary>
49 /// The reference to the <see cref="IViewEngineManager"/> instance
50 /// </summary>
51 private IViewEngineManager viewEngineManager;
53 private IResourceFactory resourceFactory;
54 private IScaffoldingSupport scaffoldSupport;
56 /// <summary>
57 /// Reference to the <see cref="IFilterFactory"/> instance
58 /// </summary>
59 private IFilterFactory filterFactory;
61 /// <summary>
62 /// Reference to the <see cref="ITransformFilterFactory"/> instance
63 /// </summary>
64 private ITransformFilterFactory transformFilterFactory;
66 /// <summary>
67 /// Holds the filters associated with the action
68 /// </summary>
69 private FilterDescriptor[] filters;
71 private IDynamicAction dynAction;
72 private MethodInfo actionMethod;
74 private bool skipFilters;
75 private bool hasError;
76 private bool hasConfiguredCache;
77 private Exception exceptionToThrow;
78 private IDictionary filtersToSkip;
79 private ActionMetaDescriptor actionDesc;
81 /// <summary>
82 /// Initializes a new instance of
83 /// the <see cref="ControllerLifecycleExecutor"/> class.
84 /// </summary>
85 /// <param name="controller">The controller.</param>
86 /// <param name="context">The context.</param>
87 public ControllerLifecycleExecutor(Controller controller, IRailsEngineContext context)
89 this.controller = controller;
90 this.context = context;
93 #region IServiceEnabledComponent
95 /// <summary>
96 /// Invoked by the framework in order to give a chance to
97 /// obtain other services
98 /// </summary>
99 /// <param name="provider">The service proviver</param>
100 public void Service(IServiceProvider provider)
102 viewEngineManager = (IViewEngineManager) provider.GetService(typeof(IViewEngineManager));
103 filterFactory = (IFilterFactory) provider.GetService(typeof(IFilterFactory));
104 resourceFactory = (IResourceFactory) provider.GetService(typeof(IResourceFactory));
105 scaffoldSupport = (IScaffoldingSupport) provider.GetService(typeof(IScaffoldingSupport));
106 transformFilterFactory = (ITransformFilterFactory)provider.GetService(typeof(ITransformFilterFactory));
108 ILoggerFactory loggerFactory = (ILoggerFactory) provider.GetService(typeof(ILoggerFactory));
110 if (loggerFactory != null)
112 logger = loggerFactory.Create(typeof(ControllerLifecycleExecutor));
116 #endregion
118 #region IDisposable
120 /// <summary>
121 /// Disposes the filters and resources associated with a controller.
122 /// </summary>
123 public void Dispose()
125 DisposeFilters();
127 ReleaseResources();
130 #endregion
132 #region IControllerLifecycleExecutor
134 /// <summary>
135 /// Should bring the controller to an usable
136 /// state by populating its fields with values that
137 /// represent the current request
138 /// </summary>
139 /// <param name="areaName">The area name</param>
140 /// <param name="controllerName">The controller name</param>
141 /// <param name="actionName">The action name</param>
142 public void InitializeController(string areaName, string controllerName, string actionName)
144 controller.InitializeControllerState(areaName, controllerName, actionName);
145 InitializeControllerFieldsFromServiceProvider();
146 controller.LayoutName = ObtainDefaultLayoutName();
147 CreateAndInitializeHelpers();
148 CreateFiltersDescriptors();
149 ProcessScaffoldIfPresent();
150 ActionProviderUtil.RegisterActions(controller);
152 // Record the action
153 controller.SetEvaluatedAction(actionName);
155 // Record the default view for this area/controller/action
156 controller.RenderView(actionName);
158 // If we have an HttpContext available, store the original view name
159 if (controller.HttpContext != null)
161 if (!controller.HttpContext.Items.Contains(Constants.OriginalViewKey))
163 controller.HttpContext.Items[Constants.OriginalViewKey] = controller._selectedViewName;
167 context.CurrentController = controller;
170 /// <summary>
171 /// Selects the action to execute based on the url information
172 /// </summary>
173 /// <param name="controllerName">The controller name</param>
174 /// <param name="actionName">The action name</param>
175 /// <returns></returns>
176 public bool SelectAction(string actionName, string controllerName)
178 return SelectAction(actionName, controllerName, null);
181 /// <summary>
182 /// Selects the action to execute based on the url information
183 /// </summary>
184 /// <param name="controllerName">The controller name</param>
185 /// <param name="actionName">The action name</param>
186 /// <param name="actionArgs">The action arguments.</param>
187 /// <returns></returns>
188 public bool SelectAction(string actionName, string controllerName, IDictionary actionArgs)
192 // Look for the target method
193 actionMethod = controller.SelectMethod(actionName, metaDescriptor.Actions, context.Request, actionArgs);
195 // If we couldn't find a method for this action, look for a dynamic action
196 dynAction = null;
198 if (actionMethod == null)
200 if (controller.DynamicActions.ContainsKey(actionName))
202 dynAction = controller.DynamicActions[actionName];
205 if (dynAction == null)
207 actionMethod = FindOutDefaultMethod(actionArgs);
209 if (actionMethod == null)
211 throw new ControllerException(
212 String.Format("Unable to locate action [{0}] on controller [{1}].", actionName, controllerName));
216 else
218 actionDesc = metaDescriptor.GetAction(actionMethod);
220 // Overrides the current layout, if the action specifies one
221 if (actionDesc.Layout != null)
223 controller.LayoutName = actionDesc.Layout.LayoutName;
226 if (actionDesc.AccessibleThrough != null)
228 string verbName = actionDesc.AccessibleThrough.Verb.ToString();
229 string requestType = context.RequestType;
231 if (String.Compare(verbName, requestType, true) != 0)
233 exceptionToThrow = new ControllerException(string.Format("Access to the action [{0}] " +
234 "on controller [{1}] is not allowed by the http verb [{2}].",
235 actionName, controllerName, requestType));
237 hasError = true;
239 return false;
244 // Record the action
245 controller.SetEvaluatedAction(actionName);
247 // Record the default view for this area/controller/action
248 controller.RenderView(actionName);
250 // Compute the filters that should be skipped for this action/method
251 filtersToSkip = new HybridDictionary();
252 skipFilters = ShouldSkip(actionMethod);
254 return true;
256 catch(Exception ex)
258 // There was an exception selecting the method
260 hasError = true;
262 exceptionToThrow = ex;
264 return false;
268 /// <summary>
269 /// Executes the method or the dynamic action
270 /// </summary>
271 public void ProcessSelectedAction()
273 ProcessSelectedAction(null);
276 /// <summary>
277 /// Executes the method or the dynamic action with custom arguments
278 /// </summary>
279 /// <param name="actionArgs">The action args.</param>
280 public void ProcessSelectedAction(IDictionary actionArgs)
282 bool actionSucceeded = false;
286 bool canProceed = RunBeforeActionFilters();
288 if (canProceed)
290 PrepareResources();
291 PrepareTransformFilter();
293 if (actionMethod != null)
295 controller.InvokeMethod(actionMethod, actionArgs);
297 else
299 dynAction.Execute(controller);
302 actionSucceeded = true;
304 if (!hasConfiguredCache &&
305 !context.Response.WasRedirected &&
306 actionDesc != null &&
307 context.Response.StatusCode == 200)
309 // We need to prevent that a controller.Send
310 // ends up configuring the cache for a different URL
311 hasConfiguredCache = true;
313 foreach(ICachePolicyConfigurer configurer in actionDesc.CacheConfigurers)
315 configurer.Configure(context.Response.CachePolicy);
320 catch(ThreadAbortException)
322 if (logger.IsErrorEnabled)
324 logger.Error("ThreadAbortException, process aborted");
327 hasError = true;
329 return;
331 catch(Exception ex)
333 exceptionToThrow = ex;
335 hasError = true;
338 RunAfterActionFilters();
340 if (hasError && exceptionToThrow != null)
342 if (logger.IsErrorEnabled)
344 logger.Error("Error processing action", exceptionToThrow);
347 if (context.Response.WasRedirected) return;
349 PerformErrorHandling();
351 else if (actionSucceeded)
353 if (context.Response.WasRedirected) return;
355 // If we haven't failed anywhere and no redirect was issued
356 if (!hasError && !context.Response.WasRedirected)
358 // Render the actual view then cleanup
359 ProcessView();
363 RunAfterRenderFilters();
366 private void PrepareTransformFilter()
368 PrepareTransformFilter(actionMethod);
371 /// <summary>
372 /// Prepares the transform filter.
373 /// </summary>
374 /// <param name="method">The method.</param>
375 protected void PrepareTransformFilter(MethodInfo method)
377 if (method == null) return;
379 ActionMetaDescriptor actionMeta = metaDescriptor.GetAction(method);
381 foreach (TransformFilterDescriptor transformFilter in actionMeta.TransformFilters)
383 ITransformFilter filter = transformFilterFactory.Create(transformFilter.TransformFilterType, context.UnderlyingContext.Response.Filter);
384 context.UnderlyingContext.Response.Filter = filter as Stream;
388 /// <summary>
389 /// Performs the error handling:
390 /// <para>
391 /// - Tries to run the rescue page<br/>
392 /// - Throws the exception<br/>
393 /// </para>
394 /// </summary>
395 public void PerformErrorHandling()
397 if (context.Response.WasRedirected) return;
399 // Try and perform the rescue
400 if (PerformRescue(actionMethod, exceptionToThrow))
402 exceptionToThrow = null;
405 RaiseOnActionExceptionOnExtension();
407 if (exceptionToThrow != null)
409 throw exceptionToThrow;
413 /// <summary>
414 /// Runs the start request filters.
415 /// </summary>
416 /// <returns><c>false</c> if the process should be stopped</returns>
417 public bool RunStartRequestFilters()
419 hasError = false;
420 exceptionToThrow = null;
424 // If we are supposed to run the filters...
425 if (!skipFilters)
427 // ...run them. If they fail...
428 if (!ProcessFilters(ExecuteEnum.StartRequest))
430 // Record that they failed.
431 return false;
435 catch(ThreadAbortException)
437 if (logger.IsErrorEnabled)
439 logger.Error("ThreadAbortException, process aborted");
442 hasError = true;
444 catch(Exception ex)
446 hasError = true;
448 if (logger.IsErrorEnabled)
450 logger.Error("Exception during filter process", ex);
453 exceptionToThrow = ex;
456 return ! hasError;
459 /// <summary>
460 /// Gets a value indicating whether an error has happened during controller processing
461 /// </summary>
462 /// <value>
463 /// <see langword="true"/> if has error; otherwise, <see langword="false"/>.
464 /// </value>
465 public bool HasError
467 get { return hasError; }
470 /// <summary>
471 /// Gets the controller.
472 /// </summary>
473 /// <value>The controller.</value>
474 public Controller Controller
476 get { return controller; }
479 #endregion
481 internal void InitializeControllerFieldsFromServiceProvider()
483 controller.InitializeFieldsFromServiceProvider(context);
485 serviceProvider = context;
487 metaDescriptor = controller.metaDescriptor;
489 controller.viewEngineManager = viewEngineManager;
491 ILoggerFactory loggerFactory = (ILoggerFactory) context.GetService(typeof(ILoggerFactory));
493 if (loggerFactory != null)
495 controller.logger = loggerFactory.Create(controller.GetType().Name);
499 /// <summary>
500 /// Creates the and initialize helpers associated with a controller.
501 /// </summary>
502 protected void CreateAndInitializeHelpers()
504 HybridDictionary helpers = new HybridDictionary();
506 // Custom helpers
508 foreach(HelperDescriptor helper in metaDescriptor.Helpers)
510 object helperInstance = Activator.CreateInstance(helper.HelperType);
512 IControllerAware aware = helperInstance as IControllerAware;
514 if (aware != null)
516 aware.SetController(controller);
519 PerformAdditionalHelperInitialization(helperInstance);
521 if (helpers.Contains(helper.Name))
523 throw new ControllerException(String.Format("Found a duplicate helper " +
524 "attribute named '{0}' on controller '{1}'", helper.Name,
525 controller.Name));
528 helpers.Add(helper.Name, helperInstance);
531 CreateStandardHelpers(helpers);
533 controller.helpers = helpers;
536 /// <summary>
537 /// Runs the after view rendering filters.
538 /// </summary>
539 /// <returns><c>false</c> if the process should be stopped</returns>
540 private bool RunBeforeActionFilters()
544 // If we are supposed to run the filters...
545 if (!skipFilters)
547 // ...run them. If they fail...
548 if (!ProcessFilters(ExecuteEnum.BeforeAction))
550 return false;
554 catch(ThreadAbortException)
556 if (logger.IsErrorEnabled)
558 logger.Error("ThreadAbortException, process aborted");
561 hasError = true;
563 catch(Exception ex)
565 hasError = true;
567 if (logger.IsErrorEnabled)
569 logger.Error("Exception during filter process", ex);
572 exceptionToThrow = ex;
575 return ! hasError;
578 /// <summary>
579 /// Runs the after action filters.
580 /// </summary>
581 private void RunAfterActionFilters()
583 if (skipFilters) return;
587 ProcessFilters(ExecuteEnum.AfterAction);
589 catch(Exception ex)
591 if (logger.IsErrorEnabled)
593 logger.Error("Error executing AfterAction filter(s)", ex);
598 /// <summary>
599 /// Runs the after view rendering filters.
600 /// </summary>
601 private void RunAfterRenderFilters()
603 if (skipFilters) return;
607 ProcessFilters(ExecuteEnum.AfterRendering);
609 catch(Exception ex)
611 if (logger.IsErrorEnabled)
613 logger.Error("Error executing AfterRendering filter(s)", ex);
618 private void CreateFiltersDescriptors()
620 if (metaDescriptor.Filters.Length != 0)
622 filters = CopyFilterDescriptors();
626 private void CreateStandardHelpers(IDictionary helpers)
628 AbstractHelper[] builtInHelpers =
629 new AbstractHelper[]
631 new AjaxHelper(), new BehaviourHelper(),
632 new UrlHelper(), new TextHelper(),
633 new EffectsFatHelper(), new ScriptaculousHelper(),
634 new DateFormatHelper(), new HtmlHelper(),
635 new ValidationHelper(), new DictHelper(),
636 new PaginationHelper(), new FormHelper(),
637 new ZebdaHelper()
640 foreach(AbstractHelper helper in builtInHelpers)
642 helper.SetController(controller);
644 String helperName = helper.GetType().Name;
646 if (!helpers.Contains(helperName))
648 helpers[helperName] = helper;
651 // Also makes the helper available with a less verbose name
652 // FormHelper and Form, AjaxHelper and Ajax
653 if (helperName.EndsWith("Helper"))
655 helpers[helperName.Substring(0, helperName.Length - 6)] = helper;
658 PerformAdditionalHelperInitialization(helper);
662 /// <summary>
663 /// Performs the additional helper initialization
664 /// checking if the helper instance implements <see cref="IServiceEnabledComponent"/>.
665 /// </summary>
666 /// <param name="helperInstance">The helper instance.</param>
667 private void PerformAdditionalHelperInitialization(object helperInstance)
669 IServiceEnabledComponent serviceEnabled = helperInstance as IServiceEnabledComponent;
671 if (serviceEnabled != null)
673 serviceEnabled.Service(serviceProvider);
677 /// <summary>
678 /// Invokes the scaffold support if the controller
679 /// is associated with a scaffold
680 /// </summary>
681 private void ProcessScaffoldIfPresent()
683 if (metaDescriptor.Scaffoldings.Count != 0)
685 if (scaffoldSupport == null)
687 String message = "You must enable scaffolding support on the " +
688 "configuration file, or, to use the standard ActiveRecord support " +
689 "copy the necessary assemblies to the bin folder.";
691 throw new RailsException(message);
694 scaffoldSupport.Process(controller);
698 #region Resources
700 private void PrepareResources()
702 CreateResources(actionMethod);
705 /// <summary>
706 /// Creates the resources associated with a controller
707 /// </summary>
708 /// <param name="method">The method.</param>
709 protected void CreateResources(MethodInfo method)
711 controller.resources = new ResourceDictionary();
713 Assembly typeAssembly = controller.GetType().Assembly;
715 foreach(ResourceDescriptor resource in metaDescriptor.Resources)
717 controller.resources.Add(resource.Name, resourceFactory.Create(resource, typeAssembly));
720 if (method == null) return;
722 ActionMetaDescriptor actionMeta = metaDescriptor.GetAction(method);
724 foreach(ResourceDescriptor resource in actionMeta.Resources)
726 controller.resources[resource.Name] = resourceFactory.Create(resource, typeAssembly);
730 /// <summary>
731 /// Releases the resources.
732 /// </summary>
733 protected void ReleaseResources()
735 if (controller.resources == null) return;
737 foreach(IResource resource in controller.resources.Values)
739 resourceFactory.Release(resource);
743 #endregion
745 #region Filters
747 /// <summary>
748 /// Identifies if no filter should run for the given action.
749 /// </summary>
750 /// <param name="method">The method.</param>
751 /// <returns></returns>
752 protected internal bool ShouldSkip(MethodInfo method)
754 if (method == null)
756 // Dynamic Action, run the filters if we have any
757 return (filters == null);
760 if (filters == null)
762 // No filters, so skip
763 return true;
766 ActionMetaDescriptor actionMeta = metaDescriptor.GetAction(method);
768 if (actionMeta.SkipFilters.Count == 0)
770 // Nothing against filters declared for this action
771 return false;
774 foreach(SkipFilterAttribute skipfilter in actionMeta.SkipFilters)
776 // SkipAllFilters handling...
777 if (skipfilter.BlanketSkip)
779 return true;
782 filtersToSkip[skipfilter.FilterType] = String.Empty;
785 return false;
788 /// <summary>
789 /// Clones all Filter descriptors, in order to get a writable copy.
790 /// </summary>
791 protected internal FilterDescriptor[] CopyFilterDescriptors()
793 FilterDescriptor[] clone = (FilterDescriptor[]) metaDescriptor.Filters.Clone();
795 for(int i = 0; i < clone.Length; i++)
797 clone[i] = (FilterDescriptor) clone[i].Clone();
800 return clone;
803 private bool ProcessFilters(ExecuteEnum when)
805 foreach(FilterDescriptor desc in filters)
807 if (filtersToSkip.Contains(desc.FilterType))
809 continue;
812 if ((desc.When & when) != 0)
814 if (!ProcessFilter(when, desc))
816 return false;
821 return true;
824 private bool ProcessFilter(ExecuteEnum when, FilterDescriptor desc)
826 if (desc.FilterInstance == null)
828 desc.FilterInstance = filterFactory.Create(desc.FilterType);
830 IFilterAttributeAware filterAttAware = desc.FilterInstance as IFilterAttributeAware;
832 if (filterAttAware != null)
834 filterAttAware.Filter = desc.Attribute;
840 if (logger.IsDebugEnabled)
842 logger.DebugFormat("Running filter {0}/{1}", when, desc.FilterType.FullName);
845 return desc.FilterInstance.Perform(when, context, controller);
847 catch(Exception ex)
849 if (logger.IsErrorEnabled)
851 logger.ErrorFormat("Error processing filter " + desc.FilterType.FullName, ex);
854 throw;
858 private void DisposeFilters()
860 if (filters == null) return;
862 foreach(FilterDescriptor desc in filters)
864 if (desc.FilterInstance != null)
866 filterFactory.Release(desc.FilterInstance);
871 #endregion
873 #region Views and Layout
875 /// <summary>
876 /// Obtains the name of the default layout.
877 /// </summary>
878 /// <returns></returns>
879 protected String ObtainDefaultLayoutName()
881 if (metaDescriptor.Layout != null)
883 return metaDescriptor.Layout.LayoutName;
885 else
887 String defaultLayout = String.Format("layouts/{0}", controller.Name);
889 if (controller.HasTemplate(defaultLayout))
891 return controller.Name;
895 return null;
898 private void ProcessView()
900 if (controller._selectedViewName != null)
902 viewEngineManager.Process(context, controller, controller._selectedViewName);
906 #endregion
908 #region Rescue
910 /// <summary>
911 /// Performs the rescue.
912 /// </summary>
913 /// <param name="method">The action (can be null in the case of dynamic actions).</param>
914 /// <param name="ex">The exception.</param>
915 /// <returns></returns>
916 protected bool PerformRescue(MethodInfo method, Exception ex)
918 context.LastException = (ex is TargetInvocationException) ? ex.InnerException : ex;
920 Type exceptionType = context.LastException.GetType();
922 RescueDescriptor att = null;
924 if (method != null)
926 ActionMetaDescriptor actionMeta = metaDescriptor.GetAction(method);
928 if (actionMeta.SkipRescue != null) return false;
930 att = GetRescueFor(actionMeta.Rescues, exceptionType);
933 if (att == null)
935 att = GetRescueFor(metaDescriptor.Rescues, exceptionType);
937 if (att == null) return false;
942 controller._selectedViewName = Path.Combine("rescues", att.ViewName);
943 ProcessView();
944 return true;
946 catch(Exception exception)
948 // In this situation, the rescue view could not be found
949 // So we're back to the default error exibition
951 if (logger.IsFatalEnabled)
953 logger.FatalFormat("Failed to process rescue view. View name " +
954 controller._selectedViewName, exception);
958 return false;
961 /// <summary>
962 /// Gets the rescue for the specified exception type.
963 /// </summary>
964 /// <param name="rescues">The rescues.</param>
965 /// <param name="exceptionType">Type of the exception.</param>
966 /// <returns></returns>
967 protected RescueDescriptor GetRescueFor(IList rescues, Type exceptionType)
969 if (rescues == null || rescues.Count == 0) return null;
971 RescueDescriptor bestCandidate = null;
973 foreach(RescueDescriptor rescue in rescues)
975 if (rescue.ExceptionType == exceptionType)
977 return rescue;
979 else if (rescue.ExceptionType != null &&
980 rescue.ExceptionType.IsAssignableFrom(exceptionType))
982 bestCandidate = rescue;
986 return bestCandidate;
989 #endregion
991 #region Extension
993 /// <summary>
994 /// Raises the on action exception on extension.
995 /// </summary>
996 protected void RaiseOnActionExceptionOnExtension()
998 ExtensionManager manager =
999 (ExtensionManager) serviceProvider.GetService(typeof(ExtensionManager));
1001 manager.RaiseActionError(context);
1004 #endregion
1006 /// <summary>
1007 /// The following lines were added to handle _default processing
1008 /// if present look for and load _default action method
1009 /// <seealso cref="DefaultActionAttribute"/>
1010 /// <param name="methodArgs">Method arguments</param>
1011 /// </summary>
1012 private MethodInfo FindOutDefaultMethod(IDictionary methodArgs)
1014 if (metaDescriptor.DefaultAction != null)
1016 return controller.SelectMethod(
1017 metaDescriptor.DefaultAction.DefaultAction,
1018 metaDescriptor.Actions, context.Request, methodArgs);
1021 return null;