/**************************************************************************** * Sangria * * Copyright (C) 2014 Tavian Barnes * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * * limitations under the License. * ****************************************************************************/ package com.tavianator.sangria.contextual; import java.lang.annotation.Annotation; import javax.annotation.Nullable; import javax.inject.Inject; import com.google.common.base.Objects; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provider; import com.google.inject.TypeLiteral; import com.google.inject.matcher.AbstractMatcher; import com.google.inject.matcher.Matcher; import com.google.inject.spi.Dependency; import com.google.inject.spi.DependencyAndSource; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.ProvisionListener; import com.google.inject.util.Providers; import com.tavianator.sangria.core.DelayedError; /** * A binder for {@link ContextSensitiveProvider}s. * *

* For example, to bind a custom logger provider, you can write this inside {@link AbstractModule#configure()}: *

* *
 * ContextSensitiveBinder.create(binder())
 *         .bind(CustomLogger.class)
 *         .toContextSensitiveProvider(CustomLoggerProvider.class);
 * 
* * @author Tavian Barnes (tavianator@tavianator.com) * @version 1.0 * @since 1.0 */ public class ContextSensitiveBinder { private static final Class[] SKIPPED_SOURCES = { ContextSensitiveBinder.class, BindingBuilder.class, }; private final Binder binder; /** * Create a {@link ContextSensitiveBinder}. * * @param binder The {@link Binder} to use. * @return A {@link ContextSensitiveBinder} instance. */ public static ContextSensitiveBinder create(Binder binder) { return new ContextSensitiveBinder(binder); } private ContextSensitiveBinder(Binder binder) { this.binder = binder.skipSources(SKIPPED_SOURCES); } /** * See the EDSL examples {@link ContextSensitiveBinder here}. */ public AnnotatedContextSensitiveBindingBuilder bind(Class type) { return new BindingBuilder<>(Key.get(type)); } /** * See the EDSL examples {@link ContextSensitiveBinder here}. */ public AnnotatedContextSensitiveBindingBuilder bind(TypeLiteral type) { return new BindingBuilder<>(Key.get(type)); } /** * See the EDSL examples {@link ContextSensitiveBinder here}. */ public ContextSensitiveBindingBuilder bind(Key key) { return new BindingBuilder<>(key); } /** * Fluent binding builder implementation. */ private class BindingBuilder implements AnnotatedContextSensitiveBindingBuilder { private final Key key; private final DelayedError error; BindingBuilder(Key key) { this.key = key; this.error = DelayedError.create(binder, "Missing call to toContextSensitiveProvider() for %s", key); } @Override public ContextSensitiveBindingBuilder annotatedWith(Class annotationType) { error.cancel(); return new BindingBuilder<>(Key.get(key.getTypeLiteral(), annotationType)); } @Override public ContextSensitiveBindingBuilder annotatedWith(Annotation annotation) { error.cancel(); return new BindingBuilder<>(Key.get(key.getTypeLiteral(), annotation)); } @Override public void toContextSensitiveProvider(Class> type) { toContextSensitiveProvider(Key.get(type)); } @Override public void toContextSensitiveProvider(TypeLiteral> type) { toContextSensitiveProvider(Key.get(type)); } @Override public void toContextSensitiveProvider(Key> type) { error.cancel(); binder.bind(key).toProvider(new ProviderAdapter<>(type)); binder.bindListener(new BindingMatcher(key), new Trigger(key)); } @Override public void toContextSensitiveProvider(ContextSensitiveProvider provider) { error.cancel(); binder.bind(key).toProvider(new ProviderAdapter<>(provider)); binder.bindListener(new BindingMatcher(key), new Trigger(key)); // Match the behaviour of LinkedBindingBuilder#toProvider(Provider) binder.requestInjection(provider); } } /** * Adapter from {@link ContextSensitiveProvider} to {@link Provider}. */ private static class ProviderAdapter implements Provider { private static final ThreadLocal CURRENT_CONTEXT = new ThreadLocal<>(); private final Object equalityKey; private final @Nullable Key> providerKey; private Provider> provider; ProviderAdapter(Key> providerKey) { this.equalityKey = providerKey; this.providerKey = providerKey; } ProviderAdapter(ContextSensitiveProvider provider) { this.equalityKey = provider; this.providerKey = null; this.provider = Providers.of(provider); } @Inject void inject(Injector injector) { if (provider == null) { provider = injector.getProvider(providerKey); } } static void pushContext(InjectionPoint ip) { CURRENT_CONTEXT.set(ip); } static void popContext() { CURRENT_CONTEXT.remove(); } @Override public T get() { ContextSensitiveProvider delegate = provider.get(); InjectionPoint ip = CURRENT_CONTEXT.get(); if (ip != null) { return delegate.getInContext(ip); } else { return delegate.getInUnknownContext(); } } // Have to implement equals()/hashCode() here to support binding de-duplication @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof ProviderAdapter)) { return false; } ProviderAdapter other = (ProviderAdapter)obj; return equalityKey.equals(other.equalityKey); } @Override public int hashCode() { return Objects.hashCode(equalityKey); } } /** * {@link Matcher} for {@link Binding}s for specific {@link Key}s. */ private static class BindingMatcher extends AbstractMatcher> { private final Key key; BindingMatcher(Key key) { this.key = key; } @Override public boolean matches(Binding binding) { return key.equals(binding.getKey()); } } /** * {@link ProvisionListener} that sets up the current {@link InjectionPoint}. */ private static class Trigger implements ProvisionListener { private final Key key; Trigger(Key key) { this.key = key; } @Override public void onProvision(ProvisionInvocation provision) { for (DependencyAndSource dependencyAndSource : provision.getDependencyChain()) { Dependency dependency = dependencyAndSource.getDependency(); if (dependency != null && key.equals(dependency.getKey())) { try { ProviderAdapter.pushContext(dependency.getInjectionPoint()); provision.provision(); } finally { ProviderAdapter.popContext(); } break; } } } // Allow listeners to be de-duplicated @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof Trigger)) { return false; } Trigger other = (Trigger)obj; return key.equals(other.key); } @Override public int hashCode() { return Objects.hashCode(key); } } }