package tc.oc.commons.core.inject; import java.util.LinkedHashSet; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.reflect.TypeToken; import tc.oc.commons.core.concurrent.ExceptionHandlingExecutor; import tc.oc.commons.core.exception.ExceptionHandler; import tc.oc.commons.core.util.AmbiguousElementException; import tc.oc.commons.core.util.CachingTypeMap; import static tc.oc.commons.core.IterableUtils.reverseForEach; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowConsumer; /** * A collection of {@link Facet}s, keyed by type, and ordered by dependency relationships. * Lifecycle callbacks can also be dispatched through this collection. * * Facets are bound into the collection using an appropriately typed {@link FacetBinder}. * * Dependencies are detected automatically from {@link Inject}ion points. */ public abstract class FacetContext<F extends Facet> { private final Class<F> baseType = (Class<F>) new TypeToken<F>(getClass()){}.getRawType(); private final Set<F> ordered = new LinkedHashSet<>(); private final CachingTypeMap<F, F> byType = CachingTypeMap.create(); private boolean initialized = false; /** * Facets often depend on the thing they are facets of, and that thing usually * depends on the facet context, so to avoid the circular dependency, we inject * the set as a Provider, and don't touch it until after injection, when the * context is enabled. */ @Inject private Provider<Set<F>> facets; private ExceptionHandlingExecutor exceptionHandlingExecutor; @Inject private void inject(ExceptionHandler exceptionHandler) { this.exceptionHandlingExecutor = () -> exceptionHandler; } private void discoverWithDependencies(Class<? extends F> type, Stack<F> stack) { final F facet = facet(type); if(!ordered.contains(facet)) { final int index = stack.indexOf(facet); if(index != -1) { // Guice supports this with proxies, but we don't, for now throw new IllegalStateException( "Circular facet dependency: " + Stream.concat(stack.subList(index, stack.size()).stream(), Stream.of(facet)) .map(f -> f.getClass().getSimpleName()) .collect(Collectors.joining(" -> ")) ); } stack.push(facet); try { Injection.dependencies(type).forEach(dependency -> { final Class<?> depType = Injection.dependencyType(dependency.getKey().getTypeLiteral()).getRawType(); if(baseType.isAssignableFrom(depType)) { discoverWithDependencies(depType.asSubclass(baseType), stack); } }); } finally { stack.pop(); } ordered.add(facet); discover(facet); } } private void init() { if(!initialized) { initialized = true; // First, dump all facets into a TypeMap so we can find them efficiently for(F facet : facets.get()) { byType.put((Class<? extends F>) facet.getClass(), facet); } // Then, discover all their dependencies and build a partial ordering. // We don't inject the dependencies, Guice has already done that, // we just need to know what order the facets should be enabled in. for(F facet : facets.get()) { discoverWithDependencies((Class<? extends F>) facet.getClass(), new Stack<>()); } } } public Set<F> all() { init(); return ordered; } public @Nullable <T extends F> T getOrNull(Class<T> type) { init(); try { return (T) byType.oneAssignableTo(type); } catch(NoSuchElementException e) { return null; } catch(AmbiguousElementException e) { throw new IllegalStateException("Multiple instances of facet " + type.getName() + " are loaded"); } } public <T extends F> Optional<T> facetMaybe(Class<T> type) { return Optional.ofNullable(getOrNull(type)); } public <T extends F> T facet(Class<T> type) { final T facet = getOrNull(type); if(facet == null) { throw new IllegalStateException("Facet " + type.getName() + " is not loaded"); } return facet; } public void enableAll() { exceptionHandlingExecutor.executeCatch(this::enableAllThrows); } public void disableAll() { exceptionHandlingExecutor.executeCatch(this::disableAllThrows); } public void enableAllThrows() throws Exception { all().forEach(rethrowConsumer(this::enableFacet)); } public void disableAllThrows() throws Exception { reverseForEach(all(), rethrowConsumer(this::disableFacet)); } protected void enableFacet(F facet) throws Exception { facet.enable(); } protected void disableFacet(F facet) throws Exception { facet.disable(); } protected void discover(F facet) {} }