HibernateUserAdministrationTest now based on the component mechanism.
[utils] / system / general / src / main / java / org / wamblee / system / core / Container.java
index 8436ebca4174ba7090966756a5b6b2e5b8923ea5..1c19be341d874e10675fb465ee702b5a662845ea 100644 (file)
@@ -18,230 +18,395 @@ package org.wamblee.system.core;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.wamblee.collections.CollectionFilter;
+import org.wamblee.conditions.Condition;
+import org.wamblee.general.Pair;
 
 /**
- * Composite system consisting of multiple subsystems.
+ * Container consisting of multiple components.
  * 
  * @author Erik Brakkee
  */
-public class Container extends AbstractComponent {
-
-       private static final Log LOG = LogFactory.getLog(Container.class);
-
-       private Component[] _systems;
-
-       public static RequiredInterface[] filterRequiredServices(
-                       ProvidedInterface aProvided,
-                       Collection<RequiredInterface> aDescriptors) {
-               List<RequiredInterface> required = new ArrayList<RequiredInterface>();
-               for (RequiredInterface descriptor : aDescriptors) {
-                       if (descriptor.implementedBy(aProvided)) {
-                               required.add(descriptor);
-                       }
-               }
-               return required.toArray(new RequiredInterface[0]);
-       }
-
-       public static ProvidedInterface[] filterProvidedServices(
-                       RequiredInterface aRequired, Collection<ProvidedInterface> aProvided) {
-               List<ProvidedInterface> provided = new ArrayList<ProvidedInterface>();
-               for (ProvidedInterface descriptor : aProvided) {
-                       if (aRequired.implementedBy(descriptor)) {
-                               provided.add(descriptor);
-                       }
-               }
-               return provided.toArray(new ProvidedInterface[0]);
-       }
-
-       /**
-        * Construcst the composite system.
-        * 
-        * @param aName
-        *            Name of the system.
-        * @param aRegistry
-        *            Service registry.
-        * @param aSystems
-        *            Subsystems.
-        * @param aProvided
-        *            Provided services of the system.
-        * @param aRequired
-        *            Required services by the system.
-        */
-       public Container(String aName, Component[] aSystems,
-                       ProvidedInterface[] aProvided, RequiredInterface[] aRequired) {
-               super(aName, aProvided, aRequired);
-               _systems = aSystems;
-               for (Component component : aSystems) {
-                       component.addContext(getQualifiedName());
-               }
-               validate(aRequired);
-       }
-
-       /**
-        * Validates the subsystems together to check that there are no required
-        * services not in the required list and no services in the provided list
-        * that cannot be provided. Also logs a warning in case of superfluous
-        * requirements.
-        */
-       private void validate(RequiredInterface[] aRequired) {
-               List<ProvidedInterface> provided = new ArrayList<ProvidedInterface>();
-               for (Component system : _systems) {
-                       provided.addAll(Arrays.asList(system.getProvidedInterfaces()));
-               }
-
-               List<RequiredInterface> required = new ArrayList<RequiredInterface>();
-               for (Component system : _systems) {
-                       required.addAll(Arrays.asList(system.getRequiredInterfaces()));
-               }
-
-               for (ProvidedInterface service : getProvidedInterfaces()) {
-                       if (!(provided.contains(service))) {
-                               throw new SystemAssemblyException(getName() + ": Service '"
-                                               + service
-                                               + "' is not provided by any of the subsystems");
-                       }
-               }
-
-               for (RequiredInterface service : getRequiredInterfaces()) {
-                       if (!(required.contains(service))) {
-                               info("Service '"
-                                               + service
-                                               + "' indicated as required is not actually required by any of the subsystems");
-                       }
-               }
-
-               List<RequiredInterface> reallyRequired = new ArrayList<RequiredInterface>(
-                               required);
-               // Compute all required interfaces that are not provided
-               for (ProvidedInterface service : provided) {
-                       List<RequiredInterface> fulfilled = Arrays
-                                       .asList(filterRequiredServices(service, reallyRequired));
-                       reallyRequired.removeAll(fulfilled);
-               }
-               // Now the remaining interfaces should be covered by the required
-               // list.
-               reallyRequired.removeAll(Arrays.asList(aRequired));
-
-               String missingRequired = "";
-               for (RequiredInterface service : reallyRequired) {
-                       missingRequired += service + "\n";
-               }
-               if (missingRequired.length() > 0) {
-                       throw new SystemAssemblyException(getName()
-                                       + ": missing required services\n" + missingRequired);
-               }
-       }
-
-       @Override
-       protected void doStart() {
-               List<ProvidedInterface> provided = new ArrayList<ProvidedInterface>();
-
-               // all interfaces from the required list of this container are
-               // provided to the components inside it.
-               RequiredInterface[] required = getRequiredInterfaces();
-               for (RequiredInterface intf : required) {
-                       ProvidedInterface provider = intf.getProvider();
-                       if (provider == null) {
-                               throw new SystemAssemblyException(getQualifiedName()
-                                               + ": required interface '" + intf + "' is not provided");
-                       }
-                       provided.add(intf.getProvider());
-               }
-
-               startImpl();
-       }
-
-       /**
-        * Starts the subsystems.
-        * 
-        * @param aRequiredServices
-        *            Services that are available from other systems that have been
-        *            started before.
-        */
-       private void startImpl() {
-               LOG.info("Starting '" + getQualifiedName() + "'");
-               List<ProvidedInterface> allProvided = new ArrayList<ProvidedInterface>();
-
-               // Add the provides of all externally required interfaces to the list of
-               // available
-               // interfaces
-               for (RequiredInterface required : getRequiredInterfaces()) {
-                       allProvided.add(required.getProvider());
-               }
-
-               List<Component> started = new ArrayList<Component>();
-               for (Component system : _systems) {
-                       try {
-                               // Check if all required services are already provided by
-                               // earlier
-                               // systems.
-                               RequiredInterface[] required = system.getRequiredInterfaces();
-
-                               for (RequiredInterface descriptor : required) {
-                                       ProvidedInterface[] filtered = filterProvidedServices(
-                                                       descriptor, allProvided);
-
-                                       if (filtered.length == 0) {
-                                               throw new SystemAssemblyException(
-                                                               "Service '"
-                                                                               + descriptor
-                                                                               + "' required by system '"
-                                                                               + system
-                                                                               + "' is not provided by systems that are started earlier");
-                                       }
-                                       if (filtered.length > 1) {
-                                               throw new SystemAssemblyException(
-                                                               "Service '"
-                                                                               + descriptor
-                                                                               + "' required by system '"
-                                                                               + system
-                                                                               + "' matches multiple services provided by other systems: "
-                                                                               + Arrays.asList(filtered));
-                                       }
-                                       descriptor.setProvider(filtered[0]);
-                               }
-
-                               // Start the service.
-                               system.start();
-                               started.add(system);
-
-                               // add all provided services
-                               ProvidedInterface[] provided = system.getProvidedInterfaces();
-                               allProvided.addAll(Arrays.asList(provided));
-                       } catch (SystemAssemblyException e) { 
-                               throw e; 
-                       } catch (RuntimeException e) {
-                               LOG.error(getQualifiedName() + ": could not start '"
-                                               + system.getQualifiedName() + "'", e);
-                               // an exception occurred, stop the successfully started
-                               // systems
-                               for (int i = started.size() - 1; i >= 0; i--) {
-                                       try {
-                                               started.get(i).stop();
-                                       } catch (Throwable t) {
-                                               LOG.error(getQualifiedName() + ": error stopping "
-                                                               + started.get(i).getQualifiedName());
-                                       }
-                               }
-                               throw e; 
-                       }
-               }
-
-       }
-
-       @Override
-       protected void doStop() {
-               for (int i = _systems.length - 1; i >= 0; i--) {
-                       _systems[i].stop();
-               }
-       }
-
-       private void info(String aMsg) {
-               LOG.info(getName() + ": " + aMsg);
-       }
+public class Container extends AbstractComponent<Scope> {
 
+    private static final Log LOG = LogFactory.getLog(Container.class);
+
+    private List<Component> _components;
+    private Set<String> _componentNames;
+    private CompositeInterfaceRestriction _restriction; 
+    private boolean _sealed;
+   
+    static ProvidedInterface[] filterProvidedServices(
+            Component aClient, RequiredInterface aRequired, Collection<Pair<ProvidedInterface,Component>> aProvided,
+            InterfaceRestriction aRestriction) {
+        List<ProvidedInterface> result = new ArrayList<ProvidedInterface>();
+        for (Pair<ProvidedInterface,Component> descriptor : aProvided) {
+            ProvidedInterface provided = descriptor.getFirst();
+            Component server = descriptor.getSecond();
+            if (aRequired.implementedBy(provided) && 
+                    !aRestriction.isViolated(aClient, aRequired, server, provided)) {
+                result.add(provided);
+            }
+        }
+        return result.toArray(new ProvidedInterface[0]);
+    }
+
+    /**
+     * Constructs the container
+     * 
+     * @param aName
+     *            Name of the container
+     * @param aComponents
+     *            Components.
+     * @param aProvided
+     *            Provided services of the container
+     * @param aRequired
+     *            Required services by the container.
+     */
+    public Container(String aName, Component[] aComponents,
+            ProvidedInterface[] aProvided, RequiredInterface[] aRequired) {
+        super(aName, aProvided, aRequired);
+        _components = new ArrayList<Component>();
+
+        _componentNames = new HashSet<String>();
+        _restriction = new CompositeInterfaceRestriction();
+        _sealed = false;
+        for (Component component : aComponents) {
+            addComponent(component);
+        }
+    }
+
+    public Container(String aName) {
+        this(aName, new Component[0], new ProvidedInterface[0],
+                new RequiredInterface[0]);
+    }
+
+    public Container addComponent(Component aComponent) {
+        checkSealed();
+        if (aComponent.getContext() != null) {
+            throw new SystemAssemblyException(
+                    "Inconsistent hierarchy, component '"
+                            + aComponent.getName()
+                            + "' is already part of another hierarchy");
+        }
+        if (_componentNames.contains(aComponent.getName())) {
+            throw new SystemAssemblyException("Duplicate component '"
+                    + aComponent.getName() + "'");
+        }
+        _components.add(aComponent);
+        _componentNames.add(aComponent.getName());
+        aComponent.addContext(getQualifiedName());
+        return this;
+    }
+    
+    /**
+     * Adds an interface restriction for explicitly configuring the 
+     * relations between components. 
+     * @param aRestriction Restriction to add. 
+     * @return Reference to this to allow call chaining. 
+     */
+    public Container addRestriction(InterfaceRestriction aRestriction) {
+        checkSealed(); 
+        _restriction.add(aRestriction);
+        return this; 
+    }
+
+    @Override
+    public Container addProvidedInterface(ProvidedInterface aProvided) {
+        checkSealed();
+        super.addProvidedInterface(aProvided);
+        return this;
+    }
+
+    @Override
+    public Container addRequiredInterface(RequiredInterface aRequired) {
+        checkSealed();
+        super.addRequiredInterface(aRequired);
+        return this;
+    }
+
+    /**
+     * Validates the components together to check that there are no required
+     * services not in the required list and no services in the provided list
+     * that cannot be provided. Also logs a warning in case of superfluous
+     * requirements.
+     * 
+     * @throws SystemAssemblyException
+     *             in case of any validation problems.
+     */
+    public void validate() {
+        validateProvidedInterfacesArePresent();
+
+        validateRequiredInterfaces();
+
+        doStartOptionalDryRun(null, true);
+    }
+
+    private void validateRequiredInterfaces() {
+        List<RequiredInterface> required = new ArrayList<RequiredInterface>();
+        for (Component component : _components) {
+            required.addAll(Arrays.asList(component.getRequiredInterfaces()));
+        }
+
+        for (RequiredInterface service : getRequiredInterfaces()) {
+            // TODO required interfaces by the component could be
+            // subclasses or implementations of the requirements
+            // of the contained components. The code below assumes
+            // an exact match.
+            if (!(required.contains(service))) {
+                info("Service '"
+                        + service
+                        + "' indicated as required is not actually required by any of the components");
+            }
+            // Check for the case that the externally required service
+            // is optional whereas the internally required service is
+            // mandatory.
+            if (service.isOptional()) {
+                for (RequiredInterface intf : required) {
+                    if (intf.equals(service) && !intf.isOptional()) {
+                        warn("Required service '"
+                                + service
+                                + "' indicated as optional is mandatory by one of its components ("
+                                + getClients(intf)
+                                + ", "
+                                + intf
+                                + "), this can lead to problems when the container is started and the interface is not provided to the container.");
+                    }
+                }
+            }
+        }
+    }
+
+    private void validateProvidedInterfacesArePresent() {
+        List<ProvidedInterface> provided = new ArrayList<ProvidedInterface>();
+        for (Component component : _components) {
+            provided.addAll(Arrays.asList(component.getProvidedInterfaces()));
+        }
+        for (ProvidedInterface service : getProvidedInterfaces()) {
+            // TODO provided interfaces by components could be
+            // provide subclasses or implementations of the
+            // provided interfaces of the container.
+            // The code below assumes an exact match.
+            if (!(provided.contains(service))) {
+                throw new SystemAssemblyException(getQualifiedName() + ": Service '"
+                        + service
+                        + "' is not provided by any of its components");
+            }
+        }
+    }
+
+    /**
+     * Seal the container, meaning that no further components or interfaces may
+     * be added.
+     */
+    public void seal() {
+        _sealed = true;
+    }
+
+    /**
+     * Checks if the container is sealed.
+     * 
+     * @return True iff the container is sealed.
+     */
+    public boolean isSealed() {
+        return _sealed;
+    }
+    
+    /**
+     * Utility method to start with an empty external scope. This is useful for
+     * top-level containers which are not part of another container. 
+     * @return Scope. 
+     */
+    public Scope start() {
+        Scope scope = new DefaultScope(getProvidedInterfaces()); 
+        return super.start(scope);
+    }
+
+    @Override
+    protected Scope doStart(Scope aExternalScope) {
+        checkSealed();
+        validate();
+        Scope scope = new DefaultScope(getProvidedInterfaces(), aExternalScope);
+        doStartOptionalDryRun(scope, false);
+        seal();
+        return scope;
+    }
+
+    private void doStartOptionalDryRun(Scope aScope, boolean aDryRun) {
+        LOG.info("Starting '" + getQualifiedName() + "'");
+
+        List<Pair<ProvidedInterface,Component>> allProvided = new ArrayList<Pair<ProvidedInterface,Component>>();
+        
+        addProvidersOfRequiredInterfaces(allProvided);
+
+        List<Component> started = new ArrayList<Component>();
+        for (Component component : _components) {
+            try {
+                initializeProvidersForRequiredInterfaces(allProvided,
+                        component, aDryRun);
+
+                // Start the service.
+                if (!aDryRun) {
+                    Object runtime = component.start(aScope);
+                    aScope.addRuntime(component, runtime);
+                    started.add(component);
+                }
+
+                // add all provided services
+                ProvidedInterface[] provided = component
+                        .getProvidedInterfaces();
+                for (ProvidedInterface prov: provided) { 
+                    allProvided.add(new Pair<ProvidedInterface,Component>(prov, component));
+                }
+            } catch (SystemAssemblyException e) {
+                throw e;
+            } catch (RuntimeException e) {
+                LOG.error(getQualifiedName() + ": could not start '"
+                        + component.getQualifiedName() + "'", e);
+                stopAlreadyStartedComponents(started, aScope);
+                throw e;
+            }
+        }
+    }
+
+    private void addProvidersOfRequiredInterfaces(
+            List<Pair<ProvidedInterface,Component>> allProvided) {
+        // all interfaces from the required list of this container are
+        // provided to the components inside it.
+        RequiredInterface[] required = getRequiredInterfaces();
+        for (RequiredInterface intf : required) {
+            ProvidedInterface provider = intf.getProvider();
+            if (provider != null) {
+                allProvided.add(new Pair<ProvidedInterface,Component>(provider, null));
+            } else {
+                if (!intf.isOptional()) {
+                    throw new SystemAssemblyException(getQualifiedName()
+                            + ": required interface '" + intf
+                            + "' is not provided");
+                }
+            }
+        }
+    }
+
+    private void stopAlreadyStartedComponents(List<Component> aStarted,
+            Scope aScope) {
+        // an exception occurred, stop the successfully started
+        // components
+        for (int i = aStarted.size() - 1; i >= 0; i--) {
+            try {
+                Component component = aStarted.get(i);
+                aStarted.get(i).stop(aScope.getRuntime(component));
+            } catch (Throwable t) {
+                LOG.error(getQualifiedName() + ": error stopping "
+                        + aStarted.get(i).getQualifiedName());
+            }
+        }
+    }
+
+    /**
+     * Sets the provided interface or a component.
+     * 
+     * @param aAllProvided
+     *            All available provided interfaces.
+     * @param aComponent
+     *            Component whose required interfaces we are looking at.
+     * @param aValidateOnly
+     *            If true then the provider will not be set for required
+     *            interfaces.
+     */
+    private void initializeProvidersForRequiredInterfaces(
+            List<Pair<ProvidedInterface,Component>> aAllProvided, Component aComponent,
+            boolean aValidateOnly) {
+        // Check if all required services are already provided by
+        // earlier
+        // systems.
+
+        for (RequiredInterface descriptor : aComponent.getRequiredInterfaces()) {
+            ProvidedInterface[] filtered = filterProvidedServices(aComponent, descriptor,
+                    aAllProvided, _restriction);
+            if (filtered.length == 1) {
+                if (!aValidateOnly) {
+                    descriptor.setProvider(filtered[0]);
+                }
+            } else if (filtered.length > 1) {
+                throw new SystemAssemblyException(
+                        "Service '"
+                                + descriptor
+                                + "' required by system '"
+                                + aComponent
+                                + "' matches multiple services provided by other systems: "
+                                + getServers(filtered));
+            } else {
+                // filtered.length == 0
+                if (!descriptor.isOptional()) {
+                    throw new SystemAssemblyException(
+                            "Service '"
+                                    + descriptor
+                                    + "' required by system '"
+                                    + aComponent
+                                    + "' is not provided by systems that are started earlier");
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void doStop(Scope aScope) {
+        for (int i = _components.size() - 1; i >= 0; i--) {
+            Component component = _components.get(i);
+            Object runtime = aScope.getRuntime(component);
+            component.stop(runtime);
+        }
+    }
+
+    private void info(String aMsg) {
+        LOG.info(getQualifiedName() + ": " + aMsg);
+    }
+
+    private void warn(String aMsg) {
+        LOG.warn(getQualifiedName() + ": " + aMsg);
+    }
+
+    private String getServers(ProvidedInterface[] aProvidedList) {
+        String result = "";
+        for (ProvidedInterface provided : aProvidedList) {
+            result += "(components ";
+            for (Component component : _components) {
+                for (ProvidedInterface provided2 : component
+                        .getProvidedInterfaces()) {
+                    if (provided.equals(provided2)) {
+                        result += component + " ";
+                    }
+                }
+            }
+            result += ", interface " + provided + ")";
+        }
+        return result;
+    }
+
+    private List<Component> getClients(RequiredInterface aRequirement) {
+        List<Component> clients = new ArrayList<Component>();
+        for (Component component : _components) {
+            for (RequiredInterface required : component.getRequiredInterfaces()) {
+                if (required.equals(aRequirement)
+                        && required.isOptional() == aRequirement.isOptional()) {
+                    clients.add(component);
+                }
+            }
+        }
+        return clients;
+    }
+
+    private void checkSealed() {
+        if (_sealed) {
+            throw new SystemAssemblyException("Container is sealed");
+        }
+    }
 }