Minor style changes
[castle.git] / MonoRail / Castle.MonoRail.Framework / SmartDispatcherController.cs
blob712c3deb3aedd20057fa0bd653e04605b6764931
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.Generic;
19 using System.Reflection;
20 using System.Collections;
21 using System.Collections.Specialized;
23 using Castle.Components.Binder;
24 using Castle.Components.Validator;
26 /// <summary>
27 /// Specialization of <see cref="Controller"/> that tries
28 /// to match the request params to method arguments.
29 /// </summary>
30 /// <remarks>
31 /// You don't even need to always use databinding within
32 /// arguments. <see cref="BindObject(ParamStore,Type,string)"/>
33 /// and <see cref="BindObjectInstance(object,string)"/>
34 /// provides the same functionality to be used in place.
35 /// </remarks>
36 public abstract class SmartDispatcherController : Controller
38 private IDataBinder binder;
39 private TreeBuilder treeBuilder = new TreeBuilder();
40 private CompositeNode paramsNode, formNode, queryStringNode;
41 private IDictionary<object, ErrorSummary> validationSummaryPerInstance = new Dictionary<object, ErrorSummary>();
43 /// <summary>
44 /// Represents the errors associated with an instance bound.
45 /// </summary>
46 protected IDictionary<object, ErrorList> boundInstances = new Dictionary<object, ErrorList>();
48 /// <summary>
49 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
50 /// </summary>
51 protected SmartDispatcherController() : this(new DataBinder())
55 /// <summary>
56 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
57 /// </summary>
58 /// <param name="binder">The binder.</param>
59 protected SmartDispatcherController(IDataBinder binder)
61 this.binder = binder;
64 /// <summary>
65 /// Gets the binder.
66 /// </summary>
67 /// <value>The binder.</value>
68 public IDataBinder Binder
70 get { return binder; }
73 /// <summary>
74 /// Gets or sets the bound instance errors.
75 /// </summary>
76 /// <value>The bound instance errors.</value>
77 public IDictionary<object, ErrorList> BoundInstanceErrors
79 get { return boundInstances; }
80 set { boundInstances = value; }
83 /// <summary>
84 /// Gets the validation summary (key is the object instance)
85 /// </summary>
86 /// <value>The validation summary per instance.</value>
87 public IDictionary<object, ErrorSummary> ValidationSummaryPerInstance
89 get { return validationSummaryPerInstance; }
92 /// <summary>
93 /// Populates the validator error summary.
94 /// </summary>
95 /// <param name="instance">The instance.</param>
96 /// <param name="binderUsedForBinding">The binder used for binding.</param>
97 protected internal void PopulateValidatorErrorSummary(object instance, IDataBinder binderUsedForBinding)
99 ValidationSummaryPerInstance[instance] = binderUsedForBinding.GetValidationSummary(instance);
102 /// <summary>
103 /// Gets the error summary associated with validation errors.
104 /// <para>
105 /// Will only work for instances populated by the <c>DataBinder</c>
106 /// </para>
107 /// </summary>
108 /// <param name="instance">object instance</param>
109 /// <returns>Error summary instance (can be null if the DataBinder wasn't configured to validate)</returns>
110 protected ErrorSummary GetErrorSummary(object instance)
112 return validationSummaryPerInstance.ContainsKey(instance) ? validationSummaryPerInstance[instance] : null;
115 /// <summary>
116 /// Returns <c>true</c> if the given instance had
117 /// validation errors during binding.
118 /// <para>
119 /// Will only work for instances populated by the <c>DataBinder</c>
120 /// </para>
121 /// </summary>
122 /// <param name="instance">object instance</param>
123 /// <returns><c>true</c> if the validation had an error</returns>
124 protected bool HasValidationError(object instance)
126 ErrorSummary summary = GetErrorSummary(instance);
128 if (summary == null) return false;
130 return summary.ErrorsCount != 0;
133 /// <summary>
134 /// Gets a list of errors that were thrown during the
135 /// object process, like conversion errors.
136 /// </summary>
137 /// <param name="instance">The instance that was populated by a binder.</param>
138 /// <returns>List of errors</returns>
139 protected ErrorList GetDataBindErrors(object instance)
141 return boundInstances[instance];
144 /// <summary>
145 /// Constructs the parameters for the action and invokes it.
146 /// </summary>
147 /// <param name="method">The method.</param>
148 /// <param name="request">The request.</param>
149 /// <param name="actionArgs">The action args.</param>
150 protected internal override void InvokeMethod(MethodInfo method, IRequest request, IDictionary actionArgs)
152 ParameterInfo[] parameters = method.GetParameters();
154 object[] methodArgs = BuildMethodArguments(parameters, request, actionArgs);
156 method.Invoke(this, methodArgs);
159 /// <summary>
160 /// Uses a simple heuristic to select the best method -- especially in the
161 /// case of method overloads.
162 /// </summary>
163 /// <param name="action">The action name</param>
164 /// <param name="actions">The avaliable actions</param>
165 /// <param name="request">The request instance</param>
166 /// <param name="actionArgs">The custom arguments for the action</param>
167 /// <returns></returns>
168 protected internal override MethodInfo SelectMethod(String action, IDictionary actions,
169 IRequest request, IDictionary actionArgs)
171 object methods = actions[action];
173 // should check for single-option as soon as possible (performance improvement)
174 if (methods is MethodInfo) return (MethodInfo) methods;
176 ArrayList candidates = (ArrayList) methods;
178 if (candidates == null) return null;
180 return SelectBestCandidate((MethodInfo[]) candidates.ToArray(typeof(MethodInfo)),
181 request.Params, actionArgs);
184 /// <summary>
185 /// Selects the best method given the set of entries
186 /// avaliable on <paramref name="webParams"/> and <paramref name="actionArgs"/>
187 /// </summary>
188 /// <param name="candidates">The candidates.</param>
189 /// <param name="webParams">The web params.</param>
190 /// <param name="actionArgs">The custom action args.</param>
191 /// <returns></returns>
192 protected virtual MethodInfo SelectBestCandidate(MethodInfo[] candidates,
193 NameValueCollection webParams,
194 IDictionary actionArgs)
196 if (candidates.Length == 1)
198 // There's nothing much to do in this situation
199 return candidates[0];
202 int lastMaxPoints = int.MinValue;
203 MethodInfo bestCandidate = null;
205 foreach(MethodInfo candidate in candidates)
207 int points = CalculatePoints(candidate, webParams, actionArgs);
209 if (lastMaxPoints < points)
211 lastMaxPoints = points;
212 bestCandidate = candidate;
216 return bestCandidate;
219 /// <summary>
220 /// Gets the name of the request parameter.
221 /// </summary>
222 /// <param name="param">The param.</param>
223 /// <returns></returns>
224 protected virtual String GetRequestParameterName(ParameterInfo param)
226 return param.Name;
229 /// <summary>
230 /// Uses a simplest algorithm to compute points for a method
231 /// based on parameters available, which in turn reflects
232 /// the best method is the one which the parameters will be
233 /// able to satistfy more arguments
234 /// </summary>
235 /// <param name="candidate">The method candidate</param>
236 /// <param name="webParams">Parameter source</param>
237 /// <param name="actionArgs">Extra parameters</param>
238 /// <returns></returns>
239 protected int CalculatePoints(MethodInfo candidate, NameValueCollection webParams, IDictionary actionArgs)
241 int points = 0;
242 int matchCount = 0;
244 ParameterInfo[] parameters = candidate.GetParameters();
246 foreach(ParameterInfo param in parameters)
249 // If the param is decorated with an attribute that implements IParameterBinder
252 object[] attributes = param.GetCustomAttributes(false);
254 String requestParameterName = null;
256 bool calculated = false;
258 foreach(object attr in attributes)
260 IParameterBinder actionParam = attr as IParameterBinder;
262 if (actionParam == null) continue;
264 points += actionParam.CalculateParamPoints(this, param);
265 calculated = true;
268 if (calculated) continue;
270 requestParameterName = GetRequestParameterName(param);
273 // Otherwise
276 Type parameterType = param.ParameterType;
278 if (binder.CanBindParameter(parameterType, requestParameterName, ParamsNode))
280 points += 10;
281 matchCount++;
284 // I'm not sure about the following. Seems to be
285 // be fragile regarding the web parameters and the actionArgs array
287 else if ((actionArgs != null) && actionArgs.Contains(requestParameterName))
289 object actionArg = actionArgs[requestParameterName];
291 bool exactMatch;
293 if (binder.Converter.CanConvert(parameterType, actionArg.GetType(), actionArg, out exactMatch))
295 points += 10;
297 // Give extra weight to exact matches.
298 if (exactMatch) points += 5;
300 matchCount++;
305 // the bonus should be nice only for disambiguation.
306 // otherwise, unmatched-parameterless-actions will always have
307 // the same weight as matched-single-parameter-actions.
308 if (matchCount == parameters.Length)
310 points += 5;
313 return points;
316 /// <summary>
317 /// Returns an array that hopefully fills the arguments of the selected action.
318 /// </summary>
319 /// <remarks>
320 /// Each parameter is inspected and we try to obtain an implementation of
321 /// <see cref="IParameterBinder"/> from the attributes the parameter have (if any).
322 /// If an implementation is found, it's used to fill the value for that parameter.
323 /// Otherwise we use simple conversion to obtain the value.
324 /// </remarks>
325 /// <param name="parameters">Parameters to obtain the values to</param>
326 /// <param name="request">The current request, which is the source to obtain the data</param>
327 /// <param name="actionArgs">Extra arguments to pass to the action.</param>
328 /// <returns>An array with the arguments values</returns>
329 protected virtual object[] BuildMethodArguments(ParameterInfo[] parameters, IRequest request, IDictionary actionArgs)
331 object[] args = new object[parameters.Length];
332 String paramName = String.Empty;
333 String value = String.Empty;
337 for(int argIndex = 0; argIndex < args.Length; argIndex++)
340 // If the parameter is decorated with an attribute
341 // that implements IParameterBinder, it's up to it
342 // to convert itself
345 ParameterInfo param = parameters[argIndex];
346 paramName = GetRequestParameterName(param);
348 bool handled = false;
350 object[] attributes = param.GetCustomAttributes(false);
352 foreach(object attr in attributes)
354 IParameterBinder paramBinder = attr as IParameterBinder;
356 if (paramBinder != null)
358 args[argIndex] = paramBinder.Bind(this, param);
360 handled = true;
361 break;
366 // Otherwise we handle it
369 if (!handled)
371 object convertedVal;
372 bool conversionSucceeded;
374 convertedVal = binder.BindParameter(param.ParameterType, paramName, ParamsNode);
376 if (convertedVal == null && (actionArgs != null) && actionArgs.Contains(paramName))
378 object actionArg = actionArgs[paramName];
380 Type actionArgType = (actionArg != null) ? actionArg.GetType() : param.ParameterType;
382 convertedVal = binder.Converter.Convert(param.ParameterType, actionArgType, actionArg, out conversionSucceeded);
385 args[argIndex] = convertedVal;
389 catch(FormatException ex)
391 throw new RailsException(
392 String.Format("Could not convert {0} to request type. " +
393 "Argument value is '{1}'", paramName, Params.Get(paramName)), ex);
395 catch(Exception ex)
397 throw new RailsException(
398 String.Format("Error building method arguments. " +
399 "Last param analyzed was {0} with value '{1}'", paramName, value), ex);
402 return args;
405 /// <summary>
406 /// Binds the object of the specified type using the given prefix.
407 /// </summary>
408 /// <param name="targetType">Type of the target.</param>
409 /// <param name="prefix">The prefix.</param>
410 /// <returns></returns>
411 protected object BindObject(Type targetType, String prefix)
413 return BindObject(ParamStore.Params, targetType, prefix);
416 /// <summary>
417 /// Binds the object of the specified type using the given prefix.
418 /// but only using the entries from the collection specified on the <paramref name="from"/>
419 /// </summary>
420 /// <param name="from">Restricts the data source of entries.</param>
421 /// <param name="targetType">Type of the target.</param>
422 /// <param name="prefix">The prefix.</param>
423 /// <returns></returns>
424 protected object BindObject(ParamStore from, Type targetType, String prefix)
426 return BindObject(from, targetType, prefix, null, null);
429 /// <summary>
430 /// Binds the object of the specified type using the given prefix.
431 /// but only using the entries from the collection specified on the <paramref name="from"/>
432 /// </summary>
433 /// <param name="from">From.</param>
434 /// <param name="targetType">Type of the target.</param>
435 /// <param name="prefix">The prefix.</param>
436 /// <param name="excludedProperties">The excluded properties, comma separated list.</param>
437 /// <param name="allowedProperties">The allowed properties, comma separated list.</param>
438 /// <returns></returns>
439 protected object BindObject(ParamStore from, Type targetType, String prefix, String excludedProperties, String allowedProperties)
441 CompositeNode node = ObtainParamsNode(from);
443 object instance = binder.BindObject(targetType, prefix, excludedProperties, allowedProperties, node);
445 boundInstances[instance] = binder.ErrorList;
446 PopulateValidatorErrorSummary(instance, binder);
448 return instance;
451 /// <summary>
452 /// Binds the object instance using the specified prefix.
453 /// </summary>
454 /// <param name="instance">The instance.</param>
455 /// <param name="prefix">The prefix.</param>
456 protected void BindObjectInstance(object instance, String prefix)
458 BindObjectInstance(instance, ParamStore.Params, prefix);
461 /// <summary>
462 /// Binds the object instance using the given prefix.
463 /// but only using the entries from the collection specified on the <paramref name="from"/>
464 /// </summary>
465 /// <param name="instance">The instance.</param>
466 /// <param name="from">From.</param>
467 /// <param name="prefix">The prefix.</param>
468 protected void BindObjectInstance(object instance, ParamStore from, String prefix)
470 CompositeNode node = ObtainParamsNode(from);
472 binder.BindObjectInstance(instance, prefix, node);
474 boundInstances[instance] = binder.ErrorList;
475 PopulateValidatorErrorSummary(instance, binder);
478 /// <summary>
479 /// Binds the object of the specified type using the given prefix.
480 /// </summary>
481 /// <typeparam name="T">Target type</typeparam>
482 /// <param name="prefix">The prefix.</param>
483 /// <returns></returns>
484 protected T BindObject<T>(String prefix)
486 return (T) BindObject(typeof(T), prefix);
489 /// <summary>
490 /// Binds the object of the specified type using the given prefix.
491 /// but only using the entries from the collection specified on the <paramref name="from"/>
492 /// </summary>
493 /// <typeparam name="T">Target type</typeparam>
494 /// <param name="from">From.</param>
495 /// <param name="prefix">The prefix.</param>
496 /// <returns></returns>
497 protected T BindObject<T>(ParamStore from, String prefix)
499 return (T) BindObject(from, typeof(T), prefix);
502 /// <summary>
503 /// Binds the object of the specified type using the given prefix.
504 /// but only using the entries from the collection specified on the <paramref name="from"/>
505 /// </summary>
506 /// <typeparam name="T"></typeparam>
507 /// <param name="from">From.</param>
508 /// <param name="prefix">The prefix.</param>
509 /// <param name="excludedProperties">The excluded properties.</param>
510 /// <param name="allowedProperties">The allowed properties.</param>
511 /// <returns></returns>
512 protected T BindObject<T>(ParamStore from, String prefix, String excludedProperties, String allowedProperties)
514 return (T) BindObject(from, typeof(T), prefix, excludedProperties, allowedProperties);
517 /// <summary>
518 /// Lazy initialized property with a hierarchical
519 /// representation of the flat data on <see cref="Controller.Params"/>
520 /// </summary>
521 protected virtual internal CompositeNode ParamsNode
525 if (paramsNode == null)
527 paramsNode = treeBuilder.BuildSourceNode(Params);
528 treeBuilder.PopulateTree(paramsNode, HttpContext.Request.Files);
531 return paramsNode;
535 /// <summary>
536 /// Lazy initialized property with a hierarchical
537 /// representation of the flat data on <see cref="IRequest.Form"/>
538 /// </summary>
539 protected virtual internal CompositeNode FormNode
543 if (formNode == null)
545 formNode = treeBuilder.BuildSourceNode(Request.Form);
546 treeBuilder.PopulateTree(formNode, HttpContext.Request.Files);
549 return formNode;
553 /// <summary>
554 /// Lazy initialized property with a hierarchical
555 /// representation of the flat data on <see cref="IRequest.QueryString"/>
556 /// </summary>
557 protected virtual internal CompositeNode QueryStringNode
561 if (queryStringNode == null)
563 queryStringNode = treeBuilder.BuildSourceNode(Request.QueryString);
564 treeBuilder.PopulateTree(queryStringNode, HttpContext.Request.Files);
567 return queryStringNode;
571 /// <summary>
572 /// This method is for internal use only
573 /// </summary>
574 /// <param name="from"></param>
575 /// <returns></returns>
576 public CompositeNode ObtainParamsNode(ParamStore from)
578 switch(from)
580 case ParamStore.Form:
581 return FormNode;
582 case ParamStore.QueryString:
583 return QueryStringNode;
584 default:
585 return ParamsNode;