diff --git a/src/Orchard.Azure/AzureFileSystem.cs b/src/Orchard.Azure/AzureFileSystem.cs index b8170b117..2e293bef3 100644 --- a/src/Orchard.Azure/AzureFileSystem.cs +++ b/src/Orchard.Azure/AzureFileSystem.cs @@ -223,8 +223,8 @@ namespace Orchard.Azure { EnsurePathIsRelative(path); using ( new HttpContextWeaver() ) { - Container.EnsureBlobExists(path); - var blob = Container.GetBlockBlobReference(String.Concat(_root, path)); + Container.EnsureBlobExists(Combine(_root, path)); + var blob = Container.GetBlockBlobReference(Combine(_root, path)); blob.Delete(); } } diff --git a/src/Orchard.Web/Config/log4net.config b/src/Orchard.Web/Config/log4net.config index 3dc7e72bd..999e11599 100644 --- a/src/Orchard.Web/Config/log4net.config +++ b/src/Orchard.Web/Config/log4net.config @@ -54,7 +54,7 @@ - + @@ -73,7 +73,7 @@ - + diff --git a/src/Orchard.Web/Core/Shapes/Views/MenuItem.cshtml b/src/Orchard.Web/Core/Shapes/Views/MenuItem.cshtml index 7b96d871d..5b0d9b766 100644 --- a/src/Orchard.Web/Core/Shapes/Views/MenuItem.cshtml +++ b/src/Orchard.Web/Core/Shapes/Views/MenuItem.cshtml @@ -6,8 +6,10 @@ if (!HasText(Model.Text)) { @DisplayChildren(Model) } else { - if (Model.Href.TrimEnd('/').ToUpperInvariant() == Request.Path.TrimEnd('/').ToUpperInvariant()) { - Model.Classes.Add("current"); + string requestUrl = Request.Path.Replace(Request.ApplicationPath, string.Empty).TrimEnd('/').ToUpperInvariant(); + string modelUrl = Model.Href.Replace(Request.ApplicationPath, string.Empty).TrimEnd('/').ToUpperInvariant(); + if ((!string.IsNullOrEmpty(modelUrl) && requestUrl.StartsWith(modelUrl)) || requestUrl == modelUrl) { + Model.Classes.Add("current"); } var tag = Tag(Model, "li"); @tag.StartElement diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 018ffdbbe..945c86768 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -14,6 +14,8 @@ using Orchard.Users.Services; using Orchard.ContentManagement; using Orchard.Users.Models; using Orchard.UI.Notify; +using Orchard.Users.Events; +using System.Collections.Generic; namespace Orchard.Users.Controllers { [HandleError, Themed] @@ -22,17 +24,19 @@ namespace Orchard.Users.Controllers { private readonly IMembershipService _membershipService; private readonly IUserService _userService; private readonly IOrchardServices _orchardServices; - + private readonly IEnumerable _userEventHandlers; public AccountController( IAuthenticationService authenticationService, IMembershipService membershipService, IUserService userService, - IOrchardServices orchardServices) { + IOrchardServices orchardServices, + IEnumerable userEventHandlers) { _authenticationService = authenticationService; _membershipService = membershipService; _userService = userService; _orchardServices = orchardServices; + _userEventHandlers = userEventHandlers; Logger = NullLogger.Instance; T = NullLocalizer.Instance; } @@ -51,8 +55,13 @@ namespace Orchard.Users.Controllers { } //TODO: (erikpo) Add a setting for whether or not to log access denieds since these can fill up a database pretty fast from bots on a high traffic site + //Suggestion: Could instead use the new AccessDenined IUserEventHandler method and let modules decide if they want to log this event? Logger.Information("Access denied to user #{0} '{1}' on {2}", currentUser.Id, currentUser.UserName, returnUrl); + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.AccessDenied(currentUser); + } + return View(); } @@ -75,13 +84,20 @@ namespace Orchard.Users.Controllers { } _authenticationService.SignIn(user, false); + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.LoggedIn(user); + } return this.RedirectLocal(returnUrl); } public ActionResult LogOff(string returnUrl) { + var wasLoggedInUser = _authenticationService.GetAuthenticatedUser(); _authenticationService.SignOut(); - + if (wasLoggedInUser != null) + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.LoggedOut(wasLoggedInUser); + } return this.RedirectLocal(returnUrl); } @@ -116,12 +132,16 @@ namespace Orchard.Users.Controllers { if (ValidateRegistration(userName, email, password, confirmPassword)) { // Attempt to register the user + // No need to report this to IUserEventHandler because _membershipService does that for us var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false)); if (user != null) { if ( user.As().EmailStatus == UserStatus.Pending ) { _userService.SendChallengeEmail(user.As(), nonce => Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", nonce = nonce }))); + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.SentChallengeEmail(user); + } return RedirectToAction("ChallengeEmailSent"); } @@ -194,6 +214,9 @@ namespace Orchard.Users.Controllers { if ( validated != null ) { _membershipService.SetPassword(validated, newPassword); + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.ChangedPassword(validated); + } return RedirectToAction("ChangePasswordSuccess"); } @@ -265,6 +288,10 @@ namespace Orchard.Users.Controllers { var user = _userService.ValidateChallenge(nonce); if ( user != null ) { + foreach (var userEventHandler in _userEventHandlers) { + userEventHandler.ConfirmedEmail(user); + } + return RedirectToAction("ChallengeEmailSuccess"); } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs b/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs index 79b0962ed..d6bbbb634 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs @@ -1,4 +1,5 @@ using Orchard.Events; +using Orchard.Security; namespace Orchard.Users.Events { public interface IUserEventHandler : IEventHandler { @@ -8,9 +9,39 @@ namespace Orchard.Users.Events { void Creating(UserContext context); /// - /// Called once a user has been created + /// Called after a user has been created /// void Created(UserContext context); + + /// + /// Called after a user has logged in + /// + void LoggedIn(IUser user); + + /// + /// Called when a user explicitly logs out (as opposed to one whos session cookie simply expires) + /// + void LoggedOut(IUser user); + + /// + /// Called when access is denied to a user + /// + void AccessDenied(IUser user); + + /// + /// Called after a user has changed password + /// + void ChangedPassword(IUser user); + + /// + /// Called after a user has confirmed their email address + /// + void SentChallengeEmail(IUser user); + + /// + /// Called after a user has confirmed their email address + /// + void ConfirmedEmail(IUser user); } } diff --git a/src/Orchard/Data/SessionConfigurationCache.cs b/src/Orchard/Data/SessionConfigurationCache.cs index 2bc3a3a7c..00c529904 100644 --- a/src/Orchard/Data/SessionConfigurationCache.cs +++ b/src/Orchard/Data/SessionConfigurationCache.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; using NHibernate.Cfg; +using Orchard.Environment; using Orchard.Environment.Configuration; using Orchard.Environment.ShellBuilders.Models; using Orchard.FileSystems.AppData; @@ -14,11 +15,13 @@ namespace Orchard.Data { private readonly ShellSettings _shellSettings; private readonly ShellBlueprint _shellBlueprint; private readonly IAppDataFolder _appDataFolder; + private readonly IHostEnvironment _hostEnvironment; - public SessionConfigurationCache(ShellSettings shellSettings, ShellBlueprint shellBlueprint, IAppDataFolder appDataFolder) { + public SessionConfigurationCache(ShellSettings shellSettings, ShellBlueprint shellBlueprint, IAppDataFolder appDataFolder, IHostEnvironment hostEnvironment) { _shellSettings = shellSettings; _shellBlueprint = shellBlueprint; _appDataFolder = appDataFolder; + _hostEnvironment = hostEnvironment; Logger = NullLogger.Instance; } @@ -51,6 +54,9 @@ namespace Orchard.Data { } private void StoreConfiguration(ConfigurationCache cache) { + if (!_hostEnvironment.IsFullTrust) + return; + var pathName = GetPathName(_shellSettings.Name); try { @@ -70,6 +76,9 @@ namespace Orchard.Data { } private ConfigurationCache ReadConfiguration(string hash) { + if (!_hostEnvironment.IsFullTrust) + return null; + var pathName = GetPathName(_shellSettings.Name); if (!_appDataFolder.FileExists(pathName)) diff --git a/src/Orchard/Logging/LoggingModule.cs b/src/Orchard/Logging/LoggingModule.cs index 559458f19..c1e3f2228 100644 --- a/src/Orchard/Logging/LoggingModule.cs +++ b/src/Orchard/Logging/LoggingModule.cs @@ -20,7 +20,7 @@ namespace Orchard.Logging { protected override void Load(ContainerBuilder moduleBuilder) { // by default, use Orchard's logger that delegates to Castle's logger factory moduleBuilder.RegisterType().As().InstancePerLifetimeScope(); - moduleBuilder.RegisterType().As().InstancePerLifetimeScope(); + moduleBuilder.RegisterType().As().InstancePerLifetimeScope(); // call CreateLogger in response to the request for an ILogger implementation moduleBuilder.Register(CreateLogger).As().InstancePerDependency(); diff --git a/src/Orchard/Logging/OrchardLog4netFactory.cs b/src/Orchard/Logging/OrchardLog4netFactory.cs new file mode 100644 index 000000000..4a5dd45d4 --- /dev/null +++ b/src/Orchard/Logging/OrchardLog4netFactory.cs @@ -0,0 +1,54 @@ +using System; +using System.Configuration; +using System.IO; +using System.Web; +using System.Web.Hosting; + +using Castle.Core.Logging; + +using log4net; + +namespace Orchard.Logging { + public class OrchardLog4netFactory : AbstractLoggerFactory { + public OrchardLog4netFactory() + : this(ConfigurationManager.AppSettings["log4net.Config"]) { + } + + public OrchardLog4netFactory(String configFilename) { + if (!String.IsNullOrWhiteSpace(configFilename)) { + var mappedConfigFilename = configFilename; + + if (HostingEnvironment.IsHosted) { + if (!VirtualPathUtility.IsAppRelative(mappedConfigFilename)) { + if (!mappedConfigFilename.StartsWith("/")) { + mappedConfigFilename = "~/" + mappedConfigFilename; + } + else { + mappedConfigFilename = "~" + mappedConfigFilename; + } + } + + mappedConfigFilename = HostingEnvironment.MapPath(mappedConfigFilename); + } + + OrchardXmlConfigurator.Configure(mappedConfigFilename); + } + } + + /// + /// Configures log4net with a stream containing XML. + /// + /// + public OrchardLog4netFactory(Stream config) { + OrchardXmlConfigurator.Configure(config); + } + + public override Castle.Core.Logging.ILogger Create(string name, LoggerLevel level) { + throw new NotSupportedException("Logger levels cannot be set at runtime. Please review your configuration file."); + } + + public override Castle.Core.Logging.ILogger Create(string name) { + return new OrchardLog4netLogger(LogManager.GetLogger(name), this); + } + } +} diff --git a/src/Orchard/Logging/OrchardLog4netLogger.cs b/src/Orchard/Logging/OrchardLog4netLogger.cs new file mode 100644 index 000000000..d204495a8 --- /dev/null +++ b/src/Orchard/Logging/OrchardLog4netLogger.cs @@ -0,0 +1,333 @@ +using System; +using System.Globalization; + +using log4net; +using log4net.Core; +using log4net.Util; + +using Logger = Castle.Core.Logging.ILogger; + +namespace Orchard.Logging { + [Serializable] + public class OrchardLog4netLogger : MarshalByRefObject, Logger { + private static readonly Type declaringType = typeof(OrchardLog4netLogger); + + public OrchardLog4netLogger(log4net.Core.ILogger logger, OrchardLog4netFactory factory) { + Logger = logger; + Factory = factory; + } + + internal OrchardLog4netLogger() { + } + + internal OrchardLog4netLogger(ILog log, OrchardLog4netFactory factory) + : this(log.Logger, factory) { + } + + public bool IsDebugEnabled { + get { return Logger.IsEnabledFor(Level.Debug); } + } + + public bool IsErrorEnabled { + get { return Logger.IsEnabledFor(Level.Error); } + } + + public bool IsFatalEnabled { + get { return Logger.IsEnabledFor(Level.Fatal); } + } + + public bool IsInfoEnabled { + get { return Logger.IsEnabledFor(Level.Info); } + } + + public bool IsWarnEnabled { + get { return Logger.IsEnabledFor(Level.Warn); } + } + + protected internal OrchardLog4netFactory Factory { get; set; } + + protected internal log4net.Core.ILogger Logger { get; set; } + + public override string ToString() { + return Logger.ToString(); + } + + public virtual Logger CreateChildLogger(String name) { + return Factory.Create(Logger.Name + "." + name); + } + + public void Debug(String message) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, message, null); + } + } + + public void Debug(Func messageFactory) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, messageFactory.Invoke(), null); + } + } + + public void Debug(String message, Exception exception) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, message, exception); + } + } + + public void DebugFormat(String format, params Object[] args) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), null); + } + } + + public void DebugFormat(Exception exception, String format, params Object[] args) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), exception); + } + } + + public void DebugFormat(IFormatProvider formatProvider, String format, params Object[] args) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, new SystemStringFormat(formatProvider, format, args), null); + } + } + + public void DebugFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, new SystemStringFormat(formatProvider, format, args), exception); + } + } + + public void Error(String message) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, message, null); + } + } + + public void Error(Func messageFactory) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, messageFactory.Invoke(), null); + } + } + + public void Error(String message, Exception exception) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, message, exception); + } + } + + public void ErrorFormat(String format, params Object[] args) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), null); + } + } + + public void ErrorFormat(Exception exception, String format, params Object[] args) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), exception); + } + } + + public void ErrorFormat(IFormatProvider formatProvider, String format, params Object[] args) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, new SystemStringFormat(formatProvider, format, args), null); + } + } + + public void ErrorFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, new SystemStringFormat(formatProvider, format, args), exception); + } + } + + public void Fatal(String message) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, message, null); + } + } + + public void Fatal(Func messageFactory) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, messageFactory.Invoke(), null); + } + } + + public void Fatal(String message, Exception exception) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, message, exception); + } + } + + public void FatalFormat(String format, params Object[] args) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), null); + } + } + + public void FatalFormat(Exception exception, String format, params Object[] args) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), exception); + } + } + + public void FatalFormat(IFormatProvider formatProvider, String format, params Object[] args) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, new SystemStringFormat(formatProvider, format, args), null); + } + } + + public void FatalFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, new SystemStringFormat(formatProvider, format, args), exception); + } + } + + public void Info(String message) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, message, null); + } + } + + public void Info(Func messageFactory) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, messageFactory.Invoke(), null); + } + } + + public void Info(String message, Exception exception) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, message, exception); + } + } + + public void InfoFormat(String format, params Object[] args) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), null); + } + } + + public void InfoFormat(Exception exception, String format, params Object[] args) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), exception); + } + } + + public void InfoFormat(IFormatProvider formatProvider, String format, params Object[] args) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, new SystemStringFormat(formatProvider, format, args), null); + } + } + + public void InfoFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, new SystemStringFormat(formatProvider, format, args), exception); + } + } + + public void Warn(String message) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, message, null); + } + } + + public void Warn(Func messageFactory) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, messageFactory.Invoke(), null); + } + } + + public void Warn(String message, Exception exception) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, message, exception); + } + } + + public void WarnFormat(String format, params Object[] args) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), null); + } + } + + public void WarnFormat(Exception exception, String format, params Object[] args) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, new SystemStringFormat(CultureInfo.InvariantCulture, format, args), exception); + } + } + + public void WarnFormat(IFormatProvider formatProvider, String format, params Object[] args) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, new SystemStringFormat(formatProvider, format, args), null); + } + } + + public void WarnFormat(Exception exception, IFormatProvider formatProvider, String format, params Object[] args) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, new SystemStringFormat(formatProvider, format, args), exception); + } + } + + [Obsolete("Use IsFatalEnabled instead")] + public bool IsFatalErrorEnabled { + get { + return Logger.IsEnabledFor(Level.Fatal); + } + } + + [Obsolete("Use DebugFormat instead")] + public void Debug(string format, params object[] args) { + if (IsDebugEnabled) { + Logger.Log(declaringType, Level.Debug, string.Format(format, args), null); + } + } + + [Obsolete("Use ErrorFormat instead")] + public void Error(string format, params object[] args) { + if (IsErrorEnabled) { + Logger.Log(declaringType, Level.Error, string.Format(format, args), null); + } + } + + [Obsolete("Use FatalFormat instead")] + public void Fatal(string format, params object[] args) { + if (IsFatalEnabled) { + Logger.Log(declaringType, Level.Fatal, string.Format(format, args), null); + } + } + + [Obsolete("Use Fatal instead")] + public void FatalError(string message) { + if (IsFatalErrorEnabled) { + Logger.Log(declaringType, Level.Fatal, message, null); + } + } + + [Obsolete("Use FatalFormat instead")] + public void FatalError(string format, params object[] args) { + if (IsFatalErrorEnabled) { + Logger.Log(declaringType, Level.Fatal, string.Format(format, args), null); + } + } + + [Obsolete("Use Fatal instead")] + public void FatalError(string message, Exception exception) { + if (IsFatalErrorEnabled) { + Logger.Log(declaringType, Level.Fatal, message, exception); + } + } + + [Obsolete("Use InfoFormat instead")] + public void Info(string format, params object[] args) { + if (IsInfoEnabled) { + Logger.Log(declaringType, Level.Info, string.Format(format, args), null); + } + } + + [Obsolete("Use WarnFormat instead")] + public void Warn(string format, params object[] args) { + if (IsWarnEnabled) { + Logger.Log(declaringType, Level.Warn, string.Format(format, args), null); + } + } + + } +} \ No newline at end of file diff --git a/src/Orchard/Logging/OrchardXmlConfigurator.cs b/src/Orchard/Logging/OrchardXmlConfigurator.cs new file mode 100644 index 000000000..965f389f9 --- /dev/null +++ b/src/Orchard/Logging/OrchardXmlConfigurator.cs @@ -0,0 +1,308 @@ +using System; +using System.IO; +using System.Reflection; +using System.Xml; + +using log4net; +using log4net.Repository; +using log4net.Repository.Hierarchy; +using log4net.Util; + +namespace Orchard.Logging { + public class OrchardXmlConfigurator { + + /// + /// Private constructor + /// + private OrchardXmlConfigurator() { + } + + /// + /// Configures log4net using the specified configuration file. + /// + /// The name of the XML file to load the configuration from. + /// + /// + /// The configuration file must be valid XML. It must contain + /// at least one element called log4net that holds + /// the log4net configuration data. + /// + /// + /// The log4net configuration file can possible be specified in the application's + /// configuration file (either MyAppName.exe.config for a + /// normal application on Web.config for an ASP.NET application). + /// + /// + /// The first element matching <configuration> will be read as the + /// configuration. If this file is also a .NET .config file then you must specify + /// a configuration section for the log4net element otherwise .NET will + /// complain. Set the type for the section handler to , for example: + /// + /// + ///
+ /// + /// + /// + /// + /// The following example configures log4net using a configuration file, of which the + /// location is stored in the application's configuration file : + /// + /// + /// using log4net.Config; + /// using System.IO; + /// using System.Configuration; + /// + /// ... + /// + /// XmlConfigurator.Configure(ConfigurationSettings.AppSettings["log4net-config-file"]); + /// + /// + /// In the .config file, the path to the log4net can be specified like this : + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void Configure(string configFilename) { + Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()), configFilename); + } + + /// + /// Configures log4net using the specified configuration data stream. + /// + /// A stream to load the XML configuration from. + /// + /// + /// The configuration data must be valid XML. It must contain + /// at least one element called log4net that holds + /// the log4net configuration data. + /// + /// + /// Note that this method will NOT close the stream parameter. + /// + /// + public static void Configure(Stream configStream) { + Configure(LogManager.GetRepository(Assembly.GetCallingAssembly()), configStream); + } + + /// + /// Configures the using the specified configuration + /// file. + /// + /// The repository to configure. + /// The name of the XML file to load the configuration from. + /// + /// + /// The configuration file must be valid XML. It must contain + /// at least one element called log4net that holds + /// the configuration data. + /// + /// + /// The log4net configuration file can possible be specified in the application's + /// configuration file (either MyAppName.exe.config for a + /// normal application on Web.config for an ASP.NET application). + /// + /// + /// The first element matching <configuration> will be read as the + /// configuration. If this file is also a .NET .config file then you must specify + /// a configuration section for the log4net element otherwise .NET will + /// complain. Set the type for the section handler to , for example: + /// + /// + ///
+ /// + /// + /// + /// + /// The following example configures log4net using a configuration file, of which the + /// location is stored in the application's configuration file : + /// + /// + /// using log4net.Config; + /// using System.IO; + /// using System.Configuration; + /// + /// ... + /// + /// XmlConfigurator.Configure(ConfigurationSettings.AppSettings["log4net-config-file"]); + /// + /// + /// In the .config file, the path to the log4net can be specified like this : + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void Configure(ILoggerRepository repository, string configFilename) { + LogLog.Debug("XmlConfigurator: configuring repository [" + repository.Name + "] using file [" + configFilename + "]"); + + if (String.IsNullOrWhiteSpace(configFilename)) { + LogLog.Error("XmlConfigurator: Configure called with null 'configFilename' parameter"); + } + else { + // Have to use File.Exists() rather than configFile.Exists() + // because configFile.Exists() caches the value, not what we want. + if (File.Exists(configFilename)) { + // Open the file for reading + FileStream fs = null; + + // Try hard to open the file + for (var retry = 5; --retry >= 0; ) { + try { + fs = new FileStream(configFilename, FileMode.Open, FileAccess.Read, FileShare.Read); + break; + } + catch (IOException ex) { + if (retry == 0) { + LogLog.Error("XmlConfigurator: Failed to open XML config file [" + configFilename + "]", ex); + + // The stream cannot be valid + fs = null; + } + + System.Threading.Thread.Sleep(250); + } + } + + if (fs != null) { + try { + // Load the configuration from the stream + Configure(repository, fs); + } + finally { + // Force the file closed whatever happens + fs.Close(); + } + } + } + else { + LogLog.Debug("XmlConfigurator: config file [" + configFilename + "] not found. Configuration unchanged."); + } + } + } + + /// + /// Configures the using the specified configuration + /// file. + /// + /// The repository to configure. + /// The stream to load the XML configuration from. + /// + /// + /// The configuration data must be valid XML. It must contain + /// at least one element called log4net that holds + /// the configuration data. + /// + /// + /// Note that this method will NOT close the stream parameter. + /// + /// + public static void Configure(ILoggerRepository repository, Stream configStream) { + LogLog.Debug("XmlConfigurator: configuring repository [" + repository.Name + "] using stream"); + + if (configStream == null) { + LogLog.Error("XmlConfigurator: Configure called with null 'configStream' parameter"); + } + else { + // Load the config file into a document + var doc = new XmlDocument(); + try { + // Create a text reader for the file stream + var xmlReader = new XmlTextReader(configStream) { DtdProcessing = DtdProcessing.Parse }; + + // Specify that the reader should not perform validation + var settings = new XmlReaderSettings { ValidationType = ValidationType.None }; + + // load the data into the document + doc.Load(xmlReader); + } + catch (Exception ex) { + LogLog.Error("XmlConfigurator: Error while loading XML configuration", ex); + + // The document is invalid + doc = null; + } + + if (doc != null) { + LogLog.Debug("XmlConfigurator: loading XML configuration"); + + // Configure using the 'log4net' element + var configNodeList = doc.GetElementsByTagName("log4net"); + if (configNodeList.Count == 0) { + LogLog.Debug("XmlConfigurator: XML configuration does not contain a element. Configuration Aborted."); + } + else if (configNodeList.Count > 1) { + LogLog.Error("XmlConfigurator: XML configuration contains [" + configNodeList.Count + "] elements. Only one is allowed. Configuration Aborted."); + } + else { + ConfigureFromXml(repository, configNodeList[0] as XmlElement); + } + } + } + } + + /// + /// Configures the specified repository using a log4net element. + /// + /// The hierarchy to configure. + /// The element to parse. + /// + /// + /// Loads the log4net configuration from the XML element + /// supplied as . + /// + /// + /// This method is ultimately called by one of the Configure methods + /// to load the configuration from an . + /// + /// + private static void ConfigureFromXml(ILoggerRepository repository, XmlElement element) { + if (element == null) { + LogLog.Error("XmlConfigurator: ConfigureFromXml called with null 'element' parameter"); + } + else if (repository == null) { + LogLog.Error("XmlConfigurator: ConfigureFromXml called with null 'repository' parameter"); + } + else { + LogLog.Debug("XmlConfigurator: Configuring Repository [" + repository.Name + "]"); + + // + // Since we're not reinventing the whole Hierarchy class from log4net to add out optimizations to XmlRepositoryConfigurator + // we've to check the neccessary casts here to be able to complete the configuration. + // + + // Needed to fire configuration changed event + var repositorySkeleton = repository as LoggerRepositorySkeleton; + + // Needed to XmlHierarchyConfigurator + var hierarchy = repository as Hierarchy; + + if (repositorySkeleton == null || hierarchy == null) { + LogLog.Warn("XmlConfigurator: Repository [" + repository + "] does not support the XmlConfigurator"); + } + else { + var configurator = new OrchardXmlHierarchyConfigurator(hierarchy); + + // Copy the xml data into the root of a new document + // this isolates the xml config data from the rest of + // the document + var newDoc = new XmlDocument(); + var newElement = (XmlElement)newDoc.AppendChild(newDoc.ImportNode(element, true)); + + // Pass the configurator the config element + configurator.Configure(newElement); + + repositorySkeleton.RaiseConfigurationChanged(EventArgs.Empty); + } + } + } + } +} diff --git a/src/Orchard/Logging/OrchardXmlHierarchyConfigurator.cs b/src/Orchard/Logging/OrchardXmlHierarchyConfigurator.cs new file mode 100644 index 000000000..a04774c38 --- /dev/null +++ b/src/Orchard/Logging/OrchardXmlHierarchyConfigurator.cs @@ -0,0 +1,864 @@ +using System; +using System.Collections; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml; + +using log4net.Appender; +using log4net.Core; +using log4net.ObjectRenderer; +using log4net.Repository.Hierarchy; +using log4net.Util; + +namespace Orchard.Logging { + /// + /// Initializes the log4net environment using an XML DOM. + /// + /// + /// + /// Configures a using an XML DOM. + /// + /// + /// Nicko Cadell + /// Gert Driesen + public class OrchardXmlHierarchyConfigurator { + private enum ConfigUpdateMode { + Merge, + Overwrite + } + + // String constants used while parsing the XML data + private const string ConfigurationTag = "log4net"; + private const string RendererTag = "renderer"; + private const string AppenderTag = "appender"; + private const string AppenderRefTag = "appender-ref"; + private const string ParamTag = "param"; + + // TODO: Deprecate use of category tags + private const string CategoryTag = "category"; + // TODO: Deprecate use of priority tag + private const string PriorityTag = "priority"; + + private const string LoggerTag = "logger"; + private const string NameAttr = "name"; + private const string TypeAttr = "type"; + private const string ValueAttr = "value"; + private const string RootTag = "root"; + private const string LevelTag = "level"; + private const string RefAttr = "ref"; + private const string AdditivityAttr = "additivity"; + private const string ThresholdAttr = "threshold"; + private const string ConfigDebugAttr = "configDebug"; + private const string InternalDebugAttr = "debug"; + private const string ConfigUpdateModeAttr = "update"; + private const string RenderingTypeAttr = "renderingClass"; + private const string RenderedTypeAttr = "renderedClass"; + + // flag used on the level element + private const string Inherited = "inherited"; + + /// + /// key: appenderName, value: appender. + /// + private Hashtable _appenderBag; + + /// + /// The Hierarchy being configured. + /// + private readonly Hierarchy _hierarchy; + + /// + /// The snapshot of the environment variables at configuration time, or null if an error has occured during querying them. + /// + private IDictionary _environmentVariables; + + /// + /// Construct the configurator for a hierarchy + /// + /// The hierarchy to build. + /// + /// + /// Initializes a new instance of the class + /// with the specified . + /// + /// + public OrchardXmlHierarchyConfigurator(Hierarchy hierarchy) { + _hierarchy = hierarchy; + _appenderBag = new Hashtable(); + } + + /// + /// Configure the hierarchy by parsing a DOM tree of XML elements. + /// + /// The root element to parse. + /// + /// + /// Configure the hierarchy by parsing a DOM tree of XML elements. + /// + /// + public void Configure(XmlElement element) { + if (element == null || _hierarchy == null) { + return; + } + + var rootElementName = element.LocalName; + + if (rootElementName != ConfigurationTag) { + LogLog.Error("XmlHierarchyConfigurator: Xml element is - not a <" + ConfigurationTag + "> element."); + return; + } + + if (!LogLog.InternalDebugging) { + // Look for a debug attribute to enable internal debug + var debugAttribute = element.GetAttribute(InternalDebugAttr); + LogLog.Debug("XmlHierarchyConfigurator: " + InternalDebugAttr + " attribute [" + debugAttribute + "]."); + + if (debugAttribute.Length > 0 && debugAttribute != "null") { + LogLog.InternalDebugging = OptionConverter.ToBoolean(debugAttribute, true); + } + else { + LogLog.Debug("XmlHierarchyConfigurator: Ignoring " + InternalDebugAttr + " attribute."); + } + + var confDebug = element.GetAttribute(ConfigDebugAttr); + if (confDebug.Length > 0 && confDebug != "null") { + LogLog.Warn("XmlHierarchyConfigurator: The \"" + ConfigDebugAttr + "\" attribute is deprecated."); + LogLog.Warn("XmlHierarchyConfigurator: Use the \"" + InternalDebugAttr + "\" attribute instead."); + LogLog.InternalDebugging = OptionConverter.ToBoolean(confDebug, true); + } + } + + // Default mode is merge + var configUpdateMode = ConfigUpdateMode.Merge; + + // Look for the config update attribute + var configUpdateModeAttribute = element.GetAttribute(ConfigUpdateModeAttr); + if (!String.IsNullOrEmpty(configUpdateModeAttribute)) { + // Parse the attribute + try { + configUpdateMode = (ConfigUpdateMode)OptionConverter.ConvertStringTo(typeof(ConfigUpdateMode), configUpdateModeAttribute); + } + catch { + LogLog.Error("XmlHierarchyConfigurator: Invalid " + ConfigUpdateModeAttr + " attribute value [" + configUpdateModeAttribute + "]"); + } + } + + // IMPL: The IFormatProvider argument to Enum.ToString() is deprecated in .NET 2.0 + LogLog.Debug("XmlHierarchyConfigurator: Configuration update mode [" + configUpdateMode + "]."); + + // Only reset configuration if overwrite flag specified + if (configUpdateMode == ConfigUpdateMode.Overwrite) { + // Reset to original unset configuration + _hierarchy.ResetConfiguration(); + LogLog.Debug("XmlHierarchyConfigurator: Configuration reset before reading config."); + } + + // Try to retrieve the environment variables + try { + _environmentVariables = System.Environment.GetEnvironmentVariables(); + } + catch (System.Security.SecurityException) { + _environmentVariables = null; + + // This security exception will occur if the caller does not have + // unrestricted environment permission. If this occurs the expansion + // will be skipped with the following warning message. + LogLog.Debug("XmlHierarchyConfigurator: Security exception while trying to expand environment variables. Error Ignored. No Expansion."); + } + + /* Building Appender objects, placing them in a local namespace + for future reference */ + + /* Process all the top level elements */ + + foreach (XmlNode currentNode in element.ChildNodes) { + if (currentNode.NodeType == XmlNodeType.Element) { + var currentElement = (XmlElement)currentNode; + + if (currentElement.LocalName == LoggerTag) { + ParseLogger(currentElement); + } + else if (currentElement.LocalName == CategoryTag) { + // TODO: deprecated use of category + ParseLogger(currentElement); + } + else if (currentElement.LocalName == RootTag) { + ParseRoot(currentElement); + } + else if (currentElement.LocalName == RendererTag) { + ParseRenderer(currentElement); + } + else if (currentElement.LocalName == AppenderTag) { + // We ignore appenders in this pass. They will + // be found and loaded if they are referenced. + } + else { + // Read the param tags and set properties on the hierarchy + SetParameter(currentElement, _hierarchy); + } + } + } + + // Lastly set the hierarchy threshold + string thresholdStr = element.GetAttribute(ThresholdAttr); + LogLog.Debug("XmlHierarchyConfigurator: Hierarchy Threshold [" + thresholdStr + "]"); + if (thresholdStr.Length > 0 && thresholdStr != "null") { + var thresholdLevel = (Level)ConvertStringTo(typeof(Level), thresholdStr); + if (thresholdLevel != null) { + _hierarchy.Threshold = thresholdLevel; + } + else { + LogLog.Warn("XmlHierarchyConfigurator: Unable to set hierarchy threshold using value [" + thresholdStr + "] (with acceptable conversion types)"); + } + } + + // Done reading config + } + + /// + /// Parse appenders by IDREF. + /// + /// The appender ref element. + /// The instance of the appender that the ref refers to. + /// + /// + /// Parse an XML element that represents an appender and return + /// the appender. + /// + /// + protected IAppender FindAppenderByReference(XmlElement appenderRef) { + var appenderName = appenderRef.GetAttribute(RefAttr); + + IAppender appender = (IAppender)_appenderBag[appenderName]; + if (appender != null) { + return appender; + } + // Find the element with that id + XmlElement element = null; + + if (!String.IsNullOrEmpty(appenderName)) { + foreach (XmlElement curAppenderElement in appenderRef.OwnerDocument.GetElementsByTagName(AppenderTag)) { + if (curAppenderElement.GetAttribute("name") != appenderName) { + continue; + } + element = curAppenderElement; + break; + } + } + + if (element == null) { + LogLog.Error("XmlHierarchyConfigurator: No appender named [" + appenderName + "] could be found."); + return null; + } + appender = ParseAppender(element); + if (appender != null) { + _appenderBag[appenderName] = appender; + } + return appender; + } + + /// + /// Parses an appender element. + /// + /// The appender element. + /// The appender instance or null when parsing failed. + /// + /// + /// Parse an XML element that represents an appender and return + /// the appender instance. + /// + /// + protected IAppender ParseAppender(XmlElement appenderElement) { + var appenderName = appenderElement.GetAttribute(NameAttr); + var typeName = appenderElement.GetAttribute(TypeAttr); + + LogLog.Debug("XmlHierarchyConfigurator: Loading Appender [" + appenderName + "] type: [" + typeName + "]"); + try { + var appender = (IAppender)Activator.CreateInstance(SystemInfo.GetTypeFromString(typeName, true, true)); + appender.Name = appenderName; + + foreach (XmlNode currentNode in appenderElement.ChildNodes) { + /* We're only interested in Elements */ + if (currentNode.NodeType == XmlNodeType.Element) { + var currentElement = (XmlElement)currentNode; + + // Look for the appender ref tag + if (currentElement.LocalName == AppenderRefTag) { + var refName = currentElement.GetAttribute(RefAttr); + + var appenderContainer = appender as IAppenderAttachable; + if (appenderContainer != null) { + LogLog.Debug("XmlHierarchyConfigurator: Attaching appender named [" + refName + "] to appender named [" + appender.Name + "]."); + + var referencedAppender = FindAppenderByReference(currentElement); + if (referencedAppender != null) { + appenderContainer.AddAppender(referencedAppender); + } + } + else { + LogLog.Error("XmlHierarchyConfigurator: Requesting attachment of appender named [" + refName + "] to appender named [" + appender.Name + "] which does not implement log4net.Core.IAppenderAttachable."); + } + } + else { + // For all other tags we use standard set param method + SetParameter(currentElement, appender); + } + } + } + + var optionHandler = appender as IOptionHandler; + if (optionHandler != null) { + optionHandler.ActivateOptions(); + } + + LogLog.Debug("XmlHierarchyConfigurator: Created Appender [" + appenderName + "]"); + return appender; + } + catch (Exception ex) { + // Yes, it's ugly. But all exceptions point to the same problem: we can't create an Appender + + LogLog.Error("XmlHierarchyConfigurator: Could not create Appender [" + appenderName + "] of type [" + typeName + "]. Reported error follows.", ex); + return null; + } + } + + /// + /// Parses a logger element. + /// + /// The logger element. + /// + /// + /// Parse an XML element that represents a logger. + /// + /// + protected void ParseLogger(XmlElement loggerElement) { + // Create a new log4net.Logger object from the element. + var loggerName = loggerElement.GetAttribute(NameAttr); + + LogLog.Debug("XmlHierarchyConfigurator: Retrieving an instance of log4net.Repository.Logger for logger [" + loggerName + "]."); + var log = _hierarchy.GetLogger(loggerName) as Logger; + + // Setting up a logger needs to be an atomic operation, in order + // to protect potential log operations while logger + // configuration is in progress. + if (log == null) { + return; + } + lock (log) { + var additivity = OptionConverter.ToBoolean(loggerElement.GetAttribute(AdditivityAttr), true); + + LogLog.Debug("XmlHierarchyConfigurator: Setting [" + log.Name + "] additivity to [" + additivity + "]."); + log.Additivity = additivity; + ParseChildrenOfLoggerElement(loggerElement, log, false); + } + } + + /// + /// Parses the root logger element. + /// + /// The root element. + /// + /// + /// Parse an XML element that represents the root logger. + /// + /// + protected void ParseRoot(XmlElement rootElement) { + var root = _hierarchy.Root; + // logger configuration needs to be atomic + lock (root) { + ParseChildrenOfLoggerElement(rootElement, root, true); + } + } + + /// + /// Parses the children of a logger element. + /// + /// The category element. + /// The logger instance. + /// Flag to indicate if the logger is the root logger. + /// + /// + /// Parse the child elements of a <logger> element. + /// + /// + protected void ParseChildrenOfLoggerElement(XmlElement catElement, Logger log, bool isRoot) { + // Remove all existing appenders from log. They will be + // reconstructed if need be. + log.RemoveAllAppenders(); + + foreach (XmlNode currentNode in catElement.ChildNodes) { + if (currentNode.NodeType == XmlNodeType.Element) { + var currentElement = (XmlElement)currentNode; + + if (currentElement.LocalName == AppenderRefTag) { + var appender = FindAppenderByReference(currentElement); + var refName = currentElement.GetAttribute(RefAttr); + if (appender != null) { + LogLog.Debug("XmlHierarchyConfigurator: Adding appender named [" + refName + "] to logger [" + log.Name + "]."); + log.AddAppender(appender); + } + else { + LogLog.Error("XmlHierarchyConfigurator: Appender named [" + refName + "] not found."); + } + } + else if (currentElement.LocalName == LevelTag || currentElement.LocalName == PriorityTag) { + ParseLevel(currentElement, log, isRoot); + } + else { + SetParameter(currentElement, log); + } + } + } + + var optionHandler = log as IOptionHandler; + if (optionHandler != null) { + optionHandler.ActivateOptions(); + } + } + + /// + /// Parses an object renderer. + /// + /// The renderer element. + /// + /// + /// Parse an XML element that represents a renderer. + /// + /// + protected void ParseRenderer(XmlElement element) { + var renderingClassName = element.GetAttribute(RenderingTypeAttr); + var renderedClassName = element.GetAttribute(RenderedTypeAttr); + + LogLog.Debug("XmlHierarchyConfigurator: Rendering class [" + renderingClassName + "], Rendered class [" + renderedClassName + "]."); + var renderer = (IObjectRenderer)OptionConverter.InstantiateByClassName(renderingClassName, typeof(IObjectRenderer), null); + if (renderer == null) { + LogLog.Error("XmlHierarchyConfigurator: Could not instantiate renderer [" + renderingClassName + "]."); + return; + } + try { + _hierarchy.RendererMap.Put(SystemInfo.GetTypeFromString(renderedClassName, true, true), renderer); + } + catch (Exception e) { + LogLog.Error("XmlHierarchyConfigurator: Could not find class [" + renderedClassName + "].", e); + } + } + + /// + /// Parses a level element. + /// + /// The level element. + /// The logger object to set the level on. + /// Flag to indicate if the logger is the root logger. + /// + /// + /// Parse an XML element that represents a level. + /// + /// + protected void ParseLevel(XmlElement element, Logger log, bool isRoot) { + var loggerName = log.Name; + if (isRoot) { + loggerName = "root"; + } + + var levelStr = element.GetAttribute(ValueAttr); + LogLog.Debug("XmlHierarchyConfigurator: Logger [" + loggerName + "] Level string is [" + levelStr + "]."); + + if (Inherited == levelStr) { + if (isRoot) { + LogLog.Error("XmlHierarchyConfigurator: Root level cannot be inherited. Ignoring directive."); + } + else { + LogLog.Debug("XmlHierarchyConfigurator: Logger [" + loggerName + "] level set to inherit from parent."); + log.Level = null; + } + } + else { + log.Level = log.Hierarchy.LevelMap[levelStr]; + if (log.Level == null) { + LogLog.Error("XmlHierarchyConfigurator: Undefined level [" + levelStr + "] on Logger [" + loggerName + "]."); + } + else { + LogLog.Debug("XmlHierarchyConfigurator: Logger [" + loggerName + "] level set to [name=\"" + log.Level.Name + "\",value=" + log.Level.Value + "]."); + } + } + } + + /// + /// Sets a parameter on an object. + /// + /// The parameter element. + /// The object to set the parameter on. + /// + /// The parameter name must correspond to a writable property + /// on the object. The value of the parameter is a string, + /// therefore this function will attempt to set a string + /// property first. If unable to set a string property it + /// will inspect the property and its argument type. It will + /// attempt to call a static method called Parse on the + /// type of the property. This method will take a single + /// string argument and return a value that can be used to + /// set the property. + /// + protected void SetParameter(XmlElement element, object target) { + // Get the property name + var name = element.GetAttribute(NameAttr); + + // If the name attribute does not exist then use the name of the element + if (element.LocalName != ParamTag || String.IsNullOrEmpty(name)) { + name = element.LocalName; + } + + // Look for the property on the target object + var targetType = target.GetType(); + Type propertyType = null; + + MethodInfo methInfo = null; + + // Try to find a writable property + var propInfo = targetType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase); + if (propInfo != null && propInfo.CanWrite) { + // found a property + propertyType = propInfo.PropertyType; + } + else { + propInfo = null; + + // look for a method with the signature Add(type) + methInfo = FindMethodInfo(targetType, name); + + if (methInfo != null) { + propertyType = methInfo.GetParameters()[0].ParameterType; + } + } + + if (propertyType == null) { + LogLog.Error("XmlHierarchyConfigurator: Cannot find Property [" + name + "] to set object on [" + target + "]"); + } + else { + string propertyValue = null; + + if (element.GetAttributeNode(ValueAttr) != null) { + propertyValue = element.GetAttribute(ValueAttr); + } + else if (element.HasChildNodes) { + // Concatenate the CDATA and Text nodes together + foreach (XmlNode childNode in element.ChildNodes) { + if (childNode.NodeType == XmlNodeType.CDATA || childNode.NodeType == XmlNodeType.Text) { + if (propertyValue == null) { + propertyValue = childNode.InnerText; + } + else { + propertyValue += childNode.InnerText; + } + } + } + } + + if (propertyValue != null) { + if (_environmentVariables != null) { + // Expand environment variables in the string. + propertyValue = OptionConverter.SubstituteVariables(propertyValue, _environmentVariables); + } + + Type parsedObjectConversionTargetType = null; + + // Check if a specific subtype is specified on the element using the 'type' attribute + var subTypeString = element.GetAttribute(TypeAttr); + if (!String.IsNullOrEmpty(subTypeString)) { + // Read the explicit subtype + try { + var subType = SystemInfo.GetTypeFromString(subTypeString, true, true); + + LogLog.Debug("XmlHierarchyConfigurator: Parameter [" + name + "] specified subtype [" + subType.FullName + "]"); + + if (!propertyType.IsAssignableFrom(subType)) { + // Check if there is an appropriate type converter + if (OptionConverter.CanConvertTypeTo(subType, propertyType)) { + // Must re-convert to the real property type + parsedObjectConversionTargetType = propertyType; + + // Use sub type as intermediary type + propertyType = subType; + } + else { + LogLog.Error("XmlHierarchyConfigurator: Subtype [" + subType.FullName + "] set on [" + name + "] is not a subclass of property type [" + propertyType.FullName + "] and there are no acceptable type conversions."); + } + } + else { + // The subtype specified is found and is actually a subtype of the property + // type, therefore we can switch to using this type. + propertyType = subType; + } + } + catch (Exception ex) { + LogLog.Error("XmlHierarchyConfigurator: Failed to find type [" + subTypeString + "] set on [" + name + "]", ex); + } + } + + // Now try to convert the string value to an acceptable type + // to pass to this property. + + var convertedValue = ConvertStringTo(propertyType, propertyValue); + + // Check if we need to do an additional conversion + if (convertedValue != null && parsedObjectConversionTargetType != null) { + LogLog.Debug("XmlHierarchyConfigurator: Performing additional conversion of value from [" + convertedValue.GetType().Name + "] to [" + parsedObjectConversionTargetType.Name + "]"); + convertedValue = OptionConverter.ConvertTypeTo(convertedValue, parsedObjectConversionTargetType); + } + + if (convertedValue != null) { + if (propInfo != null) { + // Got a converted result + LogLog.Debug("XmlHierarchyConfigurator: Setting Property [" + propInfo.Name + "] to " + convertedValue.GetType().Name + " value [" + convertedValue + "]"); + + try { + // Pass to the property + propInfo.SetValue(target, convertedValue, BindingFlags.SetProperty, null, null, CultureInfo.InvariantCulture); + } + catch (TargetInvocationException targetInvocationEx) { + LogLog.Error("XmlHierarchyConfigurator: Failed to set parameter [" + propInfo.Name + "] on object [" + target + "] using value [" + convertedValue + "]", targetInvocationEx.InnerException); + } + } + else if (methInfo != null) { + // Got a converted result + LogLog.Debug("XmlHierarchyConfigurator: Setting Collection Property [" + methInfo.Name + "] to " + convertedValue.GetType().Name + " value [" + convertedValue + "]"); + + try { + // Pass to the property + methInfo.Invoke(target, BindingFlags.InvokeMethod, null, new[] { convertedValue }, CultureInfo.InvariantCulture); + } + catch (TargetInvocationException targetInvocationEx) { + LogLog.Error("XmlHierarchyConfigurator: Failed to set parameter [" + name + "] on object [" + target + "] using value [" + convertedValue + "]", targetInvocationEx.InnerException); + } + } + } + else { + LogLog.Warn("XmlHierarchyConfigurator: Unable to set property [" + name + "] on object [" + target + "] using value [" + propertyValue + "] (with acceptable conversion types)"); + } + } + else { + object createdObject; + + if (propertyType == typeof(string) && !HasAttributesOrElements(element)) { + // If the property is a string and the element is empty (no attributes + // or child elements) then we special case the object value to an empty string. + // This is necessary because while the String is a class it does not have + // a default constructor that creates an empty string, which is the behavior + // we are trying to simulate and would be expected from CreateObjectFromXml + createdObject = ""; + } + else { + // No value specified + Type defaultObjectType = null; + if (IsTypeConstructible(propertyType)) { + defaultObjectType = propertyType; + } + + createdObject = CreateObjectFromXml(element, defaultObjectType, propertyType); + } + + if (createdObject == null) { + LogLog.Error("XmlHierarchyConfigurator: Failed to create object to set param: " + name); + } + else { + if (propInfo != null) { + // Got a converted result + LogLog.Debug("XmlHierarchyConfigurator: Setting Property [" + propInfo.Name + "] to object [" + createdObject + "]"); + + try { + // Pass to the property + propInfo.SetValue(target, createdObject, BindingFlags.SetProperty, null, null, CultureInfo.InvariantCulture); + } + catch (TargetInvocationException targetInvocationEx) { + LogLog.Error("XmlHierarchyConfigurator: Failed to set parameter [" + propInfo.Name + "] on object [" + target + "] using value [" + createdObject + "]", targetInvocationEx.InnerException); + } + } + else if (methInfo != null) { + // Got a converted result + LogLog.Debug("XmlHierarchyConfigurator: Setting Collection Property [" + methInfo.Name + "] to object [" + createdObject + "]"); + + try { + // Pass to the property + methInfo.Invoke(target, BindingFlags.InvokeMethod, null, new[] { createdObject }, CultureInfo.InvariantCulture); + } + catch (TargetInvocationException targetInvocationEx) { + LogLog.Error("XmlHierarchyConfigurator: Failed to set parameter [" + methInfo.Name + "] on object [" + target + "] using value [" + createdObject + "]", targetInvocationEx.InnerException); + } + } + } + } + } + } + + /// + /// Test if an element has no attributes or child elements + /// + /// the element to inspect + /// true if the element has any attributes or child elements, false otherwise + private static bool HasAttributesOrElements(XmlElement element) { + return element.ChildNodes.Cast().Any(node => node.NodeType == XmlNodeType.Attribute || node.NodeType == XmlNodeType.Element); + } + + /// + /// Test if a is constructible with Activator.CreateInstance. + /// + /// the type to inspect + /// true if the type is creatable using a default constructor, false otherwise + private static bool IsTypeConstructible(Type type) { + if (type.IsClass && !type.IsAbstract) { + var defaultConstructor = type.GetConstructor(new Type[0]); + if (defaultConstructor != null && !defaultConstructor.IsAbstract && !defaultConstructor.IsPrivate) { + return true; + } + } + return false; + } + + /// + /// Look for a method on the that matches the supplied + /// + /// the type that has the method + /// the name of the method + /// the method info found + /// + /// + /// The method must be a public instance method on the . + /// The method must be named or "Add" followed by . + /// The method must take a single parameter. + /// + /// + private static MethodInfo FindMethodInfo(Type targetType, string name) { + var requiredMethodNameA = name; + var requiredMethodNameB = "Add" + name; + + var methods = targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + foreach (var methInfo in methods) { + if (!methInfo.IsStatic) { + if (string.Compare(methInfo.Name, requiredMethodNameA, true, CultureInfo.InvariantCulture) == 0 || + string.Compare(methInfo.Name, requiredMethodNameB, true, CultureInfo.InvariantCulture) == 0) { + // Found matching method name + + // Look for version with one arg only + var methParams = methInfo.GetParameters(); + if (methParams.Length == 1) { + return methInfo; + } + } + } + } + return null; + } + + /// + /// Converts a string value to a target type. + /// + /// The type of object to convert the string to. + /// The string value to use as the value of the object. + /// + /// + /// An object of type with value or + /// null when the conversion could not be performed. + /// + /// + protected object ConvertStringTo(Type type, string value) { + // Hack to allow use of Level in property + if (typeof(Level) == type) { + // Property wants a level + var levelValue = _hierarchy.LevelMap[value]; + + if (levelValue == null) { + LogLog.Error("XmlHierarchyConfigurator: Unknown Level Specified [" + value + "]"); + } + + return levelValue; + } + return OptionConverter.ConvertStringTo(type, value); + } + + /// + /// Creates an object as specified in XML. + /// + /// The XML element that contains the definition of the object. + /// The object type to use if not explicitly specified. + /// The type that the returned object must be or must inherit from. + /// The object or null + /// + /// + /// Parse an XML element and create an object instance based on the configuration + /// data. + /// + /// + /// The type of the instance may be specified in the XML. If not + /// specified then the is used + /// as the type. However the type is specified it must support the + /// type. + /// + /// + protected object CreateObjectFromXml(XmlElement element, Type defaultTargetType, Type typeConstraint) { + Type objectType; + + // Get the object type + var objectTypeString = element.GetAttribute(TypeAttr); + if (String.IsNullOrEmpty(objectTypeString)) { + if (defaultTargetType == null) { + LogLog.Error("XmlHierarchyConfigurator: Object type not specified. Cannot create object of type [" + typeConstraint.FullName + "]. Missing Value or Type."); + return null; + } + // Use the default object type + objectType = defaultTargetType; + } + else { + // Read the explicit object type + try { + objectType = SystemInfo.GetTypeFromString(objectTypeString, true, true); + } + catch (Exception ex) { + LogLog.Error("XmlHierarchyConfigurator: Failed to find type [" + objectTypeString + "]", ex); + return null; + } + } + + var requiresConversion = false; + + // Got the object type. Check that it meets the typeConstraint + if (typeConstraint != null) { + if (!typeConstraint.IsAssignableFrom(objectType)) { + // Check if there is an appropriate type converter + if (OptionConverter.CanConvertTypeTo(objectType, typeConstraint)) { + requiresConversion = true; + } + else { + LogLog.Error("XmlHierarchyConfigurator: Object type [" + objectType.FullName + "] is not assignable to type [" + typeConstraint.FullName + "]. There are no acceptable type conversions."); + return null; + } + } + } + + // Create using the default constructor + object createdObject = null; + try { + createdObject = Activator.CreateInstance(objectType); + } + catch (Exception createInstanceEx) { + LogLog.Error("XmlHierarchyConfigurator: Failed to construct object of type [" + objectType.FullName + "] Exception: " + createInstanceEx); + } + + // Set any params on object + foreach (var currentNode in element.ChildNodes.Cast().Where(currentNode => currentNode.NodeType == XmlNodeType.Element)) { + SetParameter((XmlElement)currentNode, createdObject); + } + + // Check if we need to call ActivateOptions + var optionHandler = createdObject as IOptionHandler; + if (optionHandler != null) { + optionHandler.ActivateOptions(); + } + + // Ok object should be initialized + + return requiresConversion ? OptionConverter.ConvertTypeTo(createdObject, typeConstraint) : createdObject; + } + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 0f6ec65c1..6e21aa3e1 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -200,6 +200,10 @@ + + + +