// 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);
}
}
}