Skip to content

Commit 3c01dcb

Browse files
committed
Fixes: U4-6030
1 parent aa439ca commit 3c01dcb

File tree

3 files changed

+168
-163
lines changed

3 files changed

+168
-163
lines changed

src/umbraco.businesslogic/Application.cs

Lines changed: 112 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ public class Application
2626
internal const string AppConfigFileName = "applications.config";
2727
private static string _appConfig;
2828
private static readonly object Locker = new object();
29+
private static IEnumerable<Application> _allAvailableSections;
30+
private static volatile bool _isInitialized = false;
31+
32+
/// <summary>
33+
/// Initializes the service with all available application plugins
34+
/// </summary>
35+
/// <param name="allAvailableSections">
36+
/// All application plugins found in assemblies
37+
/// </param>
38+
/// <remarks>
39+
/// This is used to populate the app.config file with any applications declared in plugins that don't exist in the file
40+
/// </remarks>
41+
internal static void Initialize(IEnumerable<Application> allAvailableSections)
42+
{
43+
_allAvailableSections = allAvailableSections;
44+
}
2945

3046
/// <summary>
3147
/// gets/sets the application.config file path
@@ -49,52 +65,75 @@ internal static string AppConfigFilePath
4965
/// <summary>
5066
/// The cache storage for all applications
5167
/// </summary>
52-
internal static List<Application> Apps
68+
public static List<Application> GetSections()
5369
{
54-
get
55-
{
56-
return ApplicationContext.Current.ApplicationCache.GetCacheItem(
57-
CacheKeys.ApplicationsCacheKey,
58-
() =>
59-
{
60-
////used for unit tests
61-
//if (_testApps != null)
62-
// return _testApps;
70+
return ApplicationContext.Current.ApplicationCache.GetCacheItem<List<Application>>(
71+
CacheKeys.ApplicationsCacheKey,
72+
() =>
73+
{
74+
////used for unit tests
75+
//if (_testApps != null)
76+
// return _testApps;
6377

64-
var tmp = new List<Application>();
78+
var list = ReadFromXmlAndSort();
6579

66-
try
80+
//On first access we need to do some initialization
81+
if (_isInitialized == false)
82+
{
83+
lock (Locker)
84+
{
85+
if (_isInitialized == false)
6786
{
68-
LoadXml(doc =>
87+
//now we can check the non-volatile flag
88+
if (_allAvailableSections != null)
89+
{
90+
var hasChanges = false;
91+
92+
LoadXml(doc =>
6993
{
70-
foreach (var addElement in doc.Root.Elements("add").OrderBy(x =>
71-
{
72-
var sortOrderAttr = x.Attribute("sortOrder");
73-
return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0;
74-
}))
94+
//Now, load in the xml structure and update it with anything that is not declared there and save the file.
95+
96+
//NOTE: On the first iteration here, it will lazily scan all apps, etc... this is because this ienumerable is lazy
97+
// based on the ApplicationRegistrar - and as noted there this is not an ideal way to do things but were stuck like this
98+
// currently because of the legacy assemblies and types not in the Core.
99+
100+
//Get all the trees not registered in the config
101+
var unregistered = _allAvailableSections
102+
.Where(x => list.Any(l => l.alias == x.alias) == false)
103+
.ToArray();
104+
105+
hasChanges = unregistered.Any();
106+
107+
var count = 0;
108+
foreach (var attr in unregistered)
75109
{
76-
var sortOrderAttr = addElement.Attribute("sortOrder");
77-
tmp.Add(new Application(addElement.Attribute("name").Value,
78-
addElement.Attribute("alias").Value,
79-
addElement.Attribute("icon").Value,
80-
sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0));
110+
doc.Root.Add(new XElement("add",
111+
new XAttribute("alias", attr.alias),
112+
new XAttribute("name", attr.name),
113+
new XAttribute("icon", attr.icon),
114+
new XAttribute("sortOrder", attr.sortOrder)));
115+
count++;
81116
}
82117

83-
}, false);
84-
return tmp;
85-
}
86-
catch
87-
{
88-
//this is a bit of a hack that just ensures the application doesn't crash when the
89-
//installer is run and there is no database or connection string defined.
90-
//the reason this method may get called during the installation is that the
91-
//SqlHelper of this class is shared amongst everything "Application" wide.
118+
//don't save if there's no changes
119+
return count > 0;
120+
}, true);
92121

93-
//TODO: Perhaps we should log something here??
94-
return null;
122+
if (hasChanges)
123+
{
124+
//If there were changes, we need to re-read the structures from the XML
125+
list = ReadFromXmlAndSort();
126+
}
127+
}
128+
129+
_isInitialized = true;
95130
}
96-
});
97-
}
131+
}
132+
}
133+
134+
return list;
135+
136+
});
98137
}
99138

100139
/// <summary>
@@ -200,7 +239,7 @@ public Application(string name, string alias, string icon, int sortOrder)
200239
[MethodImpl(MethodImplOptions.Synchronized)]
201240
public static void MakeNew(string name, string alias, string icon)
202241
{
203-
MakeNew(name, alias, icon, Apps.Max(x => x.sortOrder) + 1);
242+
MakeNew(name, alias, icon, GetSections().Max(x => x.sortOrder) + 1);
204243
}
205244

206245
/// <summary>
@@ -224,6 +263,9 @@ public static void MakeNew(string name, string alias, string icon, int sortOrder
224263
new XAttribute("name", name),
225264
new XAttribute("icon", icon),
226265
new XAttribute("sortOrder", sortOrder)));
266+
267+
return true;
268+
227269
}, true);
228270

229271
//raise event
@@ -238,7 +280,7 @@ public static void MakeNew(string name, string alias, string icon, int sortOrder
238280
/// <returns></returns>
239281
public static Application getByAlias(string appAlias)
240282
{
241-
return Apps.Find(t => t.alias == appAlias);
283+
return GetSections().Find(t => t.alias == appAlias);
242284
}
243285

244286
/// <summary>
@@ -259,6 +301,9 @@ public void Delete()
259301
LoadXml(doc =>
260302
{
261303
doc.Root.Elements("add").Where(x => x.Attribute("alias") != null && x.Attribute("alias").Value == this.alias).Remove();
304+
305+
return true;
306+
262307
}, true);
263308

264309
//raise event
@@ -271,7 +316,7 @@ public void Delete()
271316
/// <returns>Returns a Application Array</returns>
272317
public static List<Application> getAll()
273318
{
274-
return Apps;
319+
return GetSections();
275320
}
276321

277322
/// <summary>
@@ -282,8 +327,32 @@ public static void RegisterIApplications()
282327
{
283328
ApplicationStartupHandler.RegisterHandlers();
284329
}
285-
286-
internal static void LoadXml(Action<XDocument> callback, bool saveAfterCallback)
330+
331+
private static List<Application> ReadFromXmlAndSort()
332+
{
333+
var tmp = new List<Application>();
334+
335+
LoadXml(doc =>
336+
{
337+
foreach (var addElement in doc.Root.Elements("add").OrderBy(x =>
338+
{
339+
var sortOrderAttr = x.Attribute("sortOrder");
340+
return sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0;
341+
}))
342+
{
343+
var sortOrderAttr = addElement.Attribute("sortOrder");
344+
tmp.Add(new Application(addElement.Attribute("name").Value,
345+
addElement.Attribute("alias").Value,
346+
addElement.Attribute("icon").Value,
347+
sortOrderAttr != null ? Convert.ToInt32(sortOrderAttr.Value) : 0));
348+
}
349+
return false;
350+
}, false);
351+
352+
return tmp;
353+
}
354+
355+
internal static void LoadXml(Func<XDocument, bool> callback, bool saveAfterCallbackIfChanged)
287356
{
288357
lock (Locker)
289358
{
@@ -293,9 +362,9 @@ internal static void LoadXml(Action<XDocument> callback, bool saveAfterCallback)
293362

294363
if (doc.Root != null)
295364
{
296-
callback.Invoke(doc);
365+
var changed = callback.Invoke(doc);
297366

298-
if (saveAfterCallback)
367+
if (saveAfterCallbackIfChanged && changed)
299368
{
300369
//ensure the folder is created!
301370
Directory.CreateDirectory(Path.GetDirectoryName(AppConfigFilePath));
Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
24
using System.Configuration;
35
using System.Data.SqlClient;
46
using System.Linq;
@@ -12,8 +14,21 @@
1214

1315
namespace umbraco.BusinessLogic
1416
{
15-
public class ApplicationRegistrar : IApplicationStartupHandler
17+
/// <summary>
18+
/// A startup handler for putting the app config in the config file based on attributes found
19+
/// </summary>
20+
/// /// <remarks>
21+
/// 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.
22+
/// 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.
23+
/// </remarks>
24+
public class ApplicationRegistrar : ApplicationEventHandler
1625
{
26+
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
27+
{
28+
//initialize the section service with a lazy collection of found app plugins
29+
Application.Initialize(new LazyEnumerableSections());
30+
}
31+
1732
private ISqlHelper _sqlHelper;
1833
protected ISqlHelper SqlHelper
1934
{
@@ -32,55 +47,51 @@ protected ISqlHelper SqlHelper
3247
}
3348
}
3449

35-
public ApplicationRegistrar()
50+
/// <summary>
51+
/// This class is here so that we can provide lazy access to tree scanning for when it is needed
52+
/// </summary>
53+
private class LazyEnumerableSections : IEnumerable<Application>
3654
{
55+
public LazyEnumerableSections()
56+
{
57+
_lazySections = new Lazy<IEnumerable<Application>>(() =>
58+
{
59+
// Load all Applications by attribute and add them to the XML config
60+
var types = PluginManager.Current.ResolveApplications();
3761

38-
//don't do anything if the application is not configured!
39-
if (!ApplicationContext.Current.IsConfigured)
40-
return;
41-
42-
// Load all Applications by attribute and add them to the XML config
43-
var types = PluginManager.Current.ResolveApplications();
62+
//since applications don't populate their metadata from the attribute and because it is an interface,
63+
//we need to interrogate the attributes for the data. Would be better to have a base class that contains
64+
//metadata populated by the attribute. Oh well i guess.
65+
var attrs = types.Select(x => x.GetCustomAttributes<ApplicationAttribute>(false).Single());
66+
return attrs.Select(x => new Application(x.Name, x.Alias, x.Icon, x.SortOrder)).ToArray();
67+
});
68+
}
4469

45-
//since applications don't populate their metadata from the attribute and because it is an interface,
46-
//we need to interrogate the attributes for the data. Would be better to have a base class that contains
47-
//metadata populated by the attribute. Oh well i guess.
48-
var attrs = types.Select(x => x.GetCustomAttributes<ApplicationAttribute>(false).Single())
49-
.Where(x => Application.getByAlias(x.Alias) == null);
70+
private readonly Lazy<IEnumerable<Application>> _lazySections;
5071

51-
var allAliases = Application.getAll().Select(x => x.alias).Concat(attrs.Select(x => x.Alias));
52-
var inString = "'" + string.Join("','", allAliases) + "'";
53-
54-
Application.LoadXml(doc =>
55-
{
56-
foreach (var attr in attrs)
57-
{
58-
doc.Root.Add(new XElement("add",
59-
new XAttribute("alias", attr.Alias),
60-
new XAttribute("name", attr.Name),
61-
new XAttribute("icon", attr.Icon),
62-
new XAttribute("sortOrder", attr.SortOrder)));
63-
}
64-
65-
var db = ApplicationContext.Current.DatabaseContext.Database;
66-
var exist = db.TableExist("umbracoApp");
67-
if (exist)
68-
{
69-
var dbApps = SqlHelper.ExecuteReader("SELECT * FROM umbracoApp WHERE appAlias NOT IN (" + inString + ")");
70-
while (dbApps.Read())
71-
{
72-
doc.Root.Add(new XElement("add",
73-
new XAttribute("alias", dbApps.GetString("appAlias")),
74-
new XAttribute("name", dbApps.GetString("appName")),
75-
new XAttribute("icon", dbApps.GetString("appIcon")),
76-
new XAttribute("sortOrder", dbApps.GetByte("sortOrder"))));
77-
}
78-
}
72+
/// <summary>
73+
/// Returns an enumerator that iterates through the collection.
74+
/// </summary>
75+
/// <returns>
76+
/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
77+
/// </returns>
78+
public IEnumerator<Application> GetEnumerator()
79+
{
80+
return _lazySections.Value.GetEnumerator();
81+
}
7982

80-
}, true);
81-
82-
//TODO Shouldn't this be enabled and then delete the whole table?
83-
//SqlHelper.ExecuteNonQuery("DELETE FROM umbracoApp");
83+
/// <summary>
84+
/// Returns an enumerator that iterates through a collection.
85+
/// </summary>
86+
/// <returns>
87+
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
88+
/// </returns>
89+
IEnumerator IEnumerable.GetEnumerator()
90+
{
91+
return GetEnumerator();
92+
}
8493
}
94+
95+
8596
}
8697
}

0 commit comments

Comments
 (0)