![JAR search and dependency download from the Maven repository](/logo.png)
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 extends M> 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 extends M> targetModuleClass,
@Nonnull final Class extends H> 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 extends M> targetModuleClass,
@Nonnull final Class extends H> handlerClass) {
this(targetModuleClass, handlerClass, null, null, null);
}
public SpringAsyncModularApp(@Nonnull final Class extends M> targetModuleClass,
@Nonnull final Class extends H> 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 extends M> 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 extends M> 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 extends T> 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 extends T> 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 extends M> 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