package tc.oc.pgm.module; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import javax.inject.Provider; import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.spi.Dependency; import com.google.inject.spi.ProviderWithDependencies; import tc.oc.commons.core.inject.HybridManifest; import tc.oc.commons.core.inject.Keys; import tc.oc.commons.core.reflect.ResolvableType; import tc.oc.commons.core.reflect.Types; import tc.oc.pgm.match.inject.MatchBinders; /** * Common manifest for map/match modules. * * Binds {@link Module} and {@link Optional< Module >} to a provider that ultimately calls * {@link #provisionModuleWithoutDependencies()} to provision the module instance. * The optional binding is whatever that method returns, and the direct binding * throws an exception if the optional is not present. The scoping for these * bindings is determined from the {@link Scope} parameter. * * Before trying to provision the module itself, any explicit dependencies * in a {@link ModuleDescription} are provisioned. If any necessary dependencies * do not load then no attempt is made to provision this module. * * @param <Base> Base type for all modules in the scope * @param <Scope> Scope annotation type * @param <Context> Context type * @param <Module> Type of this module */ public abstract class ModuleManifest< Base, Scope extends Annotation, Context extends ModuleContext<Base, Scope>, Module extends Base > extends HybridManifest implements MatchBinders { protected final TypeLiteral<Module> type; protected final Class<Module> rawType; protected final Class<Scope> scope; protected final String simpleName; protected final Key<Module> key; protected final Key<Optional<Module>> optionalKey; protected final Key<ProvisionWrapper<Module>> wrapperKey; protected final Key<Context> contextKey; protected final Key<ProvisionWrapper<? extends Base>> setElementKey; public ModuleManifest(@Nullable TypeLiteral<Module> type) { // Figure out the module type. If the given type is null, // then try to resolve it from this object's superclass. this.type = type != null ? Types.assertFullySpecified(type) : new ResolvableType<Module>(){}.in(getClass()); this.rawType = (Class<Module>) this.type.getRawType(); this.simpleName = rawType.getSimpleName(); // Resolve the scope type automatically this.scope = (Class<Scope>) new ResolvableType<Scope>(){}.in(getClass()).getRawType(); // Construct various keys related to the module/scope this.key = Key.get(this.type); this.optionalKey = Keys.optional(this.key); this.wrapperKey = ProvisionWrapper.keyOf(this.type); this.contextKey = Key.get(new ResolvableType<Context>(){}.in(getClass())); this.setElementKey = Key.get(new ResolvableType<ProvisionWrapper<? extends Base>>(){}.in(getClass())); } @Override protected void configure() { // Inject this object. A few subclasses rely on this. requestInjection(this); // Register the module in the big list of all modules inSet(setElementKey).addBinding().to(wrapperKey); // Bind the wrapper to the provider that does everything bind(wrapperKey).toProvider(new WrapperProvider()).in(scope); // Link M and Optional<M> to the wrapper final Provider<ProvisionWrapper<Module>> wrapperProvider = getProvider(wrapperKey); bind(optionalKey).toProvider(() -> wrapperProvider.get().optional()).in(scope); bind(key).toProvider(() -> wrapperProvider.get().require(null)).in(scope); } /** * Provisions any the explicit dependencies of the module (from a {@link ModuleDescription} annotation), * followed by the module itself. * * Also implements {@link com.google.inject.spi.HasDependencies}, though I have no idea * if that does anything useful. * * TODO: Replace all explicit dependencies with plain old injections so we don't need * all this complex reflection and logic. * * @see ModuleDescription * @see ProvisionWrapper */ private class WrapperProvider implements ProviderWithDependencies<ProvisionWrapper<Module>> { final Provider<Context> contextProvider = getProvider(contextKey); final Set<Dependency<?>> dependencies = new HashSet<>(); final List<Provider<? extends ProvisionWrapper<?>>> requires = new ArrayList<>(); final List<Provider<? extends ProvisionWrapper<?>>> depends = new ArrayList<>(); final List<Provider<? extends ProvisionWrapper<?>>> follows = new ArrayList<>(); WrapperProvider() { final ModuleDescription annotation = type.getRawType().getAnnotation(ModuleDescription.class); if(annotation != null) { for(Class<?> cls : annotation.requires()) { addDependency(requires, cls); } for(Class<?> cls : annotation.depends()) { addDependency(depends, cls); } for(Class<?> cls : annotation.follows()) { addDependency(follows, cls); } } } <T> void addDependency(List<Provider<? extends ProvisionWrapper<?>>> list, Class<T> module) { final Key<ProvisionWrapper<T>> key = ProvisionWrapper.keyOf(TypeLiteral.get(module)); final Provider<ProvisionWrapper<T>> provider = getProvider(key); dependencies.add(Dependency.get(key)); list.add(provider); } @Override public Set<Dependency<?>> getDependencies() { return dependencies; } @Override public ProvisionWrapper<Module> get() { // Note that all dependencies are provisioned, in the same order, // regardless of any intermediate result. This keeps the loading // process predictable, even when there are errors. boolean failed = false; boolean absent = false; // Required modules - These are expected to be present for(Provider<? extends ProvisionWrapper<?>> provider : requires) { final ProvisionWrapper<?> wrapper = provider.get(); switch(wrapper.result) { case FAILED: failed = true; break; default: wrapper.require(type); break; } } // Chained module - If any of these are absent, so are we for(Provider<? extends ProvisionWrapper<?>> provider : depends) { final ProvisionWrapper<?> wrapper = provider.get(); switch(wrapper.result) { case FAILED: failed = true; break; case ABSENT: absent = true; break; } } // Prior module - Provision these first, but we don't care if they are absent for(Provider<? extends ProvisionWrapper<?>> provider : follows) { final ProvisionWrapper<?> wrapper = provider.get(); switch(wrapper.result) { case FAILED: failed = true; break; } } if(absent) { // Decline to load because an upstream module also declined return new ProvisionWrapper<>(type, ProvisionResult.ABSENT); } else if(failed) { // Fail because an upstream module failed return new ProvisionWrapper<>(type, ProvisionResult.FAILED); } else { try { // Load this module and add it to the context (if present) return contextProvider.get() .loadModule(ModuleManifest.this::provisionModuleWithoutDependencies) .map(module -> new ProvisionWrapper<>(type, ProvisionResult.PRESENT, module)) .orElseGet(() -> new ProvisionWrapper<>(type, ProvisionResult.ABSENT)); } catch(UpstreamProvisionFailure e) { // Upstream module failed, we don't have to report this one failing too return new ProvisionWrapper<>(type, ProvisionResult.FAILED); } } } } /** * Provision the module, assuming explicit dependencies have already been provisioned * * @return The module instance, or empty if the module declines to load * * @throws ModuleLoadException If a *user* error occurred while loading the module i.e. * something that should be reported to the mapmaker. * The error is reported to the {@link ModuleContext} and * loading continues, skipping any modules dependent on this one. */ protected abstract Optional<Module> provisionModuleWithoutDependencies() throws ModuleLoadException; }