package tc.oc.commons.core.inject; import java.lang.annotation.Annotation; import java.util.Optional; import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.Key; import com.google.inject.OutOfScopeException; import com.google.inject.Provider; import com.google.inject.ProvisionException; import com.google.inject.Scope; import com.google.inject.Scopes; import com.google.inject.TypeLiteral; import tc.oc.commons.core.reflect.ResolvableType; import tc.oc.commons.core.util.Threadable; import tc.oc.commons.core.util.ThrowingRunnable; import tc.oc.commons.core.util.ThrowingSupplier; /** * A {@link Scope} that stores and retrieves objects from a thread-local {@link InjectionStore}. */ public abstract class InjectionScope<A extends Annotation> implements Scope { private final Class<A> annotation = (Class<A>) new ResolvableType<A>(){}.in(getClass()).getRawType(); private final Key<InjectionScope<A>> scopeKey = Key.get(new ResolvableType<InjectionScope<A>>(){}.in(getClass())); private final Key<InjectionStore<A>> storeKey = Key.get(new ResolvableType<InjectionStore<A>>(){}.in(getClass())); private final Threadable<InjectionStore<A>> current = new Threadable<>(); public Key<InjectionScope<A>> scopeKey() { return scopeKey; } public Key<InjectionStore<A>> storeKey() { return storeKey; } public <T> void bindSeeded(Binder binder, Key<T> key) { binder.bind(key).toProvider(() -> { throw new ProvisionException("Missing seed instance for " + key); }).in(annotation); } public boolean isScoped(Binding<?> binding) { return Scopes.isScoped(binding, this, annotation); } public <E extends Throwable> void withCurrentStore(InjectionStore<A> store, ThrowingRunnable<E> block) throws E { current.let(store, block); } public <T, E extends Throwable> T withCurrentStore(InjectionStore<A> store, ThrowingSupplier<T, E> block) throws E { return current.let(store, block); } /** * Return the current store for the given key on the current thread, or null if there * is no store in scope. * * The base method returns the current thread-local store and ignores the key. Subclasses * can override this to provide other methods of resolving the scope. */ protected Optional<InjectionStore<A>> currentStore(Key<?> key) { return Optional.ofNullable(current.get()); } public <T> Optional<T> currentInstance(Key<T> key) { return currentStore(key).map(store -> store.provide(key)); } public <T> Optional<T> currentInstance(TypeLiteral<T> type) { return currentInstance(Key.get(type)); } public <T> Optional<T> currentInstance(Class<T> type) { return currentInstance(Key.get(type)); } @Override public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) { return () -> { final InjectionStore<A> store = currentStore(key).orElseThrow( () -> new OutOfScopeException("Cannot provide " + key + " outside of " + annotation.getSimpleName() + " scope") ); // If the current store already contains the key, return its value. // Otherwise, provision an unscoped value and add it to the store. // If the key is of the store itself, just use the store we have. return store.provide(key, () -> storeKey.equals(key) ? (T) store : unscoped.get()); }; } }