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

org.nohope.spring.app.SpringAsyncModularApp Maven / Gradle / Ivy

The newest version!
package org.nohope.spring.app;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.xbean.finder.ResourceFinder;
import org.nohope.app.AsyncApp;
import org.nohope.logging.Logger;
import org.nohope.logging.LoggerFactory;
import org.nohope.reflection.IntrospectionUtils;
import org.nohope.typetools.collection.CollectionUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import static org.nohope.reflection.IntrospectionUtils.instanceOf;
import static org.nohope.reflection.IntrospectionUtils.searchConstructors;
import static org.nohope.spring.SpringUtils.*;

/**
 * Technical background
 * 

* This class assumes following classpath hierarchy: *

 *     classpath:
 *          META-INF/[appMetaInfNamespace]/[appName]-defaultContext.xml // mandatory
 *          [appName]-context.xml // <-- optional, will override default service context
 *
 *          META-INF/[moduleMetaInfNamespace]/[moduleName1].properties // <-- module descriptor, mandatory
 *          META-INF/[moduleMetaInfNamespace]/[moduleName1]-defaultContext.xml // mandatory
 *          [moduleName1]-context.xml // <-- optional, will override default module context
 * 
*

* Module descriptor is simple properties file content should looks like: *

 *     class=com.mypackage.ConcreteModule  // reserved
 *
 *     // any additional parameters
 *     key1=val1
 *     key2=val2
 * 
*

* Module name retrieved from properties file name. *

* Modules cross-dependencies injecting *

* Modules may request specific sub-modules. Consider next inheritance scheme *

 *       Module -- C
 *         /\
 *        /  \
 *       A    B
 * 
* * And three classes *
 *     class Module1 implements A {}
 *     class Module2 implements A {}
 *     class Module3 implements B {
 *         // @Dependency could be amended in case of constructor have only one IDependencyProvider parameter
 *         {@link Inject @Inject}
 *         Module3({@link IDependencyProvider IDependencyProvider<A>} prop) {
 *             prop; // will contain Module1 & Module2
 *         }
 *     }
 *     class Module4 implements C {
 *         // @Dependency should be declared for each IDependencyProvider parameter if
 *         // there is more than one such constructor parameter
 *         {@link Inject @Inject}
 *         Module3({@link Dependency @Dependency(A.class)} {@link IDependencyProvider IDependencyProvider}<A> p1,
 *                 {@link Dependency @Dependency(B.class)} {@link IDependencyProvider IDependencyProvider}<B> p2) {
 *             prop1; // will contain Module1 & Module2
 *             prop2; // will contain Module3
 *         }
 *     }
 * 
*

* Start procedure *

* On {@link #start() start} app will search for it's default service context, override it * with optionally passed service context. Then it searches for module definitions, * doing the same operations with their context (note: service context will be passed as a * parent to module context, so module will have access to all app beans) invoking appropriate * methods ({@link HandlerWithStorage#onModuleDiscovered(Class, ConfigurableApplicationContext, Properties, String) * onModuleDiscovered}, * {@link HandlerWithStorage#onModuleCreated(Object, ConfigurableApplicationContext, Properties, String) * onModuleDiscovered}). After all * {@link HandlerWithStorage#onModuleDiscoveryFinished() * onModuleDiscoveryFinished} * will be executed. * * @param module interface * @author ketoth xupack * @since 7/27/12 3:31 PM */ public final class SpringAsyncModularApp> extends AsyncApp { private static final Logger LOG = LoggerFactory.getLogger(SpringAsyncModularApp.class); private static final String META_INF = "META-INF/"; private static final String DEFAULT_MODULE_FOLDER = "module/"; private static final String DEFAULT_CONTEXT_POSTFIX = "-defaultContext.xml"; private static final String PROPERTIES_EXTENSION = ".properties"; private static final String CONTEXT_POSTFIX = "-context.xml"; private final ResourceFinder finder = new ResourceFinder(META_INF); private final Class targetModuleClass; private final String appMetaInfNamespace; private final String moduleMetaInfNamespace; private final String appName; private final ConfigurableApplicationContext ctx; private final H handler; /** * {@link SpringAsyncModularApp} constructor. * * @param targetModuleClass module type * @param appName this app name (if {@code null} passed then class name with lowercase * first letter will be used) * @param appMetaInfNamespace path relative to {@code META-INF} folder in classpath * to discover app context (if {@code null} passed then package path of app class will be used) * @param moduleMetaInfNamespace path relative to {@code META-INF} folder in classpath * to discover module context (if {@code null} passed then package path of targetClass * parameter will be used will be used) */ public SpringAsyncModularApp(@Nonnull final Class targetModuleClass, @Nonnull final Class handlerClass, @Nullable final String appName, @Nullable final String appMetaInfNamespace, @Nullable final String moduleMetaInfNamespace) { this.targetModuleClass = targetModuleClass; this.appMetaInfNamespace = appMetaInfNamespace == null ? getPackage(handlerClass) : toValidPath(appMetaInfNamespace); this.moduleMetaInfNamespace = moduleMetaInfNamespace == null ? getPackage(handlerClass) + DEFAULT_MODULE_FOLDER : toValidPath(moduleMetaInfNamespace); this.appName = appName == null ? lowercaseClassName(handlerClass) : appName; ctx = overrideRule(null, this.appMetaInfNamespace, this.appName); handler = instantiate(ctx, handlerClass); handler.setAppContext(ctx); handler.setAppName(appName); setProperties(ctx, handler); } /** * {@link SpringAsyncModularApp} constructor. Service name and search paths will * found reflectively (see {@link #SpringAsyncModularApp(Class, Class, String, String, String)}). * * @param targetModuleClass module type */ public SpringAsyncModularApp(@Nonnull final Class targetModuleClass, @Nonnull final Class handlerClass) { this(targetModuleClass, handlerClass, null, null, null); } public SpringAsyncModularApp(@Nonnull final Class targetModuleClass, @Nonnull final Class handlerClass, @Nonnull final String appName) { this(targetModuleClass, handlerClass, appName, null, null); } /** * Searches for plugin definitions in classpath and instantiates them. */ @Override protected void onStart() throws Exception { final Map properties = finder.mapAvailableProperties(moduleMetaInfNamespace); final Collection>> descriptors = new ArrayList<>(); for (final Entry e : properties.entrySet()) { final String moduleFileName = e.getKey(); final Properties moduleProperties = e.getValue(); if (!moduleFileName.endsWith(PROPERTIES_EXTENSION)) { continue; } final String moduleName = moduleFileName.substring(0, moduleFileName.lastIndexOf('.')); if (!moduleProperties.containsKey("class")) { LOG.warn("Unable to process module '{}' - no class property found", moduleName); continue; } final String moduleClassName = moduleProperties.getProperty("class"); final Class moduleClazz; try { moduleClazz = Class.forName(moduleClassName); if (!targetModuleClass.isAssignableFrom(moduleClazz)) { LOG.error("Unable to load module '{}' class '{}' is not subclass of '{}'", moduleName, moduleClazz.getCanonicalName(), targetModuleClass.getCanonicalName()); continue; } } catch (final ClassNotFoundException ex) { LOG.warn(ex, "Unable to load module '{}' class '{}' was not found", moduleName, moduleClassName); continue; } final Class finalClass = moduleClazz.asSubclass(targetModuleClass); final ConfigurableApplicationContext moduleContext = overrideRule(ctx, moduleMetaInfNamespace, moduleName); try { moduleContext.getBean(IDependencyProvider.class); throw new IllegalStateException("IDependencyProvider already defined"); } catch (NoSuchBeanDefinitionException ignored) { } descriptors.add(new ModuleDescriptor<>( moduleName, finalClass, moduleProperties, moduleContext )); } final Map, ModuleDescriptor>> unitializedModules = CollectionUtils.toMap(new LinkedHashMap<>(), descriptors, ModuleDescriptor::getModule); final Collection initializedModules = new ArrayList<>(); for (final Class clazz : getResolutionOrder(unitializedModules.keySet())) { final ModuleDescriptor> descriptor = unitializedModules.get(clazz); final ConfigurableApplicationContext ctx = descriptor.getContext(); handler.onModuleDiscovered(descriptor.getModule(), ctx, descriptor.getProperties(), descriptor.getName()); try { final IDependencyProvider bean = ctx.getBean(IDependencyProvider.class); throw new IllegalStateException("IDependencyProvider already injected! (" + bean + ')'); } catch (NoSuchBeanDefinitionException ignored) { } for (final Class dependencyClass : getDependencies(clazz)) { final AbstractDependencyProvider provider = new AbstractDependencyProvider<>(); final List instantiatedDependencies = initializedModules.stream().filter(dep -> IntrospectionUtils.instanceOf(dep.getClass(), dependencyClass)) .collect(Collectors.toList()); if (!instantiatedDependencies.isEmpty()) { provider.setModules(instantiatedDependencies); registerSingleton(ctx, dependencyClass.getCanonicalName(), provider, Dependency.class, dependencyClass); } } final M module = instantiate(ctx, clazz); LOG.debug("Module {}(class={}) loaded", descriptor.getName(), clazz.getCanonicalName()); handler.onModuleCreated(module, ctx, descriptor.getProperties(), descriptor.getName()); initializedModules.add(module); } handler.onModuleDiscoveryFinished(); LOG.info("Service started: {}", this.getClass().getCanonicalName()); } static Map, Set>> getDependencyMatrix(final Collection> modules) { final Map, Set>> dependencyMatrix = new LinkedHashMap<>(); for (final Class module : modules) { if (dependencyMatrix.containsKey(module)) { throw new IllegalStateException(); } dependencyMatrix.put(module, new LinkedHashSet<>()); for (final Class dep : getDependencies(module)) { modules.stream().filter(m -> instanceOf(m, dep)) .forEach(m -> dependencyMatrix.get(module).add(m)); } } return dependencyMatrix; } static List> getResolutionOrder(final Collection> modules) { final Map, Set>> dependencyMatrix = getDependencyMatrix(modules); final List> instantiationOrder = new ArrayList<>(); int size; do { size = dependencyMatrix.size(); final Iterator, Set>>> iter = dependencyMatrix.entrySet().iterator(); while (iter.hasNext()) { final Entry, Set>> e = iter.next(); if (instantiationOrder.containsAll(e.getValue())) { iter.remove(); instantiationOrder.add(e.getKey()); } } } while (size != dependencyMatrix.size()); if (!dependencyMatrix.isEmpty()) { throw new IllegalArgumentException("Cycle references found " + dependencyMatrix); } return instantiationOrder; } static Set> getDependencies(final Class clazz) { final Set> dependencies = new LinkedHashSet<>(); final Set> constructors = searchConstructors(clazz, ctor -> ctor.isAnnotationPresent(Inject.class) && !ctor.isSynthetic()); if (constructors.size() > 1) { throw new IllegalStateException("More than one injectable constructor found for " + clazz); } for (final Constructor constructor : constructors) { int paramIndex = 0; final Map, Class>> values = new LinkedHashMap<>(); final Annotation[][] annotations = constructor.getParameterAnnotations(); for (final Type type : constructor.getGenericParameterTypes()) { final Class typeClass = IntrospectionUtils.getClass(type); if (instanceOf(typeClass, IDependencyProvider.class)) { if (instanceOf(type, ParameterizedType.class)) { final Type hold = ((ParameterizedType) type).getActualTypeArguments()[0]; final Class heldClass = IntrospectionUtils.getClass(hold); if (heldClass == null) { throw new IllegalStateException("Missing type information for dependency provider of " + constructor.getDeclaringClass().getCanonicalName() + " module"); } Class value = null; for (final Annotation a : annotations[paramIndex]) { if (a instanceof Dependency) { value = ((Dependency) a).value(); break; } } values.put(paramIndex, new ImmutablePair<>(heldClass, value)); if (!dependencies.add(heldClass)) { throw new IllegalArgumentException("Duplicate " + heldClass + " providers found for " + clazz); } } else { throw new IllegalStateException("Unsupported type information found for dependency provider of " + constructor.getDeclaringClass().getCanonicalName() + " module (no type specified?)"); } } paramIndex++; } // ensure spring will be able to inject dependencies properly final Iterator, Class>>> i = values.entrySet().iterator(); if (values.size() == 1) { final Entry, Class>> e = i.next(); final Entry, Class> pair = e.getValue(); if (pair.getValue() != null && !pair.getKey().equals(pair.getValue())) { throw parameterQualifierMismatch(constructor, e.getKey(), pair.getKey()); } } else if (values.size() > 1) { while (i.hasNext()) { final Entry, Class>> e = i.next(); final Entry, Class> pair = e.getValue(); if (!pair.getKey().equals(pair.getValue())) { throw parameterQualifierMismatch(constructor, e.getKey(), pair.getKey()); } } } } return dependencies; } private static IllegalStateException parameterQualifierMismatch(final Constructor c, final int index, final Class expected) { return new IllegalStateException("Parameter " + index + " of " + c + " must be annotated with @Dependency(" + expected.getSimpleName() + ".class)"); } @Override protected void onPlannedStop() { handler.onApplicationStop(); } @Override protected void onForcedShutdown() { LOG.info("Externally requested termination, performing onPlannedStop()"); onPlannedStop(); } @Nonnull Class getTargetModuleClass() { return targetModuleClass; } @Nonnull String getAppMetaInfNamespace() { return appMetaInfNamespace; } @Nonnull String getModuleMetaInfNamespace() { return moduleMetaInfNamespace; } @Nonnull protected String getAppName() { return appName; } @Nonnull public H getHandler() { return handler; } @Nonnull @SuppressWarnings("ConstantConditions") private static ConfigurableApplicationContext overrideRule(@Nullable final ConfigurableApplicationContext ctx, @Nonnull final String namespace, @Nonnull final String name) { final String parentPath = concat(META_INF, namespace, name + DEFAULT_CONTEXT_POSTFIX); final String childPath = concat(namespace, name + CONTEXT_POSTFIX); if (!isResourceExists(parentPath)) { throw new IllegalArgumentException("Unable to create parent context for " + parentPath); } if (isResourceExists(childPath)) { return ensureCreate(ctx, parentPath, childPath); } LOG.warn("Unable to override {} with {}", parentPath, childPath); return ensureCreate(ctx, parentPath); } //TODO: move to utility classes? @Nonnull static String concat(final String... paths) { if (paths.length == 0) { return ""; } File file = new File(paths[0]); for (int i = 1; i < paths.length ; i++) { file = new File(file, paths[i]); } return file.getPath(); } private static String getPackage(@Nonnull final Class clazz) { return toValidPath(clazz.getPackage().getName()); } @Nonnull private static String lowercaseClassName(@Nonnull final Class clazz) { if (clazz.isAnonymousClass()) { return lowercaseClassName(clazz.getEnclosingClass()); } final String str = clazz.getSimpleName(); return str.substring(0, 1).toLowerCase() + str.substring(1); } @Nonnull private static String toValidPath(@Nonnull final String str) { return str.endsWith("/") ? str : (str + '/'); } private static boolean isResourceExists(@Nonnull final String path) { return new ClassPathResource(path).exists(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy