From aa439cacda01de5762df44109f4933d5ee5c0512 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Dec 2014 14:58:49 +1100 Subject: [PATCH] working on U4-6030 --- src/umbraco.businesslogic/ApplicationTree.cs | 213 ++++++++++++++---- .../ApplicationTreeRegistrar.cs | 204 ++++++++++++----- 2 files changed, 307 insertions(+), 110 deletions(-) diff --git a/src/umbraco.businesslogic/ApplicationTree.cs b/src/umbraco.businesslogic/ApplicationTree.cs index f8e51546f7..44a57ecbd9 100644 --- a/src/umbraco.businesslogic/ApplicationTree.cs +++ b/src/umbraco.businesslogic/ApplicationTree.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Events; using Umbraco.Core.IO; +using Umbraco.Core.Logging; using umbraco.DataLayer; namespace umbraco.BusinessLogic @@ -24,6 +25,23 @@ namespace umbraco.BusinessLogic internal const string TreeConfigFileName = "trees.config"; private static string _treeConfig; private static readonly object Locker = new object(); + private static volatile bool _isInitialized = false; + private static IEnumerable _allAvailableTrees; + + /// + /// Initializes the service with any trees found in plugins + /// + /// + /// A collection of all available tree found in assemblies in the application + /// + /// + /// This will update the trees.config with the found tree plugins that are not currently listed in the file when the first + /// access is made to resolve the tree collection + /// + internal static void Intitialize(IEnumerable allAvailableTrees) + { + _allAvailableTrees = allAvailableTrees; + } /// /// gets/sets the trees.config file path @@ -45,58 +63,136 @@ namespace umbraco.BusinessLogic } /// - /// The cache storage for all application trees + /// The main entry point to get application trees /// - private static List AppTrees + /// + /// This lazily on first access will scan for plugin trees and ensure the trees.config is up-to-date with the plugins. If plugins + /// haven't changed on disk then the file will not be saved. The trees are all then loaded from this config file into cache and returned. + /// + private static List GetAppTrees() { - get - { - return ApplicationContext.Current.ApplicationCache.GetCacheItem( - CacheKeys.ApplicationTreeCacheKey, - () => + return ApplicationContext.Current.ApplicationCache.GetCacheItem>( + CacheKeys.ApplicationTreeCacheKey, + () => + { + var list = ReadFromXmlAndSort(); + + //On first access we need to do some initialization + if (_isInitialized == false) + { + lock (Locker) { - var list = new List(); - - LoadXml(doc => + if (_isInitialized == false) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + //now we can check the non-volatile flag + if (_allAvailableTrees != null) { + var hasChanges = false; - var applicationAlias = (string)addElement.Attribute("application"); - var type = (string)addElement.Attribute("type"); - var assembly = (string)addElement.Attribute("assembly"); - - //check if the tree definition (applicationAlias + type + assembly) is already in the list - - if (!list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) - && tree.Type.InvariantEquals(type) - && tree.AssemblyName.InvariantEquals(assembly))) + LoadXml(doc => { - list.Add(new ApplicationTree( - addElement.Attribute("silent") != null ? Convert.ToBoolean(addElement.Attribute("silent").Value) : false, - addElement.Attribute("initialize") != null ? Convert.ToBoolean(addElement.Attribute("initialize").Value) : true, - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - (string)addElement.Attribute("assembly"), //this could be empty: http://issues.umbraco.org/issue/U4-1360 - addElement.Attribute("type").Value, - addElement.Attribute("action") != null ? addElement.Attribute("action").Value : "")); + //Now, load in the xml structure and update it with anything that is not declared there and save the file. + + //NOTE: On the first iteration here, it will lazily scan all trees, etc... this is because this ienumerable is lazy + // based on the ApplicationTreeRegistrar - and as noted there this is not an ideal way to do things but were stuck like this + // currently because of the legacy assemblies and types not in the Core. + + //Get all the trees not registered in the config + var unregistered = _allAvailableTrees + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + if (hasChanges == false) return false; + + //add the unregistered ones to the list and re-save the file if any changes were found + var count = 0; + foreach (var tree in unregistered) + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("title", tree.Title), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + count++; + } + + //don't save if there's no changes + return count > 0; + }, true); + + if (hasChanges) + { + //If there were changes, we need to re-read the structures from the XML + list = ReadFromXmlAndSort(); } - - } - }, false); - return list; - }); - } + _isInitialized = true; + } + } + } + + + return list; + + + }); + } + + private static List ReadFromXmlAndSort() + { + var list = new List(); + + //read in the xml file containing trees and convert them all to ApplicationTree instances + LoadXml(doc => + { + foreach (var addElement in doc.Root.Elements("add").OrderBy(x => + { + var sortOrderAttr = x.Attribute("sortOrder"); + return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; + })) + { + var applicationAlias = (string)addElement.Attribute("application"); + var type = (string)addElement.Attribute("type"); + var assembly = (string)addElement.Attribute("assembly"); + + var clrType = System.Type.GetType(type); + if (clrType == null) + { + LogHelper.Warn(typeof(ApplicationTree), "The tree definition: " + addElement.ToString() + " could not be resolved to a .Net object type"); + continue; + } + + //check if the tree definition (applicationAlias + type + assembly) is already in the list + + if (list.Any(tree => tree.ApplicationAlias.InvariantEquals(applicationAlias) && tree.GetRuntimeType() == clrType) == false) + { + list.Add(new ApplicationTree( + addElement.Attribute("silent") != null && Convert.ToBoolean(addElement.Attribute("silent").Value), + addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), + addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, + addElement.Attribute("application").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("title").Value, + addElement.Attribute("iconClosed").Value, + addElement.Attribute("iconOpen").Value, + (string)addElement.Attribute("assembly"), //this could be empty: http://issues.umbraco.org/issue/U4-1360 + addElement.Attribute("type").Value, + addElement.Attribute("action") != null ? addElement.Attribute("action").Value : "")); + } + } + + return false; + + }, false); + + return list; } /// @@ -256,6 +352,9 @@ namespace umbraco.BusinessLogic new XAttribute("type", type), new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); } + + return true; + }, true); OnNew(new ApplicationTree(silent, initialize, sortOrder, applicationAlias, alias, title, iconClosed, iconOpened, assemblyName, type, action), new EventArgs()); @@ -287,6 +386,8 @@ namespace umbraco.BusinessLogic el.Add(new XAttribute("action", string.IsNullOrEmpty(this.Action) ? "" : this.Action)); } + return true; + }, true); OnUpdated(this, new EventArgs()); @@ -304,6 +405,9 @@ namespace umbraco.BusinessLogic { doc.Root.Elements("add").Where(x => x.Attribute("application") != null && x.Attribute("application").Value == this.ApplicationAlias && x.Attribute("alias") != null && x.Attribute("alias").Value == this.Alias).Remove(); + + return true; + }, true); OnDeleted(this, new EventArgs()); @@ -317,7 +421,7 @@ namespace umbraco.BusinessLogic /// An ApplicationTree instance public static ApplicationTree getByAlias(string treeAlias) { - return AppTrees.Find(t => (t.Alias == treeAlias)); + return GetAppTrees().Find(t => (t.Alias == treeAlias)); } @@ -327,7 +431,7 @@ namespace umbraco.BusinessLogic /// Returns a ApplicationTree Array public static ApplicationTree[] getAll() { - return AppTrees.OrderBy(x => x.SortOrder).ToArray(); + return GetAppTrees().OrderBy(x => x.SortOrder).ToArray(); } /// @@ -348,7 +452,7 @@ namespace umbraco.BusinessLogic /// Returns a ApplicationTree Array public static ApplicationTree[] getApplicationTree(string applicationAlias, bool onlyInitializedApplications) { - var list = AppTrees.FindAll( + var list = GetAppTrees().FindAll( t => { if (onlyInitializedApplications) @@ -360,21 +464,34 @@ namespace umbraco.BusinessLogic return list.OrderBy(x => x.SortOrder).ToArray(); } - internal static void LoadXml(Action callback, bool saveAfterCallback) + /// + /// Loads in the xml structure from disk if one is found, otherwise loads in an empty xml structure, calls the + /// callback with the xml document and saves the structure back to disk if saveAfterCallback is true. + /// + /// + /// + internal static void LoadXml(Func callback, bool saveAfterCallbackIfChanges) { lock (Locker) { var doc = File.Exists(TreeConfigFilePath) ? XDocument.Load(TreeConfigFilePath) : XDocument.Parse(""); + if (doc.Root != null) { - callback.Invoke(doc); + var hasChanges = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanges && hasChanges + //Don't save it if it is empty, in some very rare cases if the app domain get's killed in the middle of this process + // in some insane way the file saved will be empty. I'm pretty sure it's not actually anything to do with the xml doc and + // more about the IO trying to save the XML doc, but it doesn't hurt to check. + && doc.Root != null && doc.Root.Elements().Any()) { + //ensures the folder exists Directory.CreateDirectory(Path.GetDirectoryName(TreeConfigFilePath)); + //saves it doc.Save(TreeConfigFilePath); //remove the cache now that it has changed SD: I'm leaving this here even though it diff --git a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs index e12eb2f21b..f9f83f3bbf 100644 --- a/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationTreeRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Umbraco.Core; @@ -9,81 +11,159 @@ using umbraco.interfaces; namespace umbraco.BusinessLogic { - public class ApplicationTreeRegistrar : IApplicationStartupHandler + public class ApplicationTreeRegistrar : ApplicationEventHandler { - public ApplicationTreeRegistrar() + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //don't do anything if the application or database is not configured! - if (ApplicationContext.Current == null - || !ApplicationContext.Current.IsConfigured - || !ApplicationContext.Current.DatabaseContext.IsDatabaseConfigured) - return; + //Call initialize on the tree service with the lazy enumerable class below, when the tree service needs to resolve, + // it will lazily do the scanning and comparing so it's not actually done on app start. + ApplicationTree.Intitialize(new LazyEnumerableTrees()); + } - // Load all Trees by attribute and add them to the XML config - var types = PluginManager.Current.ResolveAttributedTrees(); + //public ApplicationTreeRegistrar() + //{ + // //don't do anything if the application or database is not configured! + // if (ApplicationContext.Current == null + // || !ApplicationContext.Current.IsConfigured + // || !ApplicationContext.Current.DatabaseContext.IsDatabaseConfigured) + // return; - var items = types - .Select(x => - new Tuple(x, x.GetCustomAttributes(false).Single())) - .Where(x => ApplicationTree.getByAlias(x.Item2.Alias) == null); + // // Load all Trees by attribute and add them to the XML config + // var types = PluginManager.Current.ResolveAttributedTrees(); - var allAliases = ApplicationTree.getAll().Select(x => x.Alias).Concat(items.Select(x => x.Item2.Alias)); - var inString = "'" + string.Join("','", allAliases) + "'"; + // var items = types + // .Select(x => + // new Tuple(x, x.GetCustomAttributes(false).Single())) + // .Where(x => ApplicationTree.getByAlias(x.Item2.Alias) == null); - ApplicationTree.LoadXml(doc => + // var allAliases = ApplicationTree.getAll().Select(x => x.Alias).Concat(items.Select(x => x.Item2.Alias)); + // var inString = "'" + string.Join("','", allAliases) + "'"; + + // ApplicationTree.LoadXml(doc => + // { + // foreach (var tuple in items) + // { + // var type = tuple.Item1; + // var attr = tuple.Item2; + + // //Add the new tree that doesn't exist in the config that was found by type finding + + // doc.Root.Add(new XElement("add", + // new XAttribute("silent", attr.Silent), + // new XAttribute("initialize", attr.Initialize), + // new XAttribute("sortOrder", attr.SortOrder), + // new XAttribute("alias", attr.Alias), + // new XAttribute("application", attr.ApplicationAlias), + // new XAttribute("title", attr.Title), + // new XAttribute("iconClosed", attr.IconClosed), + // new XAttribute("iconOpen", attr.IconOpen), + // // don't add the assembly, we don't need this: + // // http://issues.umbraco.org/issue/U4-1360 + // //new XAttribute("assembly", assemblyName), + // //new XAttribute("type", typeName), + // // instead, store the assembly type name + // new XAttribute("type", type.GetFullNameWithAssembly()), + // new XAttribute("action", attr.Action))); + // } + + // //add any trees that were found in the database that don't exist in the config + + // var db = ApplicationContext.Current.DatabaseContext.Database; + // var exist = db.TableExist("umbracoAppTree"); + // if (exist) + // { + // var appTrees = db.Fetch("WHERE treeAlias NOT IN (" + inString + ")"); + // foreach (var appTree in appTrees) + // { + // var action = appTree.Action; + + // doc.Root.Add(new XElement("add", + // new XAttribute("silent", appTree.Silent), + // new XAttribute("initialize", appTree.Initialize), + // new XAttribute("sortOrder", appTree.SortOrder), + // new XAttribute("alias", appTree.Alias), + // new XAttribute("application", appTree.AppAlias), + // new XAttribute("title", appTree.Title), + // new XAttribute("iconClosed", appTree.IconClosed), + // new XAttribute("iconOpen", appTree.IconOpen), + // new XAttribute("assembly", appTree.HandlerAssembly), + // new XAttribute("type", appTree.HandlerType), + // new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); + // } + // } + + // }, true); + //} + + /// + /// This class is here so that we can provide lazy access to tree scanning for when it is needed + /// + private class LazyEnumerableTrees : IEnumerable + { + public LazyEnumerableTrees() { - foreach (var tuple in items) + _lazyTrees = new Lazy>(() => { - var type = tuple.Item1; - var attr = tuple.Item2; - - //Add the new tree that doesn't exist in the config that was found by type finding + var added = new List(); - doc.Root.Add(new XElement("add", - new XAttribute("silent", attr.Silent), - new XAttribute("initialize", attr.Initialize), - new XAttribute("sortOrder", attr.SortOrder), - new XAttribute("alias", attr.Alias), - new XAttribute("application", attr.ApplicationAlias), - new XAttribute("title", attr.Title), - new XAttribute("iconClosed", attr.IconClosed), - new XAttribute("iconOpen", attr.IconOpen), - // don't add the assembly, we don't need this: - // http://issues.umbraco.org/issue/U4-1360 - //new XAttribute("assembly", assemblyName), - //new XAttribute("type", typeName), - // instead, store the assembly type name - new XAttribute("type", type.GetFullNameWithAssembly()), - new XAttribute("action", attr.Action))); - } + // Load all Controller Trees by attribute + var types = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var items = types + .Select(x => + new Tuple(x, x.GetCustomAttributes(false).Single())) + .Select(x => new ApplicationTree( + x.Item2.Silent, x.Item2.Initialize, (byte) x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, + "", + x.Item1.GetFullNameWithAssembly(), + x.Item2.Action)) + .ToArray(); - //add any trees that were found in the database that don't exist in the config + added.AddRange(items.Select(x => x.Alias)); - var db = ApplicationContext.Current.DatabaseContext.Database; - var exist = db.TableExist("umbracoAppTree"); - if (exist) - { - var appTrees = db.Fetch("WHERE treeAlias NOT IN (" + inString + ")"); - foreach (var appTree in appTrees) - { - var action = appTree.Action; + //find the legacy trees + var legacyTreeTypes = PluginManager.Current.ResolveAttributedTrees(); + //convert them to ApplicationTree instances + var legacyItems = legacyTreeTypes + .Select(x => + new Tuple( + x, + x.GetCustomAttributes(false).SingleOrDefault())) + .Where(x => x.Item2 != null) + //make sure the legacy tree isn't added on top of the controller tree! + .Where(x => added.InvariantContains(x.Item2.Alias) == false) + .Select(x => new ApplicationTree(x.Item2.Silent, x.Item2.Initialize, (byte) x.Item2.SortOrder, x.Item2.ApplicationAlias, x.Item2.Alias, x.Item2.Title, x.Item2.IconClosed, x.Item2.IconOpen, + "", + x.Item1.GetFullNameWithAssembly(), + x.Item2.Action)); - doc.Root.Add(new XElement("add", - new XAttribute("silent", appTree.Silent), - new XAttribute("initialize", appTree.Initialize), - new XAttribute("sortOrder", appTree.SortOrder), - new XAttribute("alias", appTree.Alias), - new XAttribute("application", appTree.AppAlias), - new XAttribute("title", appTree.Title), - new XAttribute("iconClosed", appTree.IconClosed), - new XAttribute("iconOpen", appTree.IconOpen), - new XAttribute("assembly", appTree.HandlerAssembly), - new XAttribute("type", appTree.HandlerType), - new XAttribute("action", string.IsNullOrEmpty(action) ? "" : action))); - } - } + return items.Concat(legacyItems).ToArray(); + }); + } - }, true); + private readonly Lazy> _lazyTrees; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _lazyTrees.Value.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } } } } \ No newline at end of file