diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index ca6bb122d1..5278990d6a 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -18,16 +18,16 @@ namespace Umbraco.Core.Services private readonly CacheHelper _cache; private IEnumerable _allAvailableTrees; private volatile bool _isInitialized = false; - private readonly object _locker = new object(); + internal const string TreeConfigFileName = "trees.config"; + private static string _treeConfig; + private static readonly object Locker = new object(); public ApplicationTreeService(CacheHelper cache) { _cache = cache; } - internal const string TreeConfigFileName = "trees.config"; - private static string _treeConfig; - private static readonly object Locker = new object(); + /// /// gets/sets the trees.config file path diff --git a/src/Umbraco.Core/Services/ISectionService.cs b/src/Umbraco.Core/Services/ISectionService.cs index bc950b9470..df5c89d4bf 100644 --- a/src/Umbraco.Core/Services/ISectionService.cs +++ b/src/Umbraco.Core/Services/ISectionService.cs @@ -6,10 +6,15 @@ namespace Umbraco.Core.Services public interface ISectionService { /// - /// Ensures all available sections exist in the storage medium + /// Initializes the service with all available application plugins /// - /// - void Initialize(IEnumerable
existingSections); + /// + /// All application plugins found in assemblies + /// + /// + /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file + /// + void Initialize(IEnumerable
allAvailableSections); /// /// The cache storage for all applications diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index 00fa28e38c..1a3568cff3 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -17,12 +17,19 @@ namespace Umbraco.Core.Services internal class SectionService : ISectionService { private readonly IUserService _userService; + private IEnumerable
_allAvailableSections; private readonly IApplicationTreeService _applicationTreeService; + private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly CacheHelper _cache; + internal const string AppConfigFileName = "applications.config"; + private static string _appConfig; + private volatile bool _isInitialized = false; + private static readonly object Locker = new object(); public SectionService( IUserService userService, - IApplicationTreeService applicationTreeService, + IApplicationTreeService applicationTreeService, + IDatabaseUnitOfWorkProvider uowProvider, CacheHelper cache) { if (applicationTreeService == null) throw new ArgumentNullException("applicationTreeService"); @@ -30,12 +37,11 @@ namespace Umbraco.Core.Services _userService = userService; _applicationTreeService = applicationTreeService; + _uowProvider = uowProvider; _cache = cache; } - internal const string AppConfigFileName = "applications.config"; - private static string _appConfig; - private static readonly object Locker = new object(); + /// /// gets/sets the application.config file path @@ -57,22 +63,17 @@ namespace Umbraco.Core.Services } /// - /// Ensures all available sections exist in the storage medium + /// Initializes the service with all available application plugins /// - /// - public void Initialize(IEnumerable
existingSections) + /// + /// All application plugins found in assemblies + /// + /// + /// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file + /// + public void Initialize(IEnumerable
allAvailableSections) { - LoadXml(doc => - { - foreach (var attr in existingSections) - { - doc.Root.Add(new XElement("add", - new XAttribute("alias", attr.Alias), - new XAttribute("name", attr.Name), - new XAttribute("icon", attr.Icon), - new XAttribute("sortOrder", attr.SortOrder))); - } - }, true); + _allAvailableSections = allAvailableSections; } /// @@ -80,7 +81,7 @@ namespace Umbraco.Core.Services /// public IEnumerable
GetSections() { - return _cache.GetCacheItem( + return _cache.RuntimeCache.GetCacheItem>( CacheKeys.ApplicationsCacheKey, () => { @@ -88,28 +89,66 @@ namespace Umbraco.Core.Services //if (_testApps != null) // return _testApps; - var tmp = new List
(); + var list = ReadFromXmlAndSort(); - LoadXml(doc => + //On first access we need to do some initialization + if (_isInitialized == false) + { + lock (Locker) { - foreach (var addElement in doc.Root.Elements("add").OrderBy(x => - { - var sortOrderAttr = x.Attribute("sortOrder"); - return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0; - })) + if (_isInitialized == false) { - var sortOrderAttr = addElement.Attribute("sortOrder"); - tmp.Add(new Section(addElement.Attribute("name").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("icon").Value, - sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + //now we can check the non-volatile flag + if (_allAvailableSections != null) + { + var hasChanges = false; + + LoadXml(doc => + { + //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 apps, etc... this is because this ienumerable is lazy + // based on the ApplicationRegistrar - 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 = _allAvailableSections + .Where(x => list.Any(l => l.Alias == x.Alias) == false) + .ToArray(); + + hasChanges = unregistered.Any(); + + var count = 0; + foreach (var attr in unregistered) + { + doc.Root.Add(new XElement("add", + new XAttribute("alias", attr.Alias), + new XAttribute("name", attr.Name), + new XAttribute("icon", attr.Icon), + new XAttribute("sortOrder", attr.SortOrder))); + 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 tmp; + } + } + + return list; + }); } - internal void LoadXml(Action callback, bool saveAfterCallback) + internal void LoadXml(Func callback, bool saveAfterCallbackIfChanged) { lock (Locker) { @@ -119,9 +158,9 @@ namespace Umbraco.Core.Services if (doc.Root != null) { - callback.Invoke(doc); + var changed = callback.Invoke(doc); - if (saveAfterCallback) + if (saveAfterCallbackIfChanged && changed) { //ensure the folder is created! Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath)); @@ -194,6 +233,7 @@ namespace Umbraco.Core.Services new XAttribute("name", name), new XAttribute("icon", icon), new XAttribute("sortOrder", sortOrder))); + return true; }, true); //raise event @@ -209,7 +249,7 @@ namespace Umbraco.Core.Services lock (Locker) { //delete the assigned applications - ApplicationContext.Current.DatabaseContext.Database.Execute( + _uowProvider.GetUnitOfWork().Database.Execute( "delete from umbracoUser2App where app = @appAlias", new { appAlias = section.Alias }); @@ -222,7 +262,10 @@ namespace Umbraco.Core.Services LoadXml(doc => { - doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias).Remove(); + doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == section.Alias) + .Remove(); + + return true; }, true); //raise event @@ -230,6 +273,30 @@ namespace Umbraco.Core.Services } } + private List
ReadFromXmlAndSort() + { + var tmp = new List
(); + + 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 sortOrderAttr = addElement.Attribute("sortOrder"); + tmp.Add(new Section(addElement.Attribute("name").Value, + addElement.Attribute("alias").Value, + addElement.Attribute("icon").Value, + sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0)); + } + return false; + }, false); + + return tmp; + } + internal static event TypedEventHandler Deleted; private static void OnDeleted(Section app, EventArgs args) { diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 375e1ca1ba..a6e74b00d0 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -162,7 +162,7 @@ namespace Umbraco.Core.Services _treeService = new Lazy(() => new ApplicationTreeService(cache)); if (_sectionService == null) - _sectionService = new Lazy(() => new SectionService(_userService.Value, _treeService.Value, cache)); + _sectionService = new Lazy(() => new SectionService(_userService.Value, _treeService.Value, provider, cache)); if (_macroService == null) _macroService = new Lazy(() => new MacroService(provider, repositoryFactory.Value)); diff --git a/src/umbraco.businesslogic/ApplicationRegistrar.cs b/src/umbraco.businesslogic/ApplicationRegistrar.cs index dbbf3249cf..4c284653d8 100644 --- a/src/umbraco.businesslogic/ApplicationRegistrar.cs +++ b/src/umbraco.businesslogic/ApplicationRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Configuration; using System.Data.SqlClient; using System.Linq; @@ -16,23 +18,26 @@ using umbraco.interfaces; namespace umbraco.BusinessLogic { + /// + /// A startup handler for putting the app config in the config file based on attributes found + /// + /// /// + /// TODO: This is really not a very ideal process but the code is found here because tree plugins are in the Web project or the legacy business logic project. + /// Moving forward we can put the base tree plugin classes in the core and then this can all just be taken care of normally within the service. + /// public class ApplicationRegistrar : ApplicationEventHandler, IMapperConfiguration { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - // Load all Applications by attribute and add them to the XML config - var types = PluginManager.Current.ResolveApplications(); - - //since applications don't populate their metadata from the attribute and because it is an interface, - //we need to interrogate the attributes for the data. Would be better to have a base class that contains - //metadata populated by the attribute. Oh well i guess. - var attrs = types.Select(x => x.GetCustomAttributes(false).Single()) - .Where(x => applicationContext.Services.SectionService.GetByAlias(x.Alias) == null) - .ToArray(); - - applicationContext.Services.SectionService.Initialize(attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder))); + //initialize the section service with a lazy collection of found app plugins + applicationContext.Services.SectionService.Initialize(new LazyEnumerableSections()); } + /// + /// Configure a few auto-mappings + /// + /// + /// public void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() @@ -46,5 +51,50 @@ namespace umbraco.BusinessLogic .ForMember(x => x.Name, expression => expression.MapFrom(x => x.name)) .ForMember(x => x.SortOrder, expression => expression.MapFrom(x => x.sortOrder)); } + + /// + /// This class is here so that we can provide lazy access to tree scanning for when it is needed + /// + private class LazyEnumerableSections : IEnumerable
+ { + public LazyEnumerableSections() + { + _lazySections = new Lazy>(() => + { + // Load all Applications by attribute and add them to the XML config + var types = PluginManager.Current.ResolveApplications(); + + //since applications don't populate their metadata from the attribute and because it is an interface, + //we need to interrogate the attributes for the data. Would be better to have a base class that contains + //metadata populated by the attribute. Oh well i guess. + var attrs = types.Select(x => x.GetCustomAttributes(false).Single()); + return attrs.Select(x => new Section(x.Name, x.Alias, x.Icon, x.SortOrder)).ToArray(); + }); + } + + private readonly Lazy> _lazySections; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator
GetEnumerator() + { + return _lazySections.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