All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.remondis.limbus.system.LimbusSystem Maven / Gradle / Ivy

Go to download

The Limbus System is a small light-weight CDI framework managing the Limbus Core Components. The object graph is represented by an XML configuration file or can be build using the Limbus System API. This module delivers an optional system component that visualizes the object graph and its dependencies after initializing: com.remondis.limbus.system.visualize.LimbusSystemVisualizer This component can be added to the Limbus System. To keep the dependencies of this module transparent and light-weight, the graph renderer is declared as an optional dependency. Add the following dependencies to your project to use the visualisation component: <!-- Graph Stream for Visualization feature This is an optional dependency and only required if using the com.remondis.limbus.system.visualize.LimbusSystemVisualizer --> <dependency> <groupId>org.graphstream</groupId> <artifactId>gs-core</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>org.graphstream</groupId> <artifactId>gs-ui</artifactId> <version>1.3</version> </dependency>

There is a newer version: 3.1.0
Show newest version
package com.remondis.limbus.system;

import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;

import com.remondis.limbus.IInitializable;
import com.remondis.limbus.Initializable;
import com.remondis.limbus.events.EventMulticaster;
import com.remondis.limbus.events.EventMulticasterFactory;
import com.remondis.limbus.utils.Lang;
import com.remondis.limbus.utils.ReflectionUtil;
import com.remondis.limbus.utils.SerializeException;
import com.remondis.limbus.utils.XStreamUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Limbus System is a manager for lifecycle objects of the type {@link IInitializable}. The Limbus System manages
 * the (de)initialization phases and the wiring between objects of the system.
 *
 * 

Components

*

* The Limbus System acts as a light-weight Context Dependency Injection where the context is the system of registered * components. A component can request a dependency injection for all public components of the same {@link LimbusSystem} * . * The dependencies are specified using the the injection feature via * annotation {@link LimbusComponent}. * * The Limbus System only keeps track of the registered system components. All components that are not part of the * system can only interact with it using the component request methods. * *

*

* Sometimes components need a reference to the instance of the containing {@link LimbusSystem}. Use the annotation * {@link LimbusContainer} for this purpose. *

* *

(De-)Initialization Order

*

* The Limbus System is responsible for object creation, wiring between the objects and the initialization of * {@link IInitializable} objects. The components declared in the system description are initialized in the order they * occur, but if a component B that depends on component A, will be initialized after component A. * The {@link LimbusSystem} always makes sure, that when a component is initializing, all of it's dependencies are * fully initialized. *

*

* The de-initialization will be performed in the reverse initialization order. *

* * *

Accessing lifecycle objects

* The {@link LimbusSystem} manages two kinds of object types: *
    *
  • Private components: Components that do not have a request type are managed as private components. Private * components share the same lifecycle as the containing {@link LimbusSystem} but instances are not accessible from the * outside.
  • *
  • Public components: Instances of components that specify a request type are accessible using the method * {@link #getComponent(Class)}.
  • *
* * * @author schuettec * */ public final class LimbusSystem extends Initializable { private static final Logger log = LoggerFactory.getLogger(LimbusSystem.class); protected static final ObjectFactory DEFAULT_FACTORY = new ReflectiveObjectFactory(); private SystemConfiguration configuration; private ObjectFactory objectFactory; private Map>, Component> publicComponents; private List allComponents; private List infoRecords; private AtomicBoolean denyRequests = new AtomicBoolean(false); private List initializeOrder; private EventMulticaster listeners; public static final XStreamUtil DEFAULT_XSTREAM = new XStreamUtil(SystemConfiguration.class, ComponentConfiguration.class); private XStreamUtil xstream = DEFAULT_XSTREAM; public LimbusSystem() { this.listeners = EventMulticasterFactory.create(LimbusSystemListener.class); this.configuration = new SystemConfiguration(); this.objectFactory = configuration.getObjectFactory(); } public void setObjectFactory(ObjectFactory objectFactory) { this.objectFactory = objectFactory; } LimbusSystem(SystemConfiguration configuration) { this(); Lang.denyNull("configuration", configuration); this.objectFactory = configuration.getObjectFactory(); // schuettec - 21.02.2017 : Add all items from the system configuration through the public add methods, because the // system configuration can be deserialized in an invalid state. addAllFromSystemConfiguration(configuration); } private void addAllFromSystemConfiguration(SystemConfiguration configuration) { List components = configuration.getComponents(); for (ComponentConfiguration c : components) { this.configuration.addComponentConfiguration(c); } } private List lazyInfoRecord() { if (infoRecords == null) { this.infoRecords = new LinkedList(); } return infoRecords; } /** * @return Returns the current state of the Limbus System with the recorded info objects in the order they occurred. */ public List getInfoRecords() { return new LinkedList(infoRecords); } /** * Processes an {@link InfoRecord} if the information feature is enabled. * * @param supplier * The supplier or a new {@link InfoRecord}. Only evaluated if the info feature is currently enabled. */ private void infoRecord(Supplier supplier) { List infoRecords = lazyInfoRecord(); if (infoRecords != null) { infoRecords.add(supplier.get()); } } /** * Checks if a component is available by the specified request type. * * @param requestType * The component's request type. * @return Returns true if a component for this request type is available, otherwise false * is returned. */ public > boolean hasComponentConfigurationFor(Class requestType) { return configuration.containsRequestType(requestType); } /** * Adds configuration for a private component. * * @param componentType * The actual component implementation type. * @param failOnError * If true this component causes the {@link LimbusSystem} to fail if the component fails to * initialize. If false the {@link LimbusSystem} performes a successful startup, even if this * component fails to start. * */ public > void addComponentConfiguration(Class componentType, boolean failOnError) { Lang.denyNull("componentType", componentType); ComponentConfiguration component = new ComponentConfiguration(componentType, failOnError); configuration.addComponentConfiguration(component); } public > void removePrivateComponentConfiguration(Class componentType) { Lang.denyNull("componentType", componentType); ComponentConfiguration component = new ComponentConfiguration(componentType, false); configuration.removeComponentConfiguration(component); } public > void removePublicComponentConfiguration(Class requestType) { Lang.denyNull("requestType", requestType); ComponentConfiguration component = new ComponentConfiguration(requestType, null); configuration.removeComponentConfiguration(component); } /** * Adds configuration for a required public component to this Limbus System. * *

* This component causes the {@link LimbusSystem} to fail it's startup if the component fails to initialize. *

* * @param requestType * The component type, used for component requests. * @param componentType * The actual component implementation type. */ public , I extends T> void addComponentConfiguration(Class requestType, Class componentType) { Lang.denyNull("componentType", componentType); // schuettec - 06.03.2017 : Public components are always required. ComponentConfiguration component = new ComponentConfiguration(requestType, componentType, true); configuration.addComponentConfiguration(component); } /** * Removes a component configuration from this Limbus System. * * @param requestType * The component's request type to remove. */ public > void removeComponentConfiguration(Class requestType) { configuration.removeByRequestType(requestType); } /** * Provides access to a public component by its request type. * * @param requestType * The request type of the component. * @return Returns the system component. */ public > T getComponent(Class requestType) { checkState(); denyOnDemand(); // schuettec - 20.02.2017 : The Limbus System is itself a public component return ReflectionUtil.getAsExpectedType(getInstance(requestType), requestType); } /** * Checks if a Limbus component is available through this Limbus System. * * @param requestType * The request type of the component. * @return Returns true if the component is available, false otherwise. * */ public > boolean hasComponent(Class requestType) { return _hasComponent(requestType); } private boolean _hasComponent(Class requestType) { return publicComponents.containsKey(requestType); } private Object getInstance(Class requestType) { return _getComponent(requestType).getPublicReference(); } private Component _getComponent(Class requestType) { if (publicComponents.containsKey(requestType)) { Component component = publicComponents.get(requestType); return component; } else { throw new NoSuchComponentException(requestType); } } private void denyOnDemand() { if (denyRequests.get()) { throw new IllegalStateException( "The Limbus system is currently starting/stopping and no components can be requested."); } } @Override protected void performInitialize() throws LimbusSystemException { denyRequests.set(true); try { this.publicComponents = new ConcurrentHashMap<>(); this.allComponents = new LinkedList<>(); this.initializeOrder = new LinkedList(); List components = configuration.getComponents(); createAllComponents(components); forAllComponents(allComponents, (component) -> { injectDependencies(component); }); forAllComponents(allComponents, (component) -> { initializeComponentOnDemand(component); }); denyRequests.set(false); firePostInitializeEvent(); } finally { logInfoRecordsOnDemand(); } } private void firePostInitializeEvent() { listeners.multicastSilently() .postInitialize(); } private void firePreDestroyEvent() { listeners.multicastSilently() .preDestroy(); listeners.clear(); listeners = null; } /** * Logs the current state of the {@link LimbusSystem} with all of its components and initialization states. */ public void logLimbusSystemInformation() { logInfoRecords(); } private void logInfoRecordsOnDemand() { logInfoRecords(); } private void logInfoRecords() { StringBuilder b = new StringBuilder("Dumping the component info records collected:\n"); b.append(InfoRecord.toRecordHeader()) .append("\n"); List lazyInfoRecord = lazyInfoRecord(); for (InfoRecord info : lazyInfoRecord) { b.append(info.toString()) .append("\n"); } log.info(b.toString()); } private void initializeComponent(Component component) throws LimbusComponentException { ComponentConfiguration conf = component.getConfiguration(); IInitializable instance = component.getInstance(); try { log.debug("Initializing component instance {}.", instance.getClass() .getName()); instance.initialize(); initializeOrder.add(component); infoRecord(initializedRecord(component)); } catch (Exception e) { infoRecord(exceptionRecord(component)); if (conf.isFailOnError()) { throw new LimbusComponentException(requiredComponentFailMessage(conf), e); } else { log.warn(optionalComponentFailMessage(conf), e); } removeComponent(component); } } private void removeComponent(Component component) { finishComponentOnDemand(component); } private void finishComponentOnDemand(Component component) { if (isInitializedComponent(component)) { try { finishComponent(component); } catch (Exception e) { handleExceptionOnFinish(e); } } } private void finishComponent(Component component) throws Exception { IInitializable instance = component.getInstance(); try { instance.finish(); infoRecord(finishedRecord(component)); } catch (Exception e) { infoRecord(errorOnFinish(component)); } } private Supplier errorOnFinish(Component component) { return () -> { return new InfoRecord(component, ComponentStatus.ERROR); }; } private Supplier finishedRecord(Component component) { return () -> { return new InfoRecord(component, ComponentStatus.FINISHED); }; } private void createAllComponents(List components) throws LimbusSystemException { for (ComponentConfiguration conf : components) { try { createComponent(conf); if (log.isDebugEnabled()) { log.debug("Component created {}.", conf); } } catch (LimbusComponentException e) { handleComponentException(conf, e); } } } private void handleComponentException(ComponentConfiguration conf, Exception cause) throws LimbusSystemException { if (conf.isFailOnError()) { throw new LimbusSystemException(requiredComponentFailMessage(conf), cause); } else { log.warn(optionalComponentFailMessage(conf), cause); } } private String optionalComponentFailMessage(ComponentConfiguration conf) { return String.format("Cannot initialize system component %s - skipping this component because it's optional.", conf.getComponentType()); } private String requiredComponentFailMessage(ComponentConfiguration conf) { return String.format("Cannot initialize required system component %s.", conf.getComponentType()); } private void forAllComponents(Collection allComponents, ComponentConsumer consumer) throws LimbusSystemException { for (Component component : allComponents) { ComponentConfiguration configuration = component.getConfiguration(); try { consumer.consume(component); } catch (LimbusComponentException e) { handleComponentException(configuration, e); } } } private void injectDependencies(Component component) throws LimbusComponentException { Stack dependencyPath = new Stack<>(); _injectDependenciesRecursive(component, dependencyPath); } private boolean _injectDependenciesRecursive(Component component, Stack dependencyPath) throws LimbusComponentException { // Track path of dependencies dependencyPath.add(component); IInitializable instance = component.getInstance(); Class> componentType = component.configuration.getComponentType(); List fields = ReflectionUtil.getAllAnnotatedFields(componentType, LimbusContainer.class, LimbusComponent.class); // schuettec - 02.03.2017 : The current component must be initialized if some of the dependencies were initialized. boolean initializedTree = false; boolean initializedDependency = false; for (Field f : fields) { // schuettec - 20.02.2017 : Inject LimbusSystem dependencies if (f.isAnnotationPresent(LimbusContainer.class)) { // schuettec - 24.04.2017 : Currently it is not possible to create a public reference for this LimbusSystem // instance using the object factory's method, because there is not public interface for LimbusSystem. // schuettec - 24.04.2017 : Workaround here is to monitor the public methods of LimbusSystem "manually". injectValue(f, instance, this); } if (f.isAnnotationPresent(LimbusComponent.class)) { // schuettec - 20.02.2017 : Inject component dependencies LimbusComponent componentAnnotation = f.getAnnotation(LimbusComponent.class); Class requestType = getRequestTypeFromAnnotationOrField(f, componentAnnotation); if (_hasComponent(requestType)) { Component dependency = _getComponent(requestType); // Detect circular dependencies. denyCyclicDependencies(dependencyPath, component, dependency); // Fork the path for every transitive dependency Stack fork = forkDependencyPath(dependencyPath); initializedTree = _injectDependenciesRecursive(dependency, fork); initializedDependency = initializeComponentOnDemand(dependency); IInitializable dependencyInstance = dependency.getPublicReference(); injectValue(f, instance, dependencyInstance); } else { throw new LimbusComponentException( String.format("Dependency injection cannot be satisfied: Component %s requires unavailable component %s.", componentType.getName(), requestType.getName())); } } } // schuettec - 02.03.2017 : If this action initialized a component, we have to return the result boolean hasInitialized = initializedTree || initializedDependency; if (hasInitialized) { initializeComponentOnDemand(component); } return hasInitialized; } private void injectValue(Field f, Object instance, Object value) throws LimbusComponentException { try { f.setAccessible(true); f.set(instance, value); } catch (IllegalArgumentException e) { throw new LimbusComponentException(String.format("Cannot inject field %s in component %s with value %s.", f.getName(), instance.getClass() .getName(), value.getClass() .getName()), e); } catch (IllegalAccessException e) { throw new LimbusComponentException( String.format("Cannot access field %s in component %s.", f.getName(), instance.getClass() .getName()), e); } } private Class getRequestTypeFromAnnotationOrField(Field f, LimbusComponent annotation) { if (annotation.value() == Void.class) { return f.getType(); } else { return annotation.value(); } } private void denyCyclicDependencies(Stack dependencyPath, Component requestor, Component dependency) { if (dependencyPath.contains(dependency)) { int iReq = dependencyPath.indexOf(requestor); int iDep = dependencyPath.indexOf(dependency); int start = Math.min(iReq, iDep); int end = Math.max(iReq, iDep); StringBuilder circle = new StringBuilder(); for (int i = start; i <= end; i++) { Component element = dependencyPath.elementAt(i); circle.append(element.getConfiguration() .getRequestType() .getName()); if (i < end) { circle.append(" ~> "); } } throw new LimbusCyclicException(String.format("Cyclic component dependency detected: %s.", circle.toString())); } } private Stack forkDependencyPath(Stack dependencyPath) { Stack newPath = new Stack<>(); newPath.addAll(dependencyPath); return newPath; } private boolean initializeComponentOnDemand(final Component dependency) throws LimbusComponentException { // schuettec - 09.05.2017 : Do not rely on isInitialized() because this relies to heavy on implementations and // mocking is more difficult because we always have to specify the correct answer on invocation of isInitialized(). if (isInitializedComponent(dependency)) { return false; } else { initializeComponent(dependency); return true; } } private boolean isInitializedComponent(final Component component) { return initializeOrder.contains(component); } private Supplier exceptionRecord(final Component dependency) { return () -> { ComponentStatus status = ComponentStatus.UNAVAILABLE; if (dependency.getConfiguration() .isFailOnError()) { status = ComponentStatus.ERROR; } return new InfoRecord(dependency, status); }; } private Supplier initializedRecord(final Component dependency) { return () -> { return new InfoRecord(dependency, ComponentStatus.INITIALIZED); }; } private void createComponent(ComponentConfiguration conf) throws LimbusComponentException { Class> componentType = conf.getComponentType(); try { IInitializable instance = null; IInitializable publicReference = null; if (conf.isPublicComponent()) { // Public components need a request type. Class> requestType = conf.getRequestType(); // Add to public components. instance = objectFactory.createObject(requestType, componentType); publicReference = objectFactory.createPublicReference(requestType, componentType, instance); publicComponents.put(requestType, new Component(conf, instance, publicReference)); } else { instance = objectFactory.createObject(componentType); publicReference = instance; } addListenerOnDemand(instance); allComponents.add(new Component(conf, instance, publicReference)); } catch (Exception e) { throw new LimbusComponentException( String.format("Could not create Limbus component %s.", componentType.getName()), e); } } /** * Adds the specified component instance as Limbus System event subscriber if it implements the * {@link LimbusSystemListener} interface. * * @param instance * The component instance to check and subscribe on demand. */ private void addListenerOnDemand(IInitializable instance) { // Add Limbus system event subscriber if (instance instanceof LimbusSystemListener) { LimbusSystemListener limbusSystemListener = (LimbusSystemListener) instance; listeners.addSubscriber(limbusSystemListener); } } @Override protected void performFinish() { firePreDestroyEvent(); denyRequests.set(true); try { List finishOrder = new LinkedList<>(this.initializeOrder); Collections.reverse(finishOrder); forAllComponents(finishOrder, (component) -> { try { log.debug("Finishing component {} ...", component.getInstance() .getClass() .getName()); finishComponent(component); } catch (Exception e) { log.error(String.format("Error while finishing component %s.", component.getInstance()), e); } }); } catch (LimbusSystemException e) { handleExceptionOnFinish(e); } catch (RuntimeException e) { handleExceptionOnFinish(e.getCause()); } finally { this.publicComponents = null; this.allComponents = null; denyRequests.set(false); logInfoRecordsOnDemand(); } } private void handleExceptionOnFinish(Throwable e) { log.warn( "Error while finishing a component. This operation was expected to be silent - this is an implementation fault.", e); } /** * Serializes this {@link LimbusSystem} to XML representation using the specified {@link OutputStream} * * @param output * The target {@link OutputStream} * @throws SerializeException * Thrown on any serialization error */ public void serializeConfiguration(OutputStream output) throws SerializeException { xstream.writeObject(configuration, output); } /** * Creates a {@link LimbusSystem} from a serialized XML representation using the specified {@link InputStream} * * @param input * The {@link InputStream} to read from * @return Returns the deserialized {@link LimbusSystem}. * @throws SerializeException * Thrown on any serialization error */ public static LimbusSystem deserializeConfiguration(InputStream input) throws SerializeException { SystemConfiguration readObject = DEFAULT_XSTREAM.readObject(SystemConfiguration.class, input); return new LimbusSystem(readObject); } protected ObjectFactory getObjectFactory() { return objectFactory; } }