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
;
25 /// Specialization of <see cref="Controller"/> that tries
26 /// to match the request params to method arguments.
29 /// You don't even need to always use databinding within
30 /// arguments. <see cref="BindObject(ParamStore,Type,string)"/>
31 /// and <see cref="BindObjectInstance(object,string)"/>
32 /// provides the same functionality to be used in place.
34 public abstract class SmartDispatcherController
: Controller
36 private IDataBinder binder
;
39 /// Represents the errors associated with an instance bound.
41 protected IDictionary
<object, ErrorList
> boundInstances
= new Dictionary
<object, ErrorList
>();
44 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
46 protected SmartDispatcherController() : this(new DataBinder())
51 /// Initializes a new instance of the <see cref="SmartDispatcherController"/> class.
53 /// <param name="binder">The binder.</param>
54 protected SmartDispatcherController(IDataBinder binder
)
62 /// <value>The binder.</value>
63 public IDataBinder Binder
65 get { return binder; }
69 /// Constructs the parameters for the action and invokes it.
71 /// <param name="method">The method.</param>
72 /// <param name="request">The request.</param>
73 /// <param name="extraArgs">The extra args.</param>
74 /// <returns></returns>
75 protected override object InvokeMethod(MethodInfo method
, IRequest request
, IDictionary
<string, object> extraArgs
)
77 ParameterInfo
[] parameters
= method
.GetParameters();
79 object[] methodArgs
= BuildMethodArguments(parameters
, request
, extraArgs
);
81 return method
.Invoke(this, methodArgs
);
85 /// Uses a simple heuristic to select the best method -- especially in the
86 /// case of method overloads.
88 /// <param name="action">The action name</param>
89 /// <param name="actions">The avaliable actions</param>
90 /// <param name="request">The request instance</param>
91 /// <param name="actionArgs">The custom arguments for the action</param>
92 /// <returns></returns>
93 protected override MethodInfo
SelectMethod(string action
, IDictionary actions
, IRequest request
, IDictionary
<string, object> actionArgs
)
95 object methods
= actions
[action
];
97 // should check for single-option as soon as possible (performance improvement)
98 if (methods
is MethodInfo
) return (MethodInfo
) methods
;
100 ArrayList candidates
= (ArrayList
) methods
;
102 if (candidates
== null) return null;
104 return SelectBestCandidate((MethodInfo
[]) candidates
.ToArray(typeof(MethodInfo
)),
105 request
.Params
, actionArgs
);
109 /// Selects the best method given the set of entries
110 /// avaliable on <paramref name="webParams"/> and <paramref name="actionArgs"/>
112 /// <param name="candidates">The candidates.</param>
113 /// <param name="webParams">The web params.</param>
114 /// <param name="actionArgs">The custom action args.</param>
115 /// <returns></returns>
116 protected virtual MethodInfo
SelectBestCandidate(MethodInfo
[] candidates
,
117 NameValueCollection webParams
,
118 IDictionary
<string, object> actionArgs
)
120 if (candidates
.Length
== 1)
122 // There's nothing much to do in this situation
123 return candidates
[0];
126 int lastMaxPoints
= int.MinValue
;
127 MethodInfo bestCandidate
= null;
129 foreach(MethodInfo candidate
in candidates
)
131 int points
= CalculatePoints(candidate
, webParams
, actionArgs
);
133 if (lastMaxPoints
< points
)
135 lastMaxPoints
= points
;
136 bestCandidate
= candidate
;
140 return bestCandidate
;
144 /// Gets the name of the request parameter.
146 /// <param name="param">The param.</param>
147 /// <returns></returns>
148 protected virtual String
GetRequestParameterName(ParameterInfo param
)
154 /// Uses a simplest algorithm to compute points for a method
155 /// based on parameters available, which in turn reflects
156 /// the best method is the one which the parameters will be
157 /// able to satistfy more arguments
159 /// <param name="candidate">The method candidate</param>
160 /// <param name="webParams">Parameter source</param>
161 /// <param name="actionArgs">Extra parameters</param>
162 /// <returns></returns>
163 protected int CalculatePoints(MethodInfo candidate
, NameValueCollection webParams
, IDictionary
<string, object> actionArgs
)
168 ParameterInfo
[] parameters
= candidate
.GetParameters();
170 foreach(ParameterInfo param
in parameters
)
173 // If the param is decorated with an attribute that implements IParameterBinder
174 // then it calculates the points itself
177 object[] attributes
= param
.GetCustomAttributes(false);
179 String requestParameterName
;
181 bool calculated
= false;
183 foreach(object attr
in attributes
)
185 IParameterBinder actionParam
= attr
as IParameterBinder
;
187 if (actionParam
== null) continue;
189 points
+= actionParam
.CalculateParamPoints(Context
, this, ControllerContext
, param
);
193 if (calculated
) continue;
199 requestParameterName
= GetRequestParameterName(param
);
201 Type parameterType
= param
.ParameterType
;
202 bool usedActionArgs
= false;
204 if ((actionArgs
!= null) && actionArgs
.ContainsKey(requestParameterName
))
206 object value = actionArgs
[requestParameterName
];
207 Type actionArgType
= value != null ? value.GetType() : param
.ParameterType
;
211 if (binder
.Converter
.CanConvert(parameterType
, actionArgType
, value, out exactMatch
))
215 usedActionArgs
= true;
219 if (!usedActionArgs
&& binder
.CanBindParameter(parameterType
, requestParameterName
, Request
.ParamsNode
))
226 // the bonus should be nice only for disambiguation.
227 // otherwise, unmatched-parameterless-actions will always have
228 // the same weight as matched-single-parameter-actions.
229 if (matchCount
== parameters
.Length
)
238 /// Returns an array that hopefully fills the arguments of the selected action.
241 /// Each parameter is inspected and we try to obtain an implementation of
242 /// <see cref="IParameterBinder"/> from the attributes the parameter have (if any).
243 /// If an implementation is found, it's used to fill the value for that parameter.
244 /// Otherwise we use simple conversion to obtain the value.
246 /// <param name="parameters">Parameters to obtain the values to</param>
247 /// <param name="request">The current request, which is the source to obtain the data</param>
248 /// <param name="actionArgs">Extra arguments to pass to the action.</param>
249 /// <returns>An array with the arguments values</returns>
250 protected virtual object[] BuildMethodArguments(ParameterInfo
[] parameters
, IRequest request
, IDictionary
<string, object> actionArgs
)
252 object[] args
= new object[parameters
.Length
];
253 String paramName
= String
.Empty
;
254 String
value = String
.Empty
;
258 for(int argIndex
= 0; argIndex
< args
.Length
; argIndex
++)
261 // If the parameter is decorated with an attribute
262 // that implements IParameterBinder, it's up to it
266 ParameterInfo param
= parameters
[argIndex
];
267 paramName
= GetRequestParameterName(param
);
269 bool handled
= false;
271 object[] attributes
= param
.GetCustomAttributes(false);
273 foreach(object attr
in attributes
)
275 IParameterBinder paramBinder
= attr
as IParameterBinder
;
277 if (paramBinder
!= null)
279 args
[argIndex
] = paramBinder
.Bind(Context
, this, ControllerContext
, param
);
286 // Otherwise we handle it
291 object convertedVal
= null;
292 bool conversionSucceeded
= false;
294 if (actionArgs
!= null && actionArgs
.ContainsKey(paramName
))
296 object actionArg
= actionArgs
[paramName
];
298 Type actionArgType
= actionArg
!= null ? actionArg
.GetType() : param
.ParameterType
;
300 convertedVal
= binder
.Converter
.Convert(param
.ParameterType
, actionArgType
, actionArg
, out conversionSucceeded
);
303 if (!conversionSucceeded
)
305 convertedVal
= binder
.BindParameter(param
.ParameterType
, paramName
, Request
.ParamsNode
);
308 args
[argIndex
] = convertedVal
;
312 catch(FormatException ex
)
314 throw new MonoRailException(
315 String
.Format("Could not convert {0} to request type. " +
316 "Argument value is '{1}'", paramName
, Params
.Get(paramName
)), ex
);
320 throw new MonoRailException(
321 String
.Format("Error building method arguments. " +
322 "Last param analyzed was {0} with value '{1}'", paramName
, value), ex
);
329 /// Binds the object of the specified type using the given prefix.
331 /// <param name="targetType">Type of the target.</param>
332 /// <param name="prefix">The prefix.</param>
333 /// <returns></returns>
334 protected object BindObject(Type targetType
, String prefix
)
336 return BindObject(ParamStore
.Params
, targetType
, prefix
);
340 /// Binds the object of the specified type using the given prefix.
341 /// but only using the entries from the collection specified on the <paramref name="from"/>
343 /// <param name="from">Restricts the data source of entries.</param>
344 /// <param name="targetType">Type of the target.</param>
345 /// <param name="prefix">The prefix.</param>
346 /// <returns></returns>
347 protected object BindObject(ParamStore
from, Type targetType
, String prefix
)
349 return BindObject(from, targetType
, prefix
, null, null);
353 /// Binds the object of the specified type using the given prefix.
354 /// but only using the entries from the collection specified on the <paramref name="from"/>
356 /// <param name="from">From.</param>
357 /// <param name="targetType">Type of the target.</param>
358 /// <param name="prefix">The prefix.</param>
359 /// <param name="excludedProperties">The excluded properties, comma separated list.</param>
360 /// <param name="allowedProperties">The allowed properties, comma separated list.</param>
361 /// <returns></returns>
362 protected object BindObject(ParamStore
from, Type targetType
, String prefix
, String excludedProperties
,
363 String allowedProperties
)
365 CompositeNode node
= Request
.ObtainParamsNode(from);
367 object instance
= binder
.BindObject(targetType
, prefix
, excludedProperties
, allowedProperties
, node
);
369 boundInstances
[instance
] = binder
.ErrorList
;
370 PopulateValidatorErrorSummary(instance
, binder
.GetValidationSummary(instance
));
376 /// Binds the object instance using the specified prefix.
378 /// <param name="instance">The instance.</param>
379 /// <param name="prefix">The prefix.</param>
380 protected void BindObjectInstance(object instance
, String prefix
)
382 BindObjectInstance(instance
, ParamStore
.Params
, prefix
);
386 /// Binds the object instance using the given prefix.
387 /// but only using the entries from the collection specified on the <paramref name="from"/>
389 /// <param name="instance">The instance.</param>
390 /// <param name="from">From.</param>
391 /// <param name="prefix">The prefix.</param>
392 protected void BindObjectInstance(object instance
, ParamStore
from, String prefix
)
394 CompositeNode node
= Request
.ObtainParamsNode(from);
396 binder
.BindObjectInstance(instance
, prefix
, node
);
398 boundInstances
[instance
] = binder
.ErrorList
;
399 PopulateValidatorErrorSummary(instance
, binder
.GetValidationSummary(instance
));
403 /// Binds the object of the specified type using the given prefix.
405 /// <typeparam name="T">Target type</typeparam>
406 /// <param name="prefix">The prefix.</param>
407 /// <returns></returns>
408 protected T BindObject
<T
>(String prefix
)
410 return (T
) BindObject(typeof(T
), prefix
);
414 /// Binds the object of the specified type using the given prefix.
415 /// but only using the entries from the collection specified on the <paramref name="from"/>
417 /// <typeparam name="T">Target type</typeparam>
418 /// <param name="from">From.</param>
419 /// <param name="prefix">The prefix.</param>
420 /// <returns></returns>
421 protected T BindObject
<T
>(ParamStore
from, String prefix
)
423 return (T
) BindObject(from, typeof(T
), prefix
);
427 /// Binds the object of the specified type using the given prefix.
428 /// but only using the entries from the collection specified on the <paramref name="from"/>
430 /// <typeparam name="T"></typeparam>
431 /// <param name="from">From.</param>
432 /// <param name="prefix">The prefix.</param>
433 /// <param name="excludedProperties">The excluded properties.</param>
434 /// <param name="allowedProperties">The allowed properties.</param>
435 /// <returns></returns>
436 protected T BindObject
<T
>(ParamStore
from, String prefix
, String excludedProperties
, String allowedProperties
)
438 return (T
) BindObject(from, typeof(T
), prefix
, excludedProperties
, allowedProperties
);