com.kolich.curacao.components.ComponentTable.java Source code

Java tutorial

Introduction

Here is the source code for com.kolich.curacao.components.ComponentTable.java

Source

/**
 * Copyright (c) 2015 Mark S. Kolich
 * http://mark.koli.ch
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kolich.curacao.components;

import com.google.common.base.Predicates;
import com.google.common.collect.*;
import com.kolich.curacao.CuracaoConfigLoader;
import com.kolich.curacao.annotations.Component;
import com.kolich.curacao.exceptions.CuracaoException;
import com.kolich.curacao.exceptions.reflection.ComponentInstantiationException;
import org.slf4j.Logger;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletContext;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.kolich.curacao.util.reflection.CuracaoReflectionUtils.getInjectableConstructor;
import static com.kolich.curacao.util.reflection.CuracaoReflectionUtils.getTypesInPackageAnnotatedWith;
import static org.slf4j.LoggerFactory.getLogger;

public final class ComponentTable {

    private static final Logger logger__ = getLogger(ComponentTable.class);

    private static final String COMPONENT_ANNOTATION_SN = Component.class.getSimpleName();

    private static final int UNINITIALIZED = 0, INITIALIZED = 1;

    /**
     * This table maps a set of known class instance types to their
     * respective singleton objects.
     */
    private final ImmutableMap<Class<?>, Object> componentTable_;

    /**
     * A local boot switch to ensure that components in this mapping
     * table are only ever initialized if uninitialized, and destroyed
     * if initialized.
     */
    private final AtomicInteger bootSwitch_;

    /**
     * The underlying Servlet context of this application.  Used
     * only to inject the {@link ServletContext} object into instantiated
     * components as needed.
     */
    private final ServletContext context_;

    public ComponentTable(@Nonnull final ServletContext context) {
        context_ = checkNotNull(context, "Servlet context cannot be null.");
        bootSwitch_ = new AtomicInteger(UNINITIALIZED);
        final String bootPackage = CuracaoConfigLoader.getBootPackage();
        logger__.info("Loading components from declared boot-package: {}", bootPackage);
        // Scan for "components" inside of the declared boot package
        // looking for annotated Java classes that represent components.
        componentTable_ = buildComponentTable(bootPackage);
        logger__.info("Application component table: {}", componentTable_);
    }

    private final ImmutableMap<Class<?>, Object> buildComponentTable(final String bootPackage) {
        final Map<Class<?>, Object> componentMap = Maps.newLinkedHashMap(); // Linked hash map to preserve order.
        // Immediately add the Servlet context object to the component map
        // such that components and controllers who need access to the context
        // via their Injectable constructor can get it w/o any trickery.
        componentMap.put(ServletContext.class, context_);
        // Use the reflections package scanner to scan the boot package looking
        // for all classes therein that contain "annotated" component classes.
        final ImmutableSet<Class<?>> components = getTypesInPackageAnnotatedWith(bootPackage, Component.class);
        logger__.debug("Found {} components annotated with @{}", components.size(), COMPONENT_ANNOTATION_SN);
        // For each discovered component...
        for (final Class<?> component : components) {
            logger__.debug("Found @{}: {}", COMPONENT_ANNOTATION_SN, component.getCanonicalName());
            try {
                // If the component mapping table does not already contain an
                // instance for component class type, then instantiate one.
                if (!componentMap.containsKey(component)) {
                    // The "dep stack" is used to keep track of where we are as
                    // far as circular dependencies go.
                    final Set<Class<?>> depStack = Sets.newLinkedHashSet();
                    componentMap.put(component,
                            // Recursively instantiate components up-the-tree
                            // as needed, as defined based on their @Injectable
                            // annotated constructors.  Note that this method does
                            // NOT initialize the component, that is done later
                            // after all components are instantiated.
                            instantiate(components, componentMap, component, depStack));
                }
            } catch (Exception e) {
                // The component could not be instantiated.  There's no point
                // in continuing, so give up in error.
                throw new ComponentInstantiationException(
                        "Failed to " + "instantiate component instance: " + component.getCanonicalName(), e);
            }
        }
        return ImmutableMap.copyOf(componentMap);
    }

    private final Object instantiate(final ImmutableSet<Class<?>> allComponents,
            final Map<Class<?>, Object> componentMap, final Class<?> component, final Set<Class<?>> depStack)
            throws Exception {
        // Locate a single constructor worthy of injecting with ~other~
        // components, if any.  May be null.
        final Constructor<?> ctor = getInjectableConstructor(component);
        Object instance = null;
        if (ctor == null) {
            // Class.newInstance() is evil, so we do the ~right~ thing
            // here to instantiate a new instance of the component
            // using the preferred getConstructor() idiom.
            instance = component.getConstructor().newInstance();
        } else {
            final Class<?>[] types = ctor.getParameterTypes();
            // Construct an array of Object's outright to avoid system array
            // copying from a List/Collection to a vanilla array later.
            final Object[] params = new Object[types.length];
            for (int i = 0, l = types.length; i < l; i++) {
                final Class<?> type = types[i];
                // <https://github.com/markkolich/curacao/issues/7>
                // If the dependency stack contains the type we're tasked with
                // instantiating, but the component map already contains an
                // instance with this type, then it's ~not~ a real circular
                // dependency -- we already have what we need to fulfill the
                // request.
                if (depStack.contains(type) && !componentMap.containsKey(type)) {
                    // Circular dependency detected, A -> B, but B -> A.
                    // Or, A -> B -> C, but C -> A.  Can't do that, sorry!
                    throw new CuracaoException("CIRCULAR DEPENDENCY DETECTED! " + "While trying to instantiate @"
                            + COMPONENT_ANNOTATION_SN + ": " + component.getCanonicalName() + " it depends "
                            + "on the other components (" + depStack + ")");
                } else if (componentMap.containsKey(type)) {
                    // The component mapping table already contained an instance
                    // of the component type we're after.  Simply grab it and
                    // add it to the constructor parameter list.
                    params[i] = componentMap.get(type);
                } else if (type.isInterface()) {
                    // Interfaces are handled differently.  The logic here
                    // involves finding some component, if any, that implements
                    // the discovered interface type.  If one is found, we attempt
                    // to instantiate it, if it hasn't been instantiated already.
                    final Class<?> found = Iterables.tryFind(allComponents, Predicates.assignableFrom(type))
                            .orNull();
                    if (found != null) {
                        // We found some component that implements the discovered
                        // interface.  Let's try to instantiate it.  Add the
                        // ~interface~ class type ot the dependency stack.
                        depStack.add(type);
                        final Object recursiveInstance =
                                // Recursion!
                                instantiate(allComponents, componentMap, found, depStack);
                        // Add the freshly instantiated component instance to the
                        // new component mapping table as we go.
                        componentMap.put(found, recursiveInstance);
                        // Add the freshly instantiated component instance to the
                        // list of component constructor arguments/parameters.
                        params[i] = recursiveInstance;
                    } else {
                        // Found no component that implements the given interface.
                        params[i] = null;
                    }
                } else {
                    // The component mapping table does not contain a
                    // component for the given class.  We might need to
                    // instantiate a fresh one and then inject, checking
                    // carefully for circular dependencies.
                    depStack.add(component);
                    final Object recursiveInstance =
                            // Recursion!
                            instantiate(allComponents, componentMap, type, depStack);
                    // Add the freshly instantiated component instance to the
                    // new component mapping table as we go.
                    componentMap.put(type, recursiveInstance);
                    // Add the freshly instantiated component instance to the
                    // list of component constructor arguments/parameters.
                    params[i] = recursiveInstance;
                }
            }
            instance = ctor.newInstance(params);
        }
        // The freshly freshly instantiated component instance may implement
        // a set of interfaces, and therefore, can be used to inject other
        // components that have specified it using only the interface and not
        // a concrete implementation.  As such, add each implemented interface
        // to the component map pointing directly to the concrete implementation
        // instance.
        for (final Class<?> interfacz : instance.getClass().getInterfaces()) {
            // If the component is decorated with 'CuracaoComponent' don't
            // bother trying to add said interfaces to the underlying component
            // map (they're special, internal to the toolkit, unrelated to
            // user defined interfaces).
            if (!interfacz.isAssignableFrom(CuracaoComponent.class)) {
                componentMap.put(interfacz, instance);
            }
        }
        return instance;
    }

    @Nullable
    public final Object getComponentForType(@Nonnull final Class<?> clazz) {
        checkNotNull(clazz, "Class instance type cannot be null.");
        return componentTable_.get(clazz);
    }

    /**
     * Initializes all of the components in this instance.  Should only be
     * called once on application context startup.  Is gated using an atomic
     * boot switch, ensuring that this method will only initialize the
     * components if they haven't been initialized yet.  This essentially
     * guarantees that the components will only ever be initialized once,
     * calling this method multiple times (either intentionally or by mistake)
     * will have no effect.
     * @return the underlying {@link ComponentTable}, this instance
     */
    public final ComponentTable initializeAll() {
        // We use an AtomicInteger here to guard against consumers of this
        // class from calling initializeAll() on the set of components multiple
        // times.  This guarantees that the initialize() method of each
        // component will never be called more than once in the same
        // application life-cycle.
        if (bootSwitch_.compareAndSet(UNINITIALIZED, INITIALIZED)) {
            for (final Map.Entry<Class<?>, Object> entry : componentTable_.entrySet()) {
                final Class<?> clazz = entry.getKey();
                final Object component = entry.getValue();
                // Only attempt to initialize the component if it implements
                // the component initializable interface.
                if (component instanceof ComponentInitializable) {
                    try {
                        logger__.debug("Initializing @{}: {}", COMPONENT_ANNOTATION_SN, clazz.getCanonicalName());
                        ((ComponentInitializable) component).initialize();
                    } catch (Exception e) {
                        // If the component failed to initialize, should we
                        // keep going?  That's up for debate.  Currently if one
                        // component did the wrong thing, we log the error
                        // and move on.  However, it is acknowledged that this
                        // behavior may lead to other more obscure issues later.
                        logger__.error("Failed to initialize @{}: {}", COMPONENT_ANNOTATION_SN,
                                clazz.getCanonicalName(), e);
                    }
                }
            }
        }
        return this; // Convenience
    }

    /**
     * Destroys all of the components in this instance.  Should only be
     * called once on application context shutdown.  Is gated using an atomic
     * boot switch, ensuring that this method will only destroy the
     * components if they have been initialized.  This essentially guarantees
     * that the components will only ever be destroyed once, calling this
     * method multiple times (either intentionally or by mistake) will have
     * no effect.
     * @return the underlying {@link ComponentTable}, this instance
     */
    public final ComponentTable destroyAll() {
        // We use an AtomicInteger here to guard against consumers of this
        // class from calling destroyAll() on the set of components multiple
        // times.  This guarantees that the destroy() method of each
        // component will never be called more than once in the same
        // application life-cycle.
        if (bootSwitch_.compareAndSet(INITIALIZED, UNINITIALIZED)) {
            for (final Map.Entry<Class<?>, Object> entry : componentTable_.entrySet()) {
                final Class<?> clazz = entry.getKey();
                final Object component = entry.getValue();
                // Only attempt to destroy the component if it implements
                // the component destroyable interface.
                if (component instanceof ComponentDestroyable) {
                    try {
                        logger__.debug("Destroying @{}: {}", COMPONENT_ANNOTATION_SN, clazz.getCanonicalName());
                        ((ComponentDestroyable) component).destroy();
                    } catch (Exception e) {
                        logger__.error("Failed to destroy (shutdown) @{}: {}", COMPONENT_ANNOTATION_SN,
                                clazz.getCanonicalName(), e);
                    }
                }
            }
        }
        return this; // Convenience
    }

}