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
.Framework
18 using System
.Collections
;
19 using System
.Collections
.Specialized
;
21 using System
.Reflection
;
22 using System
.Threading
;
24 using Castle
.Core
.Logging
;
25 using Castle
.MonoRail
.Framework
.Helpers
;
26 using Castle
.MonoRail
.Framework
.Internal
;
29 /// Default implementation of <see cref="IControllerLifecycleExecutor"/>
31 /// Handles the whole controller lifecycle in a request.
34 public class ControllerLifecycleExecutor
: IControllerLifecycleExecutor
, IServiceEnabledComponent
37 /// Key for the executor instance on <c>Context.Items</c>
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
;
49 /// The reference to the <see cref="IViewEngineManager"/> instance
51 private IViewEngineManager viewEngineManager
;
53 private IResourceFactory resourceFactory
;
54 private IScaffoldingSupport scaffoldSupport
;
57 /// Reference to the <see cref="IFilterFactory"/> instance
59 private IFilterFactory filterFactory
;
62 /// Reference to the <see cref="ITransformFilterFactory"/> instance
64 private ITransformFilterFactory transformFilterFactory
;
67 /// Holds the filters associated with the action
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
;
82 /// Initializes a new instance of
83 /// the <see cref="ControllerLifecycleExecutor"/> class.
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
96 /// Invoked by the framework in order to give a chance to
97 /// obtain other services
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
));
121 /// Disposes the filters and resources associated with a controller.
123 public void Dispose()
132 #region IControllerLifecycleExecutor
135 /// Should bring the controller to an usable
136 /// state by populating its fields with values that
137 /// represent the current request
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
);
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
;
171 /// Selects the action to execute based on the url information
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);
182 /// Selects the action to execute based on the url information
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
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
));
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
));
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
);
258 // There was an exception selecting the method
262 exceptionToThrow
= ex
;
269 /// Executes the method or the dynamic action
271 public void ProcessSelectedAction()
273 ProcessSelectedAction(null);
277 /// Executes the method or the dynamic action with custom arguments
279 /// <param name="actionArgs">The action args.</param>
280 public void ProcessSelectedAction(IDictionary actionArgs
)
282 bool actionSucceeded
= false;
286 bool canProceed
= RunBeforeActionFilters();
291 PrepareTransformFilter();
293 if (actionMethod
!= null)
295 controller
.InvokeMethod(actionMethod
, actionArgs
);
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");
333 exceptionToThrow
= ex
;
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
363 RunAfterRenderFilters();
366 private void PrepareTransformFilter()
368 PrepareTransformFilter(actionMethod
);
372 /// Prepares the transform filter.
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
;
389 /// Performs the error handling:
391 /// - Tries to run the rescue page<br/>
392 /// - Throws the exception<br/>
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
;
414 /// Runs the start request filters.
416 /// <returns><c>false</c> if the process should be stopped</returns>
417 public bool RunStartRequestFilters()
420 exceptionToThrow
= null;
424 // If we are supposed to run the filters...
427 // ...run them. If they fail...
428 if (!ProcessFilters(ExecuteEnum
.StartRequest
))
430 // Record that they failed.
435 catch(ThreadAbortException
)
437 if (logger
.IsErrorEnabled
)
439 logger
.Error("ThreadAbortException, process aborted");
448 if (logger
.IsErrorEnabled
)
450 logger
.Error("Exception during filter process", ex
);
453 exceptionToThrow
= ex
;
460 /// Gets a value indicating whether an error has happened during controller processing
463 /// <see langword="true"/> if has error; otherwise, <see langword="false"/>.
467 get { return hasError; }
471 /// Gets the controller.
473 /// <value>The controller.</value>
474 public Controller Controller
476 get { return controller; }
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
);
500 /// Creates the and initialize helpers associated with a controller.
502 protected void CreateAndInitializeHelpers()
504 HybridDictionary helpers
= new HybridDictionary();
508 foreach(HelperDescriptor helper
in metaDescriptor
.Helpers
)
510 object helperInstance
= Activator
.CreateInstance(helper
.HelperType
);
512 IControllerAware aware
= helperInstance
as IControllerAware
;
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
,
528 helpers
.Add(helper
.Name
, helperInstance
);
531 CreateStandardHelpers(helpers
);
533 controller
.helpers
= helpers
;
537 /// Runs the after view rendering filters.
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...
547 // ...run them. If they fail...
548 if (!ProcessFilters(ExecuteEnum
.BeforeAction
))
554 catch(ThreadAbortException
)
556 if (logger
.IsErrorEnabled
)
558 logger
.Error("ThreadAbortException, process aborted");
567 if (logger
.IsErrorEnabled
)
569 logger
.Error("Exception during filter process", ex
);
572 exceptionToThrow
= ex
;
579 /// Runs the after action filters.
581 private void RunAfterActionFilters()
583 if (skipFilters
) return;
587 ProcessFilters(ExecuteEnum
.AfterAction
);
591 if (logger
.IsErrorEnabled
)
593 logger
.Error("Error executing AfterAction filter(s)", ex
);
599 /// Runs the after view rendering filters.
601 private void RunAfterRenderFilters()
603 if (skipFilters
) return;
607 ProcessFilters(ExecuteEnum
.AfterRendering
);
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
=
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(),
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
);
663 /// Performs the additional helper initialization
664 /// checking if the helper instance implements <see cref="IServiceEnabledComponent"/>.
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
);
678 /// Invokes the scaffold support if the controller
679 /// is associated with a scaffold
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
);
700 private void PrepareResources()
702 CreateResources(actionMethod
);
706 /// Creates the resources associated with a controller
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
);
731 /// Releases the resources.
733 protected void ReleaseResources()
735 if (controller
.resources
== null) return;
737 foreach(IResource resource
in controller
.resources
.Values
)
739 resourceFactory
.Release(resource
);
748 /// Identifies if no filter should run for the given action.
750 /// <param name="method">The method.</param>
751 /// <returns></returns>
752 protected internal bool ShouldSkip(MethodInfo method
)
756 // Dynamic Action, run the filters if we have any
757 return (filters
== null);
762 // No filters, so skip
766 ActionMetaDescriptor actionMeta
= metaDescriptor
.GetAction(method
);
768 if (actionMeta
.SkipFilters
.Count
== 0)
770 // Nothing against filters declared for this action
774 foreach(SkipFilterAttribute skipfilter
in actionMeta
.SkipFilters
)
776 // SkipAllFilters handling...
777 if (skipfilter
.BlanketSkip
)
782 filtersToSkip
[skipfilter
.FilterType
] = String
.Empty
;
789 /// Clones all Filter descriptors, in order to get a writable copy.
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();
803 private bool ProcessFilters(ExecuteEnum when
)
805 foreach(FilterDescriptor desc
in filters
)
807 if (filtersToSkip
.Contains(desc
.FilterType
))
812 if ((desc
.When
& when
) != 0)
814 if (!ProcessFilter(when
, desc
))
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
);
849 if (logger
.IsErrorEnabled
)
851 logger
.ErrorFormat("Error processing filter " + desc
.FilterType
.FullName
, ex
);
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
);
873 #region Views and Layout
876 /// Obtains the name of the default layout.
878 /// <returns></returns>
879 protected String
ObtainDefaultLayoutName()
881 if (metaDescriptor
.Layout
!= null)
883 return metaDescriptor
.Layout
.LayoutName
;
887 String defaultLayout
= String
.Format("layouts/{0}", controller
.Name
);
889 if (controller
.HasTemplate(defaultLayout
))
891 return controller
.Name
;
898 private void ProcessView()
900 if (controller
._selectedViewName
!= null)
902 viewEngineManager
.Process(context
, controller
, controller
._selectedViewName
);
911 /// Performs the rescue.
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;
926 ActionMetaDescriptor actionMeta
= metaDescriptor
.GetAction(method
);
928 if (actionMeta
.SkipRescue
!= null) return false;
930 att
= GetRescueFor(actionMeta
.Rescues
, exceptionType
);
935 att
= GetRescueFor(metaDescriptor
.Rescues
, exceptionType
);
937 if (att
== null) return false;
942 controller
._selectedViewName
= Path
.Combine("rescues", att
.ViewName
);
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
);
962 /// Gets the rescue for the specified exception type.
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
)
979 else if (rescue
.ExceptionType
!= null &&
980 rescue
.ExceptionType
.IsAssignableFrom(exceptionType
))
982 bestCandidate
= rescue
;
986 return bestCandidate
;
994 /// Raises the on action exception on extension.
996 protected void RaiseOnActionExceptionOnExtension()
998 ExtensionManager manager
=
999 (ExtensionManager
) serviceProvider
.GetService(typeof(ExtensionManager
));
1001 manager
.RaiseActionError(context
);
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>
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
);