#region License
/**
* Ingenious MVC : An MVC framework for .NET 2.0
* Copyright (C) 2006, JDP Group
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Authors: - Kent Boogaart (kentcb@internode.on.net)
* - James Inge
*/
#endregion
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using Ingenious.Mvc.Factories;
using Ingenious.Mvc.Util;
namespace Ingenious.Mvc.Configuration.Configurators{
/// <include file='XmlParser.doc.xml' path='/doc/member[@name="T:XmlParser"]/*'/>
internal static class XmlParser
{
private const string IdAttribute = "id";
private const string TypeAttribute = "type";
private const string EnumerationAttribute = "enumeration";
private const string StartingViewAttribute = "startingView";
private const string ViewManagerAttribute = "viewManager";
private const string ControllerAttribute = "controller";
private static readonly Log _log = Log.CreateForNamespace(typeof(XmlParser));
/// <include file='XmlParser.doc.xml' path='/doc/member[@name="M:Parse(System.Xml.XmlNode)"]/*'/>
public static ConfigurationInfo Parse(XmlNode xml)
{
Debug.Assert(xml != null);
_log.Information("Loading XSD schemas.");
//first, load the main schema
XmlSchema mainSchema = XmlSchema.Read(Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(XmlParser), "MVC.xsd"), null);
_log.Verbose("Loaded main configuration schema.");
//get all elements with a type attribute
XmlNodeList typeNodes = xml.SelectNodes("//*[@type]");
//create a schema set for all schemas
XmlSchemaSet schemas = new XmlSchemaSet();
schemas.Add(mainSchema);
//track the IDs of all schemas added to the schema set
IList<string> schemaIds = new List<string>();
//load all sub-schemas
foreach (XmlNode typeNode in typeNodes)
{
XmlAttribute typeAttribute = typeNode.Attributes[TypeAttribute];
_log.Verbose("Attempting to find type '{0}'.", typeAttribute.Value);
Type type = Type.GetType(typeAttribute.Value);
if (type == null)
{
//try and get as much useful information into the exception message as possible. The ID would be most useful if present
XmlAttribute idAttribute = typeNode.Attributes["id"];
if (idAttribute == null)
{
_log.Verbose("Type '{0}' could not be loaded.", typeAttribute.Value);
ExceptionHelper.Throw("Parse.typeNotFound", typeAttribute.Value, typeNode.Name);
}
else
{
_log.Verbose("Type '{0}' could not be loaded for element with ID '{1}'.", typeAttribute.Value, idAttribute.Value);
ExceptionHelper.Throw("Parse.typeNotFoundWithId", typeAttribute.Value, typeNode.Name, idAttribute.Value);
}
}
object[] attributes = type.GetCustomAttributes(typeof(XmlCustomParserAttribute), true);
if (attributes.Length > 0)
{
IXmlCustomParser parser = ((XmlCustomParserAttribute) attributes[0]).GetXmlCustomParser();
_log.Verbose("Type '{0}' has a custom parser of type '{1}'.", type.FullName, parser.GetType().FullName);
XmlSchema subSchema = parser.Schema;
Debug.Assert(subSchema != null, "Parser returned null from Schema property.");
//schemas must be assigned an ID
ExceptionHelper.ThrowIf(subSchema.Id == null, "FindSubSchemas.schemaIdNotSupplied", type.FullName);
//note we only add the schema once
if (!schemaIds.Contains(subSchema.Id))
{
_log.Verbose("Schema with ID '{0}' is not yet in the schema set - adding it.", subSchema.Id);
schemas.Add(subSchema);
schemaIds.Add(subSchema.Id);
}
else
{
_log.Verbose("Schema with ID '{0}' is already in the schema set - ignoring it.", subSchema.Id);
}
//need to associate any type name with the node
if (parser.TypeName != null)
{
XmlAttribute xsiTypeAttribute = typeNode.OwnerDocument.CreateAttribute("type", "http://www.w3.org/2001/XMLSchema-instance");
xsiTypeAttribute.Value = parser.TypeName;
typeNode.Attributes.Append(xsiTypeAttribute);
}
}
}
//we have all schemas so compile them
_log.Verbose("Compiling set of {0} schemas.", schemas.Count);
schemas.ValidationEventHandler += new ValidationEventHandler(CompileErrorHandler);
schemas.Compile();
_log.Information("Validating configuration.");
//create the validator
XmlReader xmlReader = null;
ValidateHelper validateHelper = new ValidateHelper();
try
{
XmlReaderSettings readerSettings = new XmlReaderSettings();
readerSettings.ValidationType = ValidationType.Schema;
//add schemas to the validator
readerSettings.Schemas.Add(schemas);
//listen for validation problems
readerSettings.ValidationEventHandler += new ValidationEventHandler(validateHelper.ValidationHandler);
//create the reader
xmlReader = XmlReader.Create(new XmlTextReader(new StringReader(xml.OuterXml)), readerSettings);
//this validates the XML document
while (xmlReader.Read()) ;
}
catch (Exception ex)
{
_log.Exception(ex);
ExceptionHelper.Throw("Parse.problemValidating", ex);
}
finally
{
if (xmlReader != null)
{
xmlReader.Close();
}
}
//we don't use ThrowIf here to make things a bit more efficient for the common case (no errors)
if (validateHelper.HasErrors)
{
_log.Error("Validation errors were detected: {0}", validateHelper.ErrorMessage);
ExceptionHelper.Throw("Parse.errorsDetected", Environment.NewLine, validateHelper.ErrorMessage);
}
_log.Information("Parsing configuration.");
//now we have validated the document, time to parse it for the info we need
ConfigurationInfo retVal = new ConfigurationInfo();
//parse any factories
Type factoryType;
object customConfiguration;
//controller factory
if (ParseFactoryType(retVal, xml, "factories/controllerFactory", typeof(IControllerFactory), out factoryType, out customConfiguration))
{
retVal.ControllerFactory = (IControllerFactory) FactoryFactory.Instance.Create(factoryType, customConfiguration, typeof(IControllerFactory));
}
//view factory
if (ParseFactoryType(retVal, xml, "factories/viewFactory", typeof(IViewFactory), out factoryType, out customConfiguration))
{
retVal.ViewFactory = (IViewFactory) FactoryFactory.Instance.Create(factoryType, customConfiguration, typeof(IViewFactory));
}
//view manager factory
if (ParseFactoryType(retVal, xml, "factories/viewManagerFactory", typeof(IViewManagerFactory), out factoryType, out customConfiguration))
{
retVal.ViewManagerFactory = (IViewManagerFactory) FactoryFactory.Instance.Create(factoryType, customConfiguration, typeof(IViewManagerFactory));
}
//navigator factory
if (ParseFactoryType(retVal, xml, "factories/navigatorFactory", typeof(INavigatorFactory), out factoryType, out customConfiguration))
{
retVal.NavigatorFactory = (INavigatorFactory) FactoryFactory.Instance.Create(factoryType, customConfiguration, typeof(INavigatorFactory));
}
//task factory
if (ParseFactoryType(retVal, xml, "factories/taskFactory", typeof(ITaskFactory), out factoryType, out customConfiguration))
{
retVal.TaskFactory = (ITaskFactory) FactoryFactory.Instance.Create(factoryType, customConfiguration, typeof(ITaskFactory));
}
//parse the controller infos
InfoCollection<Id<IController>, ControllerInfo> controllerInfos = new InfoCollection<Id<IController>, ControllerInfo>();
ParseControllers(retVal, controllerInfos, xml);
//parse the view manager infos
InfoCollection<Id<IViewManager>, ViewManagerInfo> viewManagerInfos = new InfoCollection<Id<IViewManager>, ViewManagerInfo>();
ParseViewManagers(retVal, viewManagerInfos, xml);
//parse the view infos
ParseViews(retVal, retVal.ViewInfos, xml, controllerInfos);
//parse the navigator infos
ParseNavigators(retVal, retVal.NavigatorInfos, xml, retVal.ViewInfos, viewManagerInfos);
return retVal;
}
private static void CompileErrorHandler(object sender, ValidationEventArgs e)
{
_log.Error("Compile error: {0}", e.Message);
_log.Exception(e.Exception);
ExceptionHelper.Throw("CompileErrorHandler.compileError", e.Exception, e.Message);
}
private static bool ParseFactoryType(ConfigurationInfo configurationInfo, XmlNode xml, string xpath, Type factoryInterfaceType, out Type factoryType, out object customConfiguration)
{
Debug.Assert(xml != null);
Debug.Assert(xpath != null);
Debug.Assert(factoryInterfaceType != null);
_log.Verbose("Parsing factory of type '{0}' for XPath '{1}'.", factoryInterfaceType.FullName, xpath);
XmlNode node = xml.SelectSingleNode(xpath);
if (node != null)
{
XmlAttribute typeAttribute = node.Attributes[TypeAttribute];
_log.Verbose("Type '{0}' is configured as a factory of type '{1}'.", typeAttribute.Value, factoryInterfaceType.FullName);
factoryType = Type.GetType(typeAttribute.Value);
Debug.Assert(factoryType != null, "All types have already been loaded in an earlier phase - was expecting type to load.");
ExceptionHelper.ThrowIf(!factoryInterfaceType.IsAssignableFrom(factoryType), "ParseFactory.typeDoesNotImplementInterface", factoryType.FullName, factoryInterfaceType.FullName);
ExceptionHelper.ThrowIf(factoryType.GetConstructor(Type.EmptyTypes) == null, "ParseFactory.typeDoesNotHaveDefaultConstructor", factoryType.FullName);
//parse any custom config for the factory
customConfiguration = ParseCustomConfiguration(factoryType, configurationInfo, null, node);
_log.Verbose("Type '{0}' successfully configured as a factory of type '{1}'.", factoryType.FullName, factoryInterfaceType.FullName);
return true;
}
else
{
_log.Verbose("No configuration was found for factory of type '{0}' - using the default factory.", factoryInterfaceType.FullName);
factoryType = null;
customConfiguration = null;
return false;
}
}
private static void ParseControllers(ConfigurationInfo configurationInfo, InfoCollection<Id<IController>, ControllerInfo> controllerInfos, XmlNode xml)
{
Debug.Assert(configurationInfo != null);
Debug.Assert(controllerInfos != null);
Debug.Assert(xml != null);
_log.Information("Parsing controller configuration.");
XmlNode controllersNode = xml.SelectSingleNode("controllers");
//controllers are optional
if (controllersNode != null)
{
XmlAttribute enumerationAttribute = controllersNode.Attributes[EnumerationAttribute];
XmlNodeList nodeList = controllersNode.SelectNodes("controller");
Type enumerationType = null;
if (enumerationAttribute != null)
{
enumerationType = XmlHelper.GetEnumeration(enumerationAttribute.Value);
}
foreach (XmlNode node in nodeList)
{
XmlAttribute idAttribute = node.Attributes[IdAttribute];
XmlAttribute typeAttribute = node.Attributes[TypeAttribute];
//if an enumeration is specified to identify controllers then make sure the ID is a valid enumeration member
if (enumerationType != null)
{
XmlHelper.ValidateEnumerationMember(enumerationType, idAttribute.Value);
}
//create the controller info
ControllerInfo controllerInfo = new ControllerInfo(idAttribute.Value);
controllerInfo.Type = Type.GetType(typeAttribute.Value);
Debug.Assert(controllerInfo.Type != null);
_log.Verbose("Adding ControllerInfo with ID '{0}', type '{1}'.", controllerInfo.Id, controllerInfo.Type.FullName);
controllerInfos.Add(controllerInfo);
//parse any custom configuration for the controller
controllerInfo.CustomConfiguration = ParseCustomConfiguration(controllerInfo.Type, configurationInfo, controllerInfo, node);
}
}
else
{
_log.Verbose("No controller configuration found.");
}
}
private static void ParseNavigators(ConfigurationInfo configurationInfo, InfoCollection<Id<INavigator>, NavigatorInfo> navigatorInfos, XmlNode xml, InfoCollection<Id<IView>, ViewInfo> viewInfos, InfoCollection<Id<IViewManager>, ViewManagerInfo> viewManagerInfos)
{
Debug.Assert(configurationInfo != null);
Debug.Assert(navigatorInfos != null);
Debug.Assert(xml != null);
Debug.Assert(viewInfos != null);
Debug.Assert(viewManagerInfos != null);
_log.Information("Parsing navigator configuration.");
XmlNode navigatorsNode = xml.SelectSingleNode("navigators");
XmlAttribute enumerationAttribute = navigatorsNode.Attributes[EnumerationAttribute];
XmlNodeList nodeList = navigatorsNode.SelectNodes("navigator");
Type enumerationType = null;
if (enumerationAttribute != null)
{
enumerationType = XmlHelper.GetEnumeration(enumerationAttribute.Value);
}
foreach (XmlNode node in nodeList)
{
XmlAttribute idAttribute = node.Attributes[IdAttribute];
XmlAttribute typeAttribute = node.Attributes[TypeAttribute];
XmlAttribute startingViewAttribute = node.Attributes[StartingViewAttribute];
XmlAttribute viewManagerAttribute = node.Attributes[ViewManagerAttribute];
//if an enumeration is specified to identify navigators then make sure the ID is a valid enumeration member
if (enumerationType != null)
{
XmlHelper.ValidateEnumerationMember(enumerationType, idAttribute.Value);
}
//create the navigator info
NavigatorInfo navigatorInfo = new NavigatorInfo(idAttribute.Value);
navigatorInfo.Type = Type.GetType(typeAttribute.Value);
navigatorInfo.ViewManagerInfo = viewManagerInfos[viewManagerAttribute.Value];
//the starting view is optional
if (startingViewAttribute != null)
{
_log.Verbose("Assigning starting view with ID '{0}' to navigator info.", startingViewAttribute.Value);
navigatorInfo.StartViewInfo = viewInfos[startingViewAttribute.Value];
if (navigatorInfo.StartViewInfo == null)
{
_log.Error("Starting view with ID '{0}' was not found for navigator with ID '{1}'.", startingViewAttribute.Value, navigatorInfo.Id);
ExceptionHelper.Throw("ParseNavigators.startingViewNotFound", startingViewAttribute.Value, navigatorInfo.Id);
}
}
Debug.Assert(navigatorInfo.Type != null);
if (navigatorInfo.ViewManagerInfo == null)
{
_log.Error("View manager with ID '{0}' not found for navigator with ID '{1}'.", viewManagerAttribute.Value, navigatorInfo.Id);
ExceptionHelper.Throw("ParseNavigators.viewManagerNotFound", viewManagerAttribute.Value, navigatorInfo.Id);
}
_log.Verbose("Adding NavigatorInfo with ID '{0}', type '{1}'.", navigatorInfo.Id, navigatorInfo.Type.FullName);
navigatorInfos.Add(navigatorInfo);
//parse any custom configuration for the navigator
navigatorInfo.CustomConfiguration = ParseCustomConfiguration(navigatorInfo.Type, configurationInfo, navigatorInfo, node);
}
}
private static void ParseViews(ConfigurationInfo configurationInfo, InfoCollection<Id<IView>, ViewInfo> viewInfos, XmlNode xml, InfoCollection<Id<IController>, ControllerInfo> controllerInfos)
{
Debug.Assert(configurationInfo != null);
Debug.Assert(viewInfos != null);
Debug.Assert(xml != null);
Debug.Assert(controllerInfos != null);
_log.Information("Parsing view configuration.");
XmlNode viewsNode = xml.SelectSingleNode("views");
XmlAttribute enumerationAttribute = viewsNode.Attributes[EnumerationAttribute];
XmlNodeList nodeList = viewsNode.SelectNodes("view");
Type enumerationType = null;
if (enumerationAttribute != null)
{
enumerationType = XmlHelper.GetEnumeration(enumerationAttribute.Value);
}
foreach (XmlNode node in nodeList)
{
XmlAttribute idAttribute = node.Attributes[IdAttribute];
XmlAttribute typeAttribute = node.Attributes[TypeAttribute];
XmlAttribute controllerAttribute = node.Attributes[ControllerAttribute];
//if an enumeration is specified to identify views then make sure the ID is a valid enumeration member
if (enumerationType != null)
{
XmlHelper.ValidateEnumerationMember(enumerationType, idAttribute.Value);
}
//create the view info
ViewInfo viewInfo = new ViewInfo(idAttribute.Value);
viewInfo.Type = Type.GetType(typeAttribute.Value);
//the controller is optional
if (controllerAttribute != null)
{
viewInfo.ControllerInfo = controllerInfos[controllerAttribute.Value];
if (viewInfo.ControllerInfo == null)
{
_log.Error("Controller with ID '{0}' was not found for view with ID '{1}'.", controllerAttribute.Value, viewInfo.Id);
ExceptionHelper.Throw("ParseViews.controllerNotFound", controllerAttribute.Value, viewInfo.Id);
}
}
Debug.Assert(viewInfo.Type != null);
_log.Verbose("Adding ViewInfo with ID '{0}', type '{1}'.", viewInfo.Id, viewInfo.Type.FullName);
viewInfos.Add(viewInfo);
//parse any custom configuration for the view
viewInfo.CustomConfiguration = ParseCustomConfiguration(viewInfo.Type, configurationInfo, viewInfo, node);
}
}
private static void ParseViewManagers(ConfigurationInfo configurationInfo, InfoCollection<Id<IViewManager>, ViewManagerInfo> viewManagerInfos, XmlNode xml)
{
Debug.Assert(configurationInfo != null);
Debug.Assert(viewManagerInfos != null);
Debug.Assert(xml != null);
_log.Information("Parsing view manager configuration.");
XmlNode viewsNode = xml.SelectSingleNode("viewManagers");
XmlAttribute enumerationAttribute = viewsNode.Attributes[EnumerationAttribute];
XmlNodeList nodeList = viewsNode.SelectNodes("viewManager");
Type enumerationType = null;
if (enumerationAttribute != null)
{
enumerationType = XmlHelper.GetEnumeration(enumerationAttribute.Value);
}
foreach (XmlNode node in nodeList)
{
XmlAttribute idAttribute = node.Attributes[IdAttribute];
XmlAttribute typeAttribute = node.Attributes[TypeAttribute];
//if an enumeration is specified to identify view managers then make sure the ID is a valid enumeration member
if (enumerationType != null)
{
XmlHelper.ValidateEnumerationMember(enumerationType, idAttribute.Value);
}
//create the view manager info
ViewManagerInfo viewManagerInfo = new ViewManagerInfo(idAttribute.Value);
viewManagerInfo.Type = Type.GetType(typeAttribute.Value);
Debug.Assert(viewManagerInfo.Type != null);
_log.Verbose("Adding ViewManagerInfo with ID '{0}', type '{1}'.", viewManagerInfo.Id, viewManagerInfo.Type.FullName);
viewManagerInfos.Add(viewManagerInfo);
//parse any custom configuration for the view manager
viewManagerInfo.CustomConfiguration = ParseCustomConfiguration(viewManagerInfo.Type, configurationInfo, viewManagerInfo, node);
}
}
private static object ParseCustomConfiguration(Type type, ConfigurationInfo configurationInfo, object parent, XmlNode xml)
{
Debug.Assert(configurationInfo != null);
Debug.Assert(type != null);
Debug.Assert(xml != null);
_log.Verbose("Parsing custom configuration for type '{0}'.", type.FullName);
object[] attributes = type.GetCustomAttributes(typeof(XmlCustomParserAttribute), true);
if (attributes.Length == 0)
{
_log.Error("No '{0}' attribute found on type '{1}' - custom configuration can not be parsed.", typeof(XmlCustomParserAttribute).FullName, type.FullName);
ExceptionHelper.ThrowIf(xml.HasChildNodes, "ParseCustomConfiguration.customParserNotFound", type.FullName, typeof(XmlCustomParserAttribute).FullName);
return null;
}
else
{
Debug.Assert(attributes.Length == 1);
//get the attribute
XmlCustomParserAttribute parserAttribute = (XmlCustomParserAttribute) attributes[0];
//create the parser
IXmlCustomParser customConfigurationParser = parserAttribute.GetXmlCustomParser();
_log.Verbose("Custom parser of type '{0}' found and created - parsing custom configuration.", customConfigurationParser.GetType().FullName);
//use the parser to parse the XML
return customConfigurationParser.Parse(configurationInfo, parent, xml);
}
}
private sealed class ValidateHelper
{
private StringBuilder _errorMessage;
public string ErrorMessage
{
get
{
return _errorMessage.ToString();
}
}
public bool HasErrors
{
get
{
return (_errorMessage != null);
}
}
public void ValidationHandler(object sender, ValidationEventArgs e)
{
//we cater for the common case (no errors) by only creating the StringBuilder if necessary. Once XML errors are fixed they are
//fixed and we reduce penalties by not creating the StringBuilder unless necessary
if (_errorMessage == null)
{
_errorMessage = new StringBuilder();
}
_errorMessage.Append(" - ").Append(e.Message).Append(Environment.NewLine);
}
}
}
}
|