using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LightInject;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
namespace Umbraco.Core.Components
{
// note: this class is NOT thread-safe in any ways
internal class BootLoader
{
private readonly IServiceContainer _container;
private readonly ProfilingLogger _proflog;
private IUmbracoComponent[] _components;
private bool _booted;
private const int LogThresholdMilliseconds = 100;
///
/// Initializes a new instance of the class.
///
/// The application container.
public BootLoader(IServiceContainer container)
{
if (container == null) throw new ArgumentNullException(nameof(container));
_container = container;
_proflog = container.GetInstance();
}
private class EnableInfo
{
public bool Enabled;
public int Weight = -1;
}
public void Boot(IEnumerable componentTypes, RuntimeLevel level)
{
if (_booted) throw new InvalidOperationException("Can not boot, has already booted.");
var orderedComponentTypes = PrepareComponentTypes(componentTypes, level);
InstanciateComponents(orderedComponentTypes);
ComposeComponents(level);
InitializeComponents();
// rejoice!
_booted = true;
}
private IEnumerable PrepareComponentTypes(IEnumerable componentTypes, RuntimeLevel level)
{
using (_proflog.DebugDuration("Preparing component types.", "Prepared component types."))
{
return PrepareComponentTypes2(componentTypes, level);
}
}
private static IEnumerable PrepareComponentTypes2(IEnumerable componentTypes, RuntimeLevel level)
{
// create a list, remove those that cannot be enabled due to runtime level
var componentTypeList = componentTypes
.Where(x =>
{
var attr = x.GetCustomAttribute();
return attr == null || level >= attr.MinLevel;
})
.ToList();
// cannot remove that one - ever
if (componentTypeList.Contains(typeof(UmbracoCoreComponent)) == false)
componentTypeList.Add(typeof(UmbracoCoreComponent));
var enabled = new Dictionary();
// process the enable/disable attributes
// these two attributes are *not* inherited and apply to *classes* only (not interfaces).
// remote declarations (when a component enables/disables *another* component)
// have priority over local declarations (when a component disables itself) so that
// ppl can enable components that, by default, are disabled.
// what happens in case of conflicting remote declarations is unspecified. more
// precisely, the last declaration to be processed wins, but the order of the
// declarations depends on the type finder and is unspecified.
foreach (var componentType in componentTypeList)
{
foreach (var attr in componentType.GetCustomAttributes())
{
var type = attr.EnabledType ?? componentType;
EnableInfo enableInfo;
if (enabled.TryGetValue(type, out enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
var weight = type == componentType ? 1 : 2;
if (enableInfo.Weight > weight) continue;
enableInfo.Enabled = true;
enableInfo.Weight = weight;
}
foreach (var attr in componentType.GetCustomAttributes())
{
var type = attr.DisabledType ?? componentType;
if (type == typeof(UmbracoCoreComponent)) throw new InvalidOperationException("Cannot disable UmbracoCoreComponent.");
EnableInfo enableInfo;
if (enabled.TryGetValue(type, out enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
var weight = type == componentType ? 1 : 2;
if (enableInfo.Weight > weight) continue;
enableInfo.Enabled = false;
enableInfo.Weight = weight;
}
}
// remove components that end up being disabled
foreach (var kvp in enabled.Where(x => x.Value.Enabled == false))
componentTypeList.Remove(kvp.Key);
// sort the components according to their dependencies
var items = new List>();
var temp = new List(); // reduce allocs
foreach (var type in componentTypeList)
{
temp.Clear();
//// for tests
//Console.WriteLine("Components & Dependencies:");
//Console.WriteLine(type.FullName);
//foreach (var attribute in type.GetCustomAttributes())
// Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : ""));
//foreach (var i in type.GetInterfaces())
//{
// Console.WriteLine(" " + i.FullName);
// foreach (var attribute in i.GetCustomAttributes())
// Console.WriteLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue ? (attribute.Weak.Value ? " (weak)" : " (strong)") : ""));
//}
//Console.WriteLine("/");
//Console.WriteLine();
// get attributes
// these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
var attributes = type
.GetInterfaces().SelectMany(x => x.GetCustomAttributes())
.Concat(type.GetCustomAttributes());
// what happens in case of conflicting attributes (different strong/weak for same type) is not specified.
foreach (var attr in attributes)
{
// requiring an interface = require any enabled component implementing that interface
// unless strong, and then require at least one enabled component implementing that interface
if (attr.RequiredType.IsInterface)
{
var implems = componentTypeList.Where(x => attr.RequiredType.IsAssignableFrom(x)).ToList();
if (implems.Count > 0)
temp.AddRange(implems);
else if (attr.Weak == false) // if explicitely set to !weak, is strong, else is weak
throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
}
// requiring a class = require that the component is enabled
// unless weak, and then requires it if it is enabled
else
{
if (componentTypeList.Contains(attr.RequiredType))
temp.Add(attr.RequiredType);
else if (attr.Weak != true) // if not explicitely set to weak, is strong
throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
}
}
var dependsOn = temp.Distinct().Select(x => x.FullName).ToArray();
items.Add(new TopologicalSorter.DependencyField(type.FullName, dependsOn, new Lazy(() => type)));
}
return TopologicalSorter.GetSortedItems(items);
}
private void InstanciateComponents(IEnumerable types)
{
using (_proflog.DebugDuration("Instanciating components.", "Instanciated components."))
{
_components = types.Select(x => (IUmbracoComponent) Activator.CreateInstance(x)).ToArray();
}
}
private void ComposeComponents(RuntimeLevel level)
{
using (_proflog.DebugDuration($"Composing components. (log when >{LogThresholdMilliseconds}ms)", "Composed components."))
{
var composition = new Composition(_container, level);
foreach (var component in _components)
{
var componentType = component.GetType();
using (_proflog.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
component.Compose(composition);
}
}
}
}
private void InitializeComponents()
{
// use a container scope to ensure that PerScope instances are disposed
// components that require instances that should not survive should register them with PerScope lifetime
using (_proflog.DebugDuration($"Initializing components. (log when >{LogThresholdMilliseconds}ms)", "Initialized components."))
using (_container.BeginScope())
{
foreach (var component in _components)
{
var componentType = component.GetType();
var initializers = componentType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == "Initialize" && x.IsGenericMethod == false);
using (_proflog.DebugDuration($"Initializing {componentType.FullName}.", $"Initialised {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
foreach (var initializer in initializers)
{
var parameters = initializer.GetParameters()
.Select(x => GetParameter(componentType, x.ParameterType))
.ToArray();
initializer.Invoke(component, parameters);
}
}
}
}
}
private object GetParameter(Type componentType, Type parameterType)
{
object param;
try
{
param = _container.TryGetInstance(parameterType);
}
catch (Exception e)
{
throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.", e);
}
if (param == null) throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.");
return param;
}
public void Terminate()
{
if (_booted == false)
{
_proflog.Logger.Warn("Cannot terminate, has not booted.");
return;
}
using (_proflog.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
{
foreach (var component in _components)
{
var componentType = component.GetType();
using (_proflog.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
component.Terminate();
}
}
}
}
}
}