/***********************************************************
*
* Copyright 2007 Pixel Dragons Ltd. All rights reserved.
*
* http://www.codeplex.com/PixelDragonsMVC for licence, more
* information and latest source code.
*
* Check out our other stuff at http://www.pixeldragons.com
*
***********************************************************/
using System;
using System.Web;
using System.Collections.Generic;
using System.Web.SessionState;
using System.Configuration;
using System.Collections.Specialized;
using System.IO;
using System.Xml;
using System.Web.Compilation;
using System.Reflection;
using System.Web.UI;
using PixelDragons.MVC.Interfaces;
using PixelDragons.MVC.Exceptions;
using PixelDragons.MVC.Controllers;
using PixelDragons.MVC.Configuration;
using PixelDragons.MVC.Persistence;
using NHibernate;
using log4net;
namespace PixelDragons.MVC
{
public class MVCHandler : IHttpHandler, IRequiresSessionState
{
#region Fields
private static readonly ILog _logger = LogManager.GetLogger(typeof(MVCHandler));
private MVCConfigurationHandler _settings;
private Dictionary<string, Type> _controllerTypeCache = new Dictionary<string, Type>();
#endregion
#region IHttpHander
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
DateTime start = DateTime.Now;
_logger.Info("**************** NEW REQUEST ****************");
_logger.InfoFormat("Processing request '{0}'", context.Request.Path);
if (_settings == null)
{
//This is the first time this instance of the handler has run, so load the settings
LoadSettings(context);
}
//Create an nHibernate session factory builder for this request
SessionManager sessionManager = new SessionManager(_settings);
_logger.Debug("nHibernate session manager created");
TransactionManager transactionManager = new TransactionManager(sessionManager);
_logger.Debug("nHibernate transaction manager created");
try
{
//Get the controller and action name (the command) from the request
Command command = GetCommandFromContext(context);
//Setup a transaction if required (based on the action name and settings)
ITransaction transaction = transactionManager.AutoStartTransaction(_settings, command);
//Get the controller class from the configuration and set it up
IController controller = GetController(command);
controller.Context = context;
controller.Request = context.Request;
controller.Server = context.Server;
controller.Response = context.Response;
controller.Session = context.Session;
controller.Logger = LogManager.GetLogger(controller.GetType());
controller.SessionManager = sessionManager;
controller.TransactionManager = transactionManager;
controller.PersistenceManager = new PersistenceManager(sessionManager);
controller.AjaxViewPart = context.Request["ajaxViewPart"];
controller.Command = command;
try
{
//Get the correct view to show
RunActionAndGetViewToShow(controller, command, context);
}
catch (Exception ex)
{
//An unexpected error, so rollback the transaction (if PixelDragonsMVC.NET created one)
if (transaction != null)
{
transactionManager.Rollback(transaction);
_logger.Debug("Rolled back transaction");
}
//Log the error
string error = GetFullExceptionText(ex) + "\r\n\r\n";
error += ex.GetBaseException().StackTrace;
_logger.Error(error);
//throw (ex);
}
finally
{
//Either render or redirect to the selected view
if (command.View.ViewType == ViewType.Render)
{
_logger.DebugFormat("Rendering view: {0}", command.View.ViewPath);
context.Server.Execute(command.View.ViewPath);
}
else
{
_logger.DebugFormat("Redirecting to url: {0}", command.View.ViewUrl);
context.Response.Redirect(command.View.ViewUrl);
}
}
}
finally
{
//The request is now completed so close the nhibernate session
_logger.Debug("Closing nHibernate active session");
sessionManager.CloseActiveSession(transactionManager);
TimeSpan duration = new TimeSpan(DateTime.Now.Ticks - start.Ticks);
_logger.InfoFormat("*** Request '{0}' complete in {1}ms ***", context.Request.Path, duration.TotalMilliseconds);
}
}
#endregion
#region Methods
private void RunActionAndGetViewToShow(IController controller, Command command, HttpContext context)
{
//Call the actions as required and get the view to show
command.View = null;
try
{
//Allow the controller to execution some code before the action is called
_logger.DebugFormat("Calling BeforeAction() in controller: {0}", controller.GetType().ToString());
controller.BeforeAction();
//Call the action for this command
_logger.DebugFormat("Calling action in controller: {0}", controller.GetType().ToString());
CallAction(controller, command, context);
//Allow the controller to execution some code after the action has been called
_logger.DebugFormat("Calling AfterAction() in controller: {0}", controller.GetType().ToString());
controller.AfterAction();
}
catch (ActionException ex)
{
//An exception was thrown to override the view to display
_logger.DebugFormat("ActionException thrown, getting the view '{0}'", ex.ViewName);
command.View = GetView(command, ex.ViewName);
}
finally
{
//Store the model (as set by the action) for the view to access when rendering
context.Items.Add("model", controller.Model);
if (command.View == null)
{
//Get the view
_logger.Debug("Getting the correct view...");
string viewName = controller.ViewName;
if (String.IsNullOrEmpty(viewName))
{
//The controller hasn't specified a view so use the default
_logger.Debug("No view specified by the controller, using the default...");
command.View = GetDefaultView(command);
}
else
{
//The controller specified a view, so look it up in the config
_logger.DebugFormat("Getting the view '{0}' which was specified by the controller...", viewName);
command.View = GetView(command, viewName);
}
}
}
}
private string GetFullExceptionText(Exception ex)
{
string message = "";
message = ex.Message;
if (ex.InnerException != null)
{
message += "\r\n" + GetFullExceptionText(ex.InnerException);
}
return message;
}
private void LoadSettings(HttpContext context)
{
_logger.Debug("Loading MVC settings...");
_settings = (MVCConfigurationHandler)ConfigurationManager.GetSection("mvc");
if (_settings != null)
{
_settings.MappingFilePath = Path.Combine(context.Request.PhysicalApplicationPath, _settings.MappingFile);
//Load the config xml
XmlDocument xml = new XmlDocument();
xml.Load(_settings.MappingFilePath);
_settings.ConfigXml = xml;
_logger.Debug("Loaded settings successfully");
}
else
{
throw(new Exception("Unable to find MVC element in web.config"));
}
}
private Command GetCommandFromContext(HttpContext context)
{
//From the context, get the controller and action. In PixelDragons.MVC.NET,
//this is in the format: http://......./controllerName-actionName.ext
//(ext is the file extention mapped in the web.config, ashx by default).
//Parameters for the action can be passed on the querystring or posted.
//TODO: Convert this to use a regular expression defined in the config file
string path = context.Request.Path;
int start = path.LastIndexOf('/') + 1;
int end = path.LastIndexOf('.');
string commandText = path.Substring(start, end - start);
string[] controllerActionPair = commandText.Split(new char[] { '-' });
string controllerName = controllerActionPair[0];
string actionName = null;
if (controllerActionPair.Length == 2)
{
actionName = controllerActionPair[1];
}
_logger.DebugFormat("Command for request '{0}' parsed. Command Text: '{1}', Controller Name: '{2}', Action Name: '{3}'", path, commandText, controllerName, actionName);
return new Command(commandText, controllerName, actionName);
}
private IController GetController(Command command)
{
//First try to get the controller type from the cache
Type controllerType = null;
if (_controllerTypeCache.ContainsKey(command.CommandText))
{
_logger.DebugFormat("Getting controller from cache for command '{0}'", command.CommandText);
controllerType = _controllerTypeCache[command.CommandText];
}
else
{
_logger.DebugFormat("Trying to get controller from pattern '{0}'", _settings.ControllerPattern);
//Otherwise, try to get the controller from the controller pattern
string controllerClass = _settings.ControllerPattern.Replace("[ControllerName]", command.ControllerName);
controllerType = BuildManager.GetType(controllerClass, false, false);
if (controllerType == null)
{
_logger.DebugFormat("Controller class '{0}' does not exist, trying to lookup in config", controllerClass);
//Couldn't get the controller type from the controller pattern, so look up in the config
XmlElement controllerNode = (XmlElement)_settings.ConfigXml.DocumentElement.SelectSingleNode(String.Format("controllers/controller[@name='{0}']", command.ControllerName.ToLower()));
if (controllerNode != null && controllerNode.HasAttribute("class"))
{
controllerClass = controllerNode.GetAttribute("class");
controllerType = BuildManager.GetType(controllerClass, true, false);
}
}
if (controllerType == null)
{
_logger.DebugFormat("No controller available for '{0}', so using the default controller", command.CommandText);
//No controller found so use the default controller. This means that a command
//doesn't need it's own controller if there it just needs to show the default
//view.
controllerType = BuildManager.GetType(_settings.DefaultController, true, false);
}
//Cache this for next time
_controllerTypeCache.Add(command.CommandText, controllerType);
}
_logger.DebugFormat("Creating controller: {0}", controllerType.ToString());
return (IController)Activator.CreateInstance(controllerType);
}
private View GetDefaultView(Command command)
{
string viewPath;
if (command.ActionName != null)
{
_logger.DebugFormat("Trying to get view path from pattern '{0}'", _settings.ViewWithActionPattern);
viewPath = _settings.ViewWithActionPattern.Replace("[ControllerName]", command.ControllerName);
viewPath = viewPath.Replace("[ActionName]", command.ActionName);
}
else
{
_logger.DebugFormat("Trying to get view path from pattern '{0}'", _settings.ViewWithNoActionPattern);
viewPath = _settings.ViewWithNoActionPattern.Replace("[ControllerName]", command.ControllerName);
}
_logger.DebugFormat("Using view path '{0}' to render", viewPath);
return new View(viewPath, "", ViewType.Render);
}
private View GetView(Command command, string viewName)
{
_logger.DebugFormat("Getting view from the config xml for command '{0}' and view name '{1}'...", command.CommandText, viewName);
//For this command, get the view that should be displayed from the config
View view = null;
string xpath = String.Format("controllers/controller[@name='{0}']/action[@name='{1}']/view[@name='{2}']", command.OriginalControllerName, command.OriginalActionName, viewName);
XmlElement viewNode = (XmlElement)_settings.ConfigXml.DocumentElement.SelectSingleNode(xpath);
if (viewNode != null)
{
_logger.Debug("Found an explicit view for the controller/action/view, looking up path...");
view = GetViewFromConfigNode(viewNode);
}
else
{
//There is no explicit view for this action, so check the shared area
_logger.Debug("No explicit view for the controller/action/view, looking in shared area...");
viewNode = (XmlElement)_settings.ConfigXml.DocumentElement.SelectSingleNode(String.Format("shared/view[@name='{0}']", viewName));
if (viewNode != null)
{
_logger.Debug("Found an explicit view in the shared area, looking up path...");
view = GetViewFromConfigNode(viewNode);
}
}
if (view == null)
{
//Unable to find a view in the config so show the default
_logger.Debug("Unable to find a view in the config so show the default");
view = GetDefaultView(command);
}
return view;
}
private View GetViewFromConfigNode(XmlElement viewNode)
{
View view = new View();
view.ViewType = (ViewType)Enum.Parse(typeof(ViewType), viewNode.GetAttribute("type"), true);
view.ViewUrl = viewNode.GetAttribute("url");
string viewRef = viewNode.GetAttribute("ref");
XmlElement viewPathNode = (XmlElement)_settings.ConfigXml.DocumentElement.SelectSingleNode(String.Format("views/view[@id='{0}']", viewRef));
if (viewPathNode != null)
{
view.ViewPath = viewPathNode.GetAttribute("path");
}
_logger.DebugFormat("Found view in config ViewType: '{0}', ViewPath: '{1}', ViewUrl: '{2}'", view.ViewType.ToString(), view.ViewPath, view.ViewUrl);
return view;
}
private void CallAction(IController controller, Command command, HttpContext context)
{
if (command.ActionName == null)
{
//There was no action name so call the default action
_logger.DebugFormat("No action name, so calling DefaultAction() in controller: {0}", controller.GetType().ToString());
controller.DefaultAction();
}
else
{
//Get the action method using reflection
Type controllerType = controller.GetType();
MethodInfo method = controllerType.GetMethod(command.ActionName);
if (method != null)
{
_logger.DebugFormat("Found action method '{0}' in controller: {1}, converting parameters...", command.ActionName, controller.GetType().ToString());
//Make up the list of parameters
ParameterInfo[] methodParams = method.GetParameters();
List<object> paramList = new List<object>();
foreach (ParameterInfo param in methodParams)
{
//Convert request param to correct type
object convertedValue = null;
if (param.ParameterType == typeof(HttpPostedFile))
{
//Get the posted file from the files collection
convertedValue = context.Request.Files[param.Name];
}
else if (param.ParameterType == typeof(Guid))
{
//Get the posted file from the files collection
string valueAsString = context.Request[param.Name];
if (valueAsString == null)
{
convertedValue = Guid.Empty;
}
else
{
convertedValue = new Guid(valueAsString);
}
}
else
{
//Convert value to correct type
string valueAsString = context.Request[param.Name];
if (param.ParameterType.IsArray)
{
//This is an array
if (valueAsString.Length > 0)
{
string[] array = valueAsString.Split(',');
convertedValue = ArrayConverter.ConvertStringArray(array, param.ParameterType);
}
}
else
{
//Normal type (not an array)
try
{
convertedValue = Convert.ChangeType(valueAsString, param.ParameterType);
}
catch(Exception)
{
convertedValue = null;
}
}
}
if (convertedValue == null)
{
//The converted value is null, create a new instance of this type
//to ensure we are using the defaults for value types.
if (param.ParameterType.IsValueType)
{
convertedValue = Activator.CreateInstance(param.ParameterType);
}
}
//Add converted value to param list
paramList.Add(convertedValue);
}
//Call the action with parameters
_logger.DebugFormat("Calling action method '{0}' in controller: {1}", command.ActionName, controller.GetType().ToString());
method.Invoke(controller, paramList.ToArray());
}
else
{
//There is no action by that name, so call the default
_logger.DebugFormat("No action method by the name '{0}', so calling DefaultAction() in controller: {1}", command.ActionName, controller.GetType().ToString());
controller.DefaultAction();
}
}
}
#endregion
}
}