![JAR search and dependency download from the Maven repository](/logo.png)
com.remondis.limbus.system.LimbusSystem Maven / Gradle / Ivy
Show all versions of limbus-system Show documentation
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 extends IInitializable>> 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 extends IInitializable>> componentType = conf.getComponentType();
try {
IInitializable> instance = null;
IInitializable> publicReference = null;
if (conf.isPublicComponent()) {
// Public components need a request type.
Class extends IInitializable>> 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;
}
}