1 // Copyright 2004-2008 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
;
22 using Castle
.Components
.Binder
;
26 /// Specialization of <see cref="Controller"/> that tries
27 /// to match the request params to method arguments.
30 /// You don't even need to always use databinding within
31 /// arguments. <see cref="BindObject(ParamStore,Type,string)"/>
32 /// and <see cref="BindObjectInstance(object,string)"/>
33 /// provides the same functionality to be used in place.
35 public abstract class SmartDispatcherController
: Controller
37 private IDataBinder binder
;
40 /// Represents the errors associated with an instance bound.
42 protected IDictionary
<object, ErrorList
> boundInstances
= new Dictionary
<object, ErrorList
>();
45 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
47 protected SmartDispatcherController() : this(new DataBinder())
52 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
54 /// <param name="binder">The binder.</param>
55 protected SmartDispatcherController(IDataBinder binder
)
63 /// <value>The binder.</value>
64 public IDataBinder Binder
66 get { return binder; }
70 /// Constructs the parameters for the action and invokes it.
72 /// <param name="method">The method.</param>
73 /// <param name="request">The request.</param>
74 /// <param name="extraArgs">The extra args.</param>
75 /// <returns></returns>
76 protected override object InvokeMethod(MethodInfo method
, IRequest request
, IDictionary
<string, object> extraArgs
)
78 ParameterInfo
[] parameters
= method
.GetParameters();
80 object[] methodArgs
= BuildMethodArguments(parameters
, request
, extraArgs
);
82 return method
.Invoke(this, methodArgs
);
86 /// Uses a simple heuristic to select the best method -- especially in the
87 /// case of method overloads.
89 /// <param name="action">The action name</param>
90 /// <param name="actions">The avaliable actions</param>
91 /// <param name="request">The request instance</param>
92 /// <param name="actionArgs">The custom arguments for the action</param>
93 /// <param name="actionType">Type of the action.</param>
94 /// <returns></returns>
95 protected override MethodInfo
SelectMethod(string action
, IDictionary actions
, IRequest request
, IDictionary
<string, object> actionArgs
, ActionType actionType
)
97 object methods
= actions
[action
];
99 // should check for single-option as soon as possible (performance improvement)
100 if (methods
is MethodInfo
) return (MethodInfo
) methods
;
102 if (methods
is AsyncActionPair
)
104 AsyncActionPair pair
= (AsyncActionPair
) methods
;
105 if (actionType
== ActionType
.AsyncBegin
)
106 return pair
.BeginActionInfo
;
108 return pair
.EndActionInfo
;
111 ArrayList candidates
= (ArrayList
) methods
;
113 if (candidates
== null) return null;
115 return SelectBestCandidate((MethodInfo
[]) candidates
.ToArray(typeof(MethodInfo
)),
116 request
.Params
, actionArgs
);
120 /// Selects the best method given the set of entries
121 /// avaliable on <paramref name="webParams"/> and <paramref name="actionArgs"/>
123 /// <param name="candidates">The candidates.</param>
124 /// <param name="webParams">The web params.</param>
125 /// <param name="actionArgs">The custom action args.</param>
126 /// <returns></returns>
127 protected virtual MethodInfo
SelectBestCandidate(MethodInfo
[] candidates
,
128 NameValueCollection webParams
,
129 IDictionary
<string, object> actionArgs
)
131 if (candidates
.Length
== 1)
133 // There's nothing much to do in this situation
134 return candidates
[0];
137 int lastMaxPoints
= int.MinValue
;
138 MethodInfo bestCandidate
= null;
140 foreach(MethodInfo candidate
in candidates
)
142 int points
= CalculatePoints(candidate
, webParams
, actionArgs
);
144 if (lastMaxPoints
< points
)
146 lastMaxPoints
= points
;
147 bestCandidate
= candidate
;
151 return bestCandidate
;
155 /// Gets the name of the request parameter.
157 /// <param name="param">The param.</param>
158 /// <returns></returns>
159 protected virtual String
GetRequestParameterName(ParameterInfo param
)
165 /// Uses a simplest algorithm to compute points for a method
166 /// based on parameters available, which in turn reflects
167 /// the best method is the one which the parameters will be
168 /// able to satistfy more arguments
170 /// <param name="candidate">The method candidate</param>
171 /// <param name="webParams">Parameter source</param>
172 /// <param name="actionArgs">Extra parameters</param>
173 /// <returns></returns>
174 protected int CalculatePoints(MethodInfo candidate
, NameValueCollection webParams
, IDictionary
<string, object> actionArgs
)
179 ParameterInfo
[] parameters
= candidate
.GetParameters();
181 foreach(ParameterInfo param
in parameters
)
184 // If the param is decorated with an attribute that implements IParameterBinder
185 // then it calculates the points itself
188 object[] attributes
= param
.GetCustomAttributes(false);
190 String requestParameterName
;
192 bool calculated
= false;
194 foreach(object attr
in attributes
)
196 IParameterBinder actionParam
= attr
as IParameterBinder
;
198 if (actionParam
== null) continue;
200 points
+= actionParam
.CalculateParamPoints(Context
, this, ControllerContext
, param
);
204 if (calculated
) continue;
210 requestParameterName
= GetRequestParameterName(param
);
212 Type parameterType
= param
.ParameterType
;
213 bool usedActionArgs
= false;
215 if ((actionArgs
!= null) && actionArgs
.ContainsKey(requestParameterName
))
217 object value = actionArgs
[requestParameterName
];
218 Type actionArgType
= value != null ? value.GetType() : param
.ParameterType
;
222 if (binder
.Converter
.CanConvert(parameterType
, actionArgType
, value, out exactMatch
))
226 usedActionArgs
= true;
230 if (!usedActionArgs
&& binder
.CanBindParameter(parameterType
, requestParameterName
, Request
.ParamsNode
))
237 // the bonus should be nice only for disambiguation.
238 // otherwise, unmatched-parameterless-actions will always have
239 // the same weight as matched-single-parameter-actions.
240 if (matchCount
== parameters
.Length
)
249 /// Returns an array that hopefully fills the arguments of the selected action.
252 /// Each parameter is inspected and we try to obtain an implementation of
253 /// <see cref="IParameterBinder"/> from the attributes the parameter have (if any).
254 /// If an implementation is found, it's used to fill the value for that parameter.
255 /// Otherwise we use simple conversion to obtain the value.
257 /// <param name="parameters">Parameters to obtain the values to</param>
258 /// <param name="request">The current request, which is the source to obtain the data</param>
259 /// <param name="actionArgs">Extra arguments to pass to the action.</param>
260 /// <returns>An array with the arguments values</returns>
261 protected virtual object[] BuildMethodArguments(ParameterInfo
[] parameters
, IRequest request
, IDictionary
<string, object> actionArgs
)
263 object[] args
= new object[parameters
.Length
];
264 String paramName
= String
.Empty
;
265 String
value = String
.Empty
;
269 for(int argIndex
= 0; argIndex
< args
.Length
; argIndex
++)
272 // If the parameter is decorated with an attribute
273 // that implements IParameterBinder, it's up to it
277 ParameterInfo param
= parameters
[argIndex
];
278 paramName
= GetRequestParameterName(param
);
280 bool handled
= false;
282 object[] attributes
= param
.GetCustomAttributes(false);
284 foreach(object attr
in attributes
)
286 IParameterBinder paramBinder
= attr
as IParameterBinder
;
288 if (paramBinder
!= null)
290 args
[argIndex
] = paramBinder
.Bind(Context
, this, ControllerContext
, param
);
297 // Otherwise we handle it
302 object convertedVal
= null;
303 bool conversionSucceeded
= false;
305 if (actionArgs
!= null && actionArgs
.ContainsKey(paramName
))
307 object actionArg
= actionArgs
[paramName
];
309 Type actionArgType
= actionArg
!= null ? actionArg
.GetType() : param
.ParameterType
;
311 convertedVal
= binder
.Converter
.Convert(param
.ParameterType
, actionArgType
, actionArg
, out conversionSucceeded
);
314 if (!conversionSucceeded
)
316 convertedVal
= binder
.BindParameter(param
.ParameterType
, paramName
, Request
.ParamsNode
);
319 args
[argIndex
] = convertedVal
;
323 catch(FormatException ex
)
325 throw new MonoRailException(
326 String
.Format("Could not convert {0} to request type. " +
327 "Argument value is '{1}'", paramName
, Params
.Get(paramName
)), ex
);
331 throw new MonoRailException(
332 String
.Format("Error building method arguments. " +
333 "Last param analyzed was {0} with value '{1}'", paramName
, value), ex
);
340 /// Binds the object of the specified type using the given prefix.
342 /// <param name="targetType">Type of the target.</param>
343 /// <param name="prefix">The prefix.</param>
344 /// <returns></returns>
345 protected object BindObject(Type targetType
, String prefix
)
347 return BindObject(ParamStore
.Params
, targetType
, prefix
);
351 /// Binds the object of the specified type using the given prefix.
352 /// but only using the entries from the collection specified on the <paramref name="from"/>
354 /// <param name="from">Restricts the data source of entries.</param>
355 /// <param name="targetType">Type of the target.</param>
356 /// <param name="prefix">The prefix.</param>
357 /// <returns></returns>
358 protected object BindObject(ParamStore
from, Type targetType
, String prefix
)
360 return BindObject(from, targetType
, prefix
, null, null);
364 /// Binds the object of the specified type using the given prefix.
365 /// but only using the entries from the collection specified on the <paramref name="from"/>
367 /// <param name="from">From.</param>
368 /// <param name="targetType">Type of the target.</param>
369 /// <param name="prefix">The prefix.</param>
370 /// <param name="excludedProperties">The excluded properties, comma separated list.</param>
371 /// <param name="allowedProperties">The allowed properties, comma separated list.</param>
372 /// <returns></returns>
373 protected object BindObject(ParamStore
from, Type targetType
, String prefix
, String excludedProperties
,
374 String allowedProperties
)
376 CompositeNode node
= Request
.ObtainParamsNode(from);
378 object instance
= binder
.BindObject(targetType
, prefix
, excludedProperties
, allowedProperties
, node
);
380 boundInstances
[instance
] = binder
.ErrorList
;
381 PopulateValidatorErrorSummary(instance
, binder
.GetValidationSummary(instance
));
387 /// Binds the object instance using the specified prefix.
389 /// <param name="instance">The instance.</param>
390 /// <param name="prefix">The prefix.</param>
391 protected void BindObjectInstance(object instance
, String prefix
)
393 BindObjectInstance(instance
, ParamStore
.Params
, prefix
);
397 /// Binds the object instance using the given prefix.
398 /// but only using the entries from the collection specified on the <paramref name="from"/>
400 /// <param name="instance">The instance.</param>
401 /// <param name="from">From.</param>
402 /// <param name="prefix">The prefix.</param>
403 protected void BindObjectInstance(object instance
, ParamStore
from, String prefix
)
405 CompositeNode node
= Request
.ObtainParamsNode(from);
407 binder
.BindObjectInstance(instance
, prefix
, node
);
409 boundInstances
[instance
] = binder
.ErrorList
;
410 PopulateValidatorErrorSummary(instance
, binder
.GetValidationSummary(instance
));
414 /// Binds the object of the specified type using the given prefix.
416 /// <typeparam name="T">Target type</typeparam>
417 /// <param name="prefix">The prefix.</param>
418 /// <returns></returns>
419 protected T BindObject
<T
>(String prefix
)
421 return (T
) BindObject(typeof(T
), prefix
);
425 /// Binds the object of the specified type using the given prefix.
426 /// but only using the entries from the collection specified on the <paramref name="from"/>
428 /// <typeparam name="T">Target type</typeparam>
429 /// <param name="from">From.</param>
430 /// <param name="prefix">The prefix.</param>
431 /// <returns></returns>
432 protected T BindObject
<T
>(ParamStore
from, String prefix
)
434 return (T
) BindObject(from, typeof(T
), prefix
);
438 /// Binds the object of the specified type using the given prefix.
439 /// but only using the entries from the collection specified on the <paramref name="from"/>
441 /// <typeparam name="T"></typeparam>
442 /// <param name="from">From.</param>
443 /// <param name="prefix">The prefix.</param>
444 /// <param name="excludedProperties">The excluded properties.</param>
445 /// <param name="allowedProperties">The allowed properties.</param>
446 /// <returns></returns>
447 protected T BindObject
<T
>(ParamStore
from, String prefix
, String excludedProperties
, String allowedProperties
)
449 return (T
) BindObject(from, typeof(T
), prefix
, excludedProperties
, allowedProperties
);