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
.Generic
;
19 using System
.Reflection
;
20 using System
.Collections
;
21 using System
.Collections
.Specialized
;
23 using Castle
.Components
.Binder
;
24 using Castle
.Components
.Validator
;
27 /// Specialization of <see cref="Controller"/> that tries
28 /// to match the request params to method arguments.
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.
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
>();
44 /// Represents the errors associated with an instance bound.
46 protected IDictionary
<object, ErrorList
> boundInstances
= new Dictionary
<object, ErrorList
>();
49 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
51 protected SmartDispatcherController() : this(new DataBinder())
56 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
58 /// <param name="binder">The binder.</param>
59 protected SmartDispatcherController(IDataBinder binder
)
67 /// <value>The binder.</value>
68 public IDataBinder Binder
70 get { return binder; }
74 /// Gets or sets the bound instance errors.
76 /// <value>The bound instance errors.</value>
77 public IDictionary
<object, ErrorList
> BoundInstanceErrors
79 get { return boundInstances; }
80 set { boundInstances = value; }
84 /// Gets the validation summary (key is the object instance)
86 /// <value>The validation summary per instance.</value>
87 public IDictionary
<object, ErrorSummary
> ValidationSummaryPerInstance
89 get { return validationSummaryPerInstance; }
93 /// Populates the validator error 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
);
103 /// Gets the error summary associated with validation errors.
105 /// Will only work for instances populated by the <c>DataBinder</c>
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;
116 /// Returns <c>true</c> if the given instance had
117 /// validation errors during binding.
119 /// Will only work for instances populated by the <c>DataBinder</c>
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;
134 /// Gets a list of errors that were thrown during the
135 /// object process, like conversion errors.
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
];
145 /// Constructs the parameters for the action and invokes it.
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
);
160 /// Uses a simple heuristic to select the best method -- especially in the
161 /// case of method overloads.
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
);
185 /// Selects the best method given the set of entries
186 /// avaliable on <paramref name="webParams"/> and <paramref name="actionArgs"/>
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
;
220 /// Gets the name of the request parameter.
222 /// <param name="param">The param.</param>
223 /// <returns></returns>
224 protected virtual String
GetRequestParameterName(ParameterInfo param
)
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
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
)
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
);
268 if (calculated
) continue;
270 requestParameterName
= GetRequestParameterName(param
);
276 Type parameterType
= param
.ParameterType
;
278 if (binder
.CanBindParameter(parameterType
, requestParameterName
, ParamsNode
))
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
];
293 if (binder
.Converter
.CanConvert(parameterType
, actionArg
.GetType(), actionArg
, out exactMatch
))
297 // Give extra weight to exact matches.
298 if (exactMatch
) points
+= 5;
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
)
317 /// Returns an array that hopefully fills the arguments of the selected action.
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.
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
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
);
366 // Otherwise we handle it
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
);
397 throw new RailsException(
398 String
.Format("Error building method arguments. " +
399 "Last param analyzed was {0} with value '{1}'", paramName
, value), ex
);
406 /// Binds the object of the specified type using the given prefix.
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
);
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"/>
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);
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"/>
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
);
452 /// Binds the object instance using the specified prefix.
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
);
462 /// Binds the object instance using the given prefix.
463 /// but only using the entries from the collection specified on the <paramref name="from"/>
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
);
479 /// Binds the object of the specified type using the given prefix.
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
);
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"/>
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
);
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"/>
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
);
518 /// Lazy initialized property with a hierarchical
519 /// representation of the flat data on <see cref="Controller.Params"/>
521 protected virtual internal CompositeNode ParamsNode
525 if (paramsNode
== null)
527 paramsNode
= treeBuilder
.BuildSourceNode(Params
);
528 treeBuilder
.PopulateTree(paramsNode
, HttpContext
.Request
.Files
);
536 /// Lazy initialized property with a hierarchical
537 /// representation of the flat data on <see cref="IRequest.Form"/>
539 protected virtual internal CompositeNode FormNode
543 if (formNode
== null)
545 formNode
= treeBuilder
.BuildSourceNode(Request
.Form
);
546 treeBuilder
.PopulateTree(formNode
, HttpContext
.Request
.Files
);
554 /// Lazy initialized property with a hierarchical
555 /// representation of the flat data on <see cref="IRequest.QueryString"/>
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
;
572 /// This method is for internal use only
574 /// <param name="from"></param>
575 /// <returns></returns>
576 public CompositeNode
ObtainParamsNode(ParamStore
from)
580 case ParamStore
.Form
:
582 case ParamStore
.QueryString
:
583 return QueryStringNode
;