// Copyright 2004-2008 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Castle.MonoRail.Framework { using System; using System.Collections.Generic; using System.Reflection; using System.Collections; using System.Collections.Specialized; using Castle.Components.Binder; using Providers; /// /// Specialization of that tries /// to match the request params to method arguments. /// /// /// You don't even need to always use databinding within /// arguments. /// and /// provides the same functionality to be used in place. /// public abstract class SmartDispatcherController : Controller { private IDataBinder binder; /// /// Represents the errors associated with an instance bound. /// protected IDictionary boundInstances = new Dictionary(); /// /// Initializes a new instance of the class. /// protected SmartDispatcherController() : this(new DataBinder()) { } /// /// Initializes a new instance of the class. /// /// The binder. protected SmartDispatcherController(IDataBinder binder) { this.binder = binder; } /// /// Gets the binder. /// /// The binder. public IDataBinder Binder { get { return binder; } } /// /// Constructs the parameters for the action and invokes it. /// /// The method. /// The request. /// The extra args. /// protected override object InvokeMethod(MethodInfo method, IRequest request, IDictionary extraArgs) { ParameterInfo[] parameters = method.GetParameters(); object[] methodArgs = BuildMethodArguments(parameters, request, extraArgs); return method.Invoke(this, methodArgs); } /// /// Uses a simple heuristic to select the best method -- especially in the /// case of method overloads. /// /// The action name /// The avaliable actions /// The request instance /// The custom arguments for the action /// Type of the action. /// protected override MethodInfo SelectMethod(string action, IDictionary actions, IRequest request, IDictionary actionArgs, ActionType actionType) { object methods = actions[action]; // should check for single-option as soon as possible (performance improvement) if (methods is MethodInfo) return (MethodInfo) methods; if (methods is AsyncActionPair) { AsyncActionPair pair = (AsyncActionPair) methods; if (actionType == ActionType.AsyncBegin) return pair.BeginActionInfo; else return pair.EndActionInfo; } ArrayList candidates = (ArrayList) methods; if (candidates == null) return null; return SelectBestCandidate((MethodInfo[]) candidates.ToArray(typeof(MethodInfo)), request.Params, actionArgs); } /// /// Selects the best method given the set of entries /// avaliable on and /// /// The candidates. /// The web params. /// The custom action args. /// protected virtual MethodInfo SelectBestCandidate(MethodInfo[] candidates, NameValueCollection webParams, IDictionary actionArgs) { if (candidates.Length == 1) { // There's nothing much to do in this situation return candidates[0]; } int lastMaxPoints = int.MinValue; MethodInfo bestCandidate = null; foreach(MethodInfo candidate in candidates) { int points = CalculatePoints(candidate, webParams, actionArgs); if (lastMaxPoints < points) { lastMaxPoints = points; bestCandidate = candidate; } } return bestCandidate; } /// /// Gets the name of the request parameter. /// /// The param. /// protected virtual String GetRequestParameterName(ParameterInfo param) { return param.Name; } /// /// Uses a simplest algorithm to compute points for a method /// based on parameters available, which in turn reflects /// the best method is the one which the parameters will be /// able to satistfy more arguments /// /// The method candidate /// Parameter source /// Extra parameters /// protected int CalculatePoints(MethodInfo candidate, NameValueCollection webParams, IDictionary actionArgs) { int points = 0; int matchCount = 0; ParameterInfo[] parameters = candidate.GetParameters(); foreach(ParameterInfo param in parameters) { // // If the param is decorated with an attribute that implements IParameterBinder // then it calculates the points itself // object[] attributes = param.GetCustomAttributes(false); String requestParameterName; bool calculated = false; foreach(object attr in attributes) { IParameterBinder actionParam = attr as IParameterBinder; if (actionParam == null) continue; points += actionParam.CalculateParamPoints(Context, this, ControllerContext, param); calculated = true; } if (calculated) continue; // // Otherwise // requestParameterName = GetRequestParameterName(param); Type parameterType = param.ParameterType; bool usedActionArgs = false; if ((actionArgs != null) && actionArgs.ContainsKey(requestParameterName)) { object value = actionArgs[requestParameterName]; Type actionArgType = value != null ? value.GetType() : param.ParameterType; bool exactMatch; if (binder.Converter.CanConvert(parameterType, actionArgType, value, out exactMatch)) { points += 10; matchCount++; usedActionArgs = true; } } if (!usedActionArgs && binder.CanBindParameter(parameterType, requestParameterName, Request.ParamsNode)) { points += 10; matchCount++; } } // the bonus should be nice only for disambiguation. // otherwise, unmatched-parameterless-actions will always have // the same weight as matched-single-parameter-actions. if (matchCount == parameters.Length) { points += 5; } return points; } /// /// Returns an array that hopefully fills the arguments of the selected action. /// /// /// Each parameter is inspected and we try to obtain an implementation of /// from the attributes the parameter have (if any). /// If an implementation is found, it's used to fill the value for that parameter. /// Otherwise we use simple conversion to obtain the value. /// /// Parameters to obtain the values to /// The current request, which is the source to obtain the data /// Extra arguments to pass to the action. /// An array with the arguments values protected virtual object[] BuildMethodArguments(ParameterInfo[] parameters, IRequest request, IDictionary actionArgs) { object[] args = new object[parameters.Length]; String paramName = String.Empty; String value = String.Empty; try { for(int argIndex = 0; argIndex < args.Length; argIndex++) { // // If the parameter is decorated with an attribute // that implements IParameterBinder, it's up to it // to convert itself // ParameterInfo param = parameters[argIndex]; paramName = GetRequestParameterName(param); bool handled = false; object[] attributes = param.GetCustomAttributes(false); foreach(object attr in attributes) { IParameterBinder paramBinder = attr as IParameterBinder; if (paramBinder != null) { args[argIndex] = paramBinder.Bind(Context, this, ControllerContext, param); handled = true; break; } } // // Otherwise we handle it // if (!handled) { object convertedVal= null; bool conversionSucceeded = false; if (actionArgs != null && actionArgs.ContainsKey(paramName)) { object actionArg = actionArgs[paramName]; Type actionArgType = actionArg != null ? actionArg.GetType() : param.ParameterType; convertedVal = binder.Converter.Convert(param.ParameterType, actionArgType, actionArg, out conversionSucceeded); } if (!conversionSucceeded) { convertedVal = binder.BindParameter(param.ParameterType, paramName, Request.ParamsNode); } args[argIndex] = convertedVal; } } } catch(FormatException ex) { throw new MonoRailException( String.Format("Could not convert {0} to request type. " + "Argument value is '{1}'", paramName, Params.Get(paramName)), ex); } catch(Exception ex) { throw new MonoRailException( String.Format("Error building method arguments. " + "Last param analyzed was {0} with value '{1}'", paramName, value), ex); } return args; } /// /// Binds the object of the specified type using the given prefix. /// /// Type of the target. /// The prefix. /// protected object BindObject(Type targetType, String prefix) { return BindObject(ParamStore.Params, targetType, prefix); } /// /// Binds the object of the specified type using the given prefix. /// but only using the entries from the collection specified on the /// /// Restricts the data source of entries. /// Type of the target. /// The prefix. /// protected object BindObject(ParamStore from, Type targetType, String prefix) { return BindObject(from, targetType, prefix, null, null); } /// /// Binds the object of the specified type using the given prefix. /// but only using the entries from the collection specified on the /// /// From. /// Type of the target. /// The prefix. /// The excluded properties, comma separated list. /// The allowed properties, comma separated list. /// protected object BindObject(ParamStore from, Type targetType, String prefix, String excludedProperties, String allowedProperties) { CompositeNode node = Request.ObtainParamsNode(from); object instance = binder.BindObject(targetType, prefix, excludedProperties, allowedProperties, node); boundInstances[instance] = binder.ErrorList; PopulateValidatorErrorSummary(instance, binder.GetValidationSummary(instance)); return instance; } /// /// Binds the object instance using the specified prefix. /// /// The instance. /// The prefix. protected void BindObjectInstance(object instance, String prefix) { BindObjectInstance(instance, ParamStore.Params, prefix); } /// /// Binds the object instance using the given prefix. /// but only using the entries from the collection specified on the /// /// The instance. /// From. /// The prefix. protected void BindObjectInstance(object instance, ParamStore from, String prefix) { CompositeNode node = Request.ObtainParamsNode(from); binder.BindObjectInstance(instance, prefix, node); boundInstances[instance] = binder.ErrorList; PopulateValidatorErrorSummary(instance, binder.GetValidationSummary(instance)); } /// /// Binds the object of the specified type using the given prefix. /// /// Target type /// The prefix. /// protected T BindObject(String prefix) { return (T) BindObject(typeof(T), prefix); } /// /// Binds the object of the specified type using the given prefix. /// but only using the entries from the collection specified on the /// /// Target type /// From. /// The prefix. /// protected T BindObject(ParamStore from, String prefix) { return (T) BindObject(from, typeof(T), prefix); } /// /// Binds the object of the specified type using the given prefix. /// but only using the entries from the collection specified on the /// /// /// From. /// The prefix. /// The excluded properties. /// The allowed properties. /// protected T BindObject(ParamStore from, String prefix, String excludedProperties, String allowedProperties) { return (T) BindObject(from, typeof(T), prefix, excludedProperties, allowedProperties); } } }