diff options
5 files changed, 308 insertions, 33 deletions
diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java index 7f86973..f398f71 100644 --- a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java @@ -18,24 +18,27 @@ package com.tavianator.sangria.contextual; import java.lang.annotation.Annotation; -import javax.annotation.Nullable; +import java.util.*; 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.ConfigurationException; 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.BindingTargetVisitor; import com.google.inject.spi.Dependency; import com.google.inject.spi.DependencyAndSource; import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ProviderInstanceBinding; +import com.google.inject.spi.ProviderWithExtensionVisitor; import com.google.inject.spi.ProvisionListener; -import com.google.inject.util.Providers; import com.tavianator.sangria.core.DelayedError; @@ -137,7 +140,7 @@ public class ContextSensitiveBinder { public void toContextSensitiveProvider(Key<? extends ContextSensitiveProvider<? extends T>> key) { error.cancel(); - binder.bind(bindingKey).toProvider(new ProviderAdapter<>(key)); + binder.bind(bindingKey).toProvider(new ProviderKeyAdapter<>(key)); binder.bindListener(new BindingMatcher(bindingKey), new Trigger(bindingKey)); } @@ -145,7 +148,7 @@ public class ContextSensitiveBinder { public void toContextSensitiveProvider(ContextSensitiveProvider<? extends T> provider) { error.cancel(); - binder.bind(bindingKey).toProvider(new ProviderAdapter<>(provider)); + binder.bind(bindingKey).toProvider(new ProviderInstanceAdapter<>(provider)); binder.bindListener(new BindingMatcher(bindingKey), new Trigger(bindingKey)); // Match the behaviour of LinkedBindingBuilder#toProvider(Provider) binder.requestInjection(provider); @@ -155,66 +158,143 @@ public class ContextSensitiveBinder { /** * Adapter from {@link ContextSensitiveProvider} to {@link Provider}. */ - private static class ProviderAdapter<T> implements Provider<T> { + private static abstract class ProviderAdapter<T> implements ProviderWithExtensionVisitor<T> { private static final ThreadLocal<InjectionPoint> CURRENT_CONTEXT = new ThreadLocal<>(); - private final Object equalityKey; - private final @Nullable Key<? extends ContextSensitiveProvider<? extends T>> providerKey; - private Provider<? extends ContextSensitiveProvider<? extends T>> provider; + static void pushContext(InjectionPoint ip) { + CURRENT_CONTEXT.set(ip); + } - ProviderAdapter(Key<? extends ContextSensitiveProvider<? extends T>> providerKey) { - this.equalityKey = providerKey; - this.providerKey = providerKey; + static void popContext() { + CURRENT_CONTEXT.remove(); } - ProviderAdapter(ContextSensitiveProvider<T> provider) { - this.equalityKey = provider; - this.providerKey = null; - this.provider = Providers.of(provider); + @Override + public T get() { + InjectionPoint ip = CURRENT_CONTEXT.get(); + if (ip != null) { + return delegate().getInContext(ip); + } else { + return delegate().getInUnknownContext(); + } + } + + abstract ContextSensitiveProvider<? extends T> delegate(); + + // Have to implement equals()/hashCode() to support binding de-duplication + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); + } + + private static class ProviderKeyAdapter<T> extends ProviderAdapter<T> implements ContextSensitiveProviderKeyBinding<T> { + private final Key<? extends ContextSensitiveProvider<? extends T>> providerKey; + private Provider<? extends ContextSensitiveProvider<? extends T>> provider; + + ProviderKeyAdapter(Key<? extends ContextSensitiveProvider<? extends T>> providerKey) { + this.providerKey = providerKey; } @Inject void inject(Injector injector) { - if (provider == null) { - provider = injector.getProvider(providerKey); + provider = injector.getProvider(providerKey); + } + + @Override + ContextSensitiveProvider<? extends T> delegate() { + return provider.get(); + } + + @Override + public <B, V> V acceptExtensionVisitor(BindingTargetVisitor<B, V> visitor, ProviderInstanceBinding<? extends B> binding) { + if (visitor instanceof ContextSensitiveBindingVisitor) { + return ((ContextSensitiveBindingVisitor<T, V>)visitor).visit(this); + } else { + return visitor.visit(binding); } } - static void pushContext(InjectionPoint ip) { - CURRENT_CONTEXT.set(ip); + @Override + public Key<? extends ContextSensitiveProvider<? extends T>> getContextSensitiveProviderKey() { + return providerKey; } - static void popContext() { - CURRENT_CONTEXT.remove(); + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (!(obj instanceof ProviderKeyAdapter)) { + return false; + } + + ProviderKeyAdapter<?> other = (ProviderKeyAdapter<?>)obj; + return providerKey.equals(other.providerKey); } @Override - public T get() { - ContextSensitiveProvider<? extends T> delegate = provider.get(); - InjectionPoint ip = CURRENT_CONTEXT.get(); - if (ip != null) { - return delegate.getInContext(ip); + public int hashCode() { + return Objects.hashCode(providerKey); + } + } + + private static class ProviderInstanceAdapter<T> extends ProviderAdapter<T> implements ContextSensitiveProviderInstanceBinding<T> { + private final ContextSensitiveProvider<? extends T> instance; + private Set<InjectionPoint> injectionPoints; + + ProviderInstanceAdapter(ContextSensitiveProvider<? extends T> instance) { + this.instance = instance; + + Set<InjectionPoint> injectionPoints; + try { + injectionPoints = InjectionPoint.forInstanceMethodsAndFields(instance.getClass()); + } catch (ConfigurationException e) { + // We can ignore the error, the earlier requestInjection(instance) call will have reported it + injectionPoints = e.getPartialValue(); + } + this.injectionPoints = injectionPoints; + } + + @Override + ContextSensitiveProvider<? extends T> delegate() { + return instance; + } + + @Override + public <B, V> V acceptExtensionVisitor(BindingTargetVisitor<B, V> visitor, ProviderInstanceBinding<? extends B> binding) { + if (visitor instanceof ContextSensitiveBindingVisitor) { + return ((ContextSensitiveBindingVisitor<T, V>)visitor).visit(this); } else { - return delegate.getInUnknownContext(); + return visitor.visit(binding); } } - // Have to implement equals()/hashCode() here to support binding de-duplication + @Override + public ContextSensitiveProvider<? extends T> getContextSensitiveProviderInstance() { + return instance; + } + + @Override + public Set<InjectionPoint> getInjectionPoints() { + return injectionPoints; + } + @Override public boolean equals(Object obj) { if (obj == this) { return true; - } else if (!(obj instanceof ProviderAdapter)) { + } else if (!(obj instanceof ProviderInstanceAdapter)) { return false; } - ProviderAdapter<?> other = (ProviderAdapter<?>)obj; - return equalityKey.equals(other.equalityKey); + ProviderInstanceAdapter<?> other = (ProviderInstanceAdapter<?>)obj; + return instance.equals(other.instance); } @Override public int hashCode() { - return Objects.hashCode(equalityKey); + return Objects.hashCode(instance); } } diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingVisitor.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingVisitor.java new file mode 100644 index 0000000..59a3864 --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingVisitor.java @@ -0,0 +1,45 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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 com.google.inject.spi.BindingTargetVisitor; + +/** + * Visitor interface for the context-sensitive binding SPI. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public interface ContextSensitiveBindingVisitor<T, V> extends BindingTargetVisitor<T, V> { + /** + * Visit a {@link ContextSensitiveProviderKeyBinding}. + * + * @param binding The binding to visit. + * @return A value of type {@code V}. + */ + V visit(ContextSensitiveProviderKeyBinding<? extends T> binding); + + /** + * Visit a {@link ContextSensitiveProviderKeyBinding}. + * + * @param binding The binding to visit. + * @return A value of type {@code V}. + */ + V visit(ContextSensitiveProviderInstanceBinding<? extends T> binding); +} diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderInstanceBinding.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderInstanceBinding.java new file mode 100644 index 0000000..2fb3353 --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderInstanceBinding.java @@ -0,0 +1,25 @@ +package com.tavianator.sangria.contextual; + +import java.util.*; + +import com.google.inject.spi.InjectionPoint; + +/** + * SPI for {@link ContextSensitiveProvider} key bindings. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @see ContextSensitiveBindingBuilder#toContextSensitiveProvider(ContextSensitiveProvider) + * @since 1.0 + */ +public interface ContextSensitiveProviderInstanceBinding<T> { + /** + * @return The {@link ContextSensitiveProvider} instance for this binding. + */ + ContextSensitiveProvider<? extends T> getContextSensitiveProviderInstance(); + + /** + * @return The field and method {@link InjectionPoint}s of the {@link ContextSensitiveProvider} instance. + */ + Set<InjectionPoint> getInjectionPoints(); +} diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderKeyBinding.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderKeyBinding.java new file mode 100644 index 0000000..cdf025f --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProviderKeyBinding.java @@ -0,0 +1,35 @@ +/**************************************************************************** + * Sangria * + * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> * + * * + * 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 com.google.inject.Key; + +/** + * SPI for {@link ContextSensitiveProvider} key bindings. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + * @see ContextSensitiveBindingBuilder#toContextSensitiveProvider(Key) + */ +public interface ContextSensitiveProviderKeyBinding<T> { + /** + * @return The {@link Key} used to retrieve the {@link ContextSensitiveProvider}'s binding. + */ + Key<? extends ContextSensitiveProvider<? extends T>> getContextSensitiveProviderKey(); +} diff --git a/sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java b/sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java index 3463b0e..1e2ab49 100644 --- a/sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java +++ b/sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java @@ -17,10 +17,12 @@ package com.tavianator.sangria.contextual; +import java.util.*; import javax.inject.Inject; import javax.inject.Named; import com.google.inject.AbstractModule; +import com.google.inject.Binding; import com.google.inject.CreationException; import com.google.inject.Guice; import com.google.inject.Injector; @@ -30,6 +32,9 @@ import com.google.inject.Provider; import com.google.inject.ProvisionException; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; +import com.google.inject.spi.DefaultBindingTargetVisitor; +import com.google.inject.spi.Element; +import com.google.inject.spi.Elements; import com.google.inject.spi.InjectionPoint; import org.junit.Rule; import org.junit.Test; @@ -49,6 +54,10 @@ public class ContextSensitiveBinderTest { public @Rule ExpectedException thrown = ExpectedException.none(); private static class SelfProvider implements ContextSensitiveProvider<String> { + // For testing getInjectionPoints() in the SPI below + @SuppressWarnings("unused") + @Inject Injector injector; + @Override public String getInContext(InjectionPoint injectionPoint) { return injectionPoint.getDeclaringType().getRawType().getSimpleName(); @@ -138,7 +147,7 @@ public class ContextSensitiveBinderTest { } @Test - public void testDeDuplication() { + public void testKeyDeDuplication() { Injector injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() { @@ -153,6 +162,30 @@ public class ContextSensitiveBinderTest { .toContextSensitiveProvider(SelfProvider.class); } }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("<unknown>")); + } + + @Test + public void testInstanceDeDuplication() { + final SelfProvider selfProvider = new SelfProvider(); + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder contextualBinder = ContextSensitiveBinder.create(binder()); + contextualBinder + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(selfProvider); + contextualBinder + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(selfProvider); + } + }); + HasSelf hasSelf = injector.getInstance(HasSelf.class); assertThat(hasSelf.self, equalTo("HasSelf")); assertThat(hasSelf.selfProvider.get(), equalTo("<unknown>")); @@ -272,4 +305,61 @@ public class ContextSensitiveBinderTest { } }); } + + private static class TestVisitor<T> extends DefaultBindingTargetVisitor<T, Boolean> implements ContextSensitiveBindingVisitor<T, Boolean> { + @Override + public Boolean visit(ContextSensitiveProviderKeyBinding<? extends T> binding) { + assertThat(binding.getContextSensitiveProviderKey().equals(new Key<SelfProvider>() { }), is(true)); + return true; + } + + @Override + public Boolean visit(ContextSensitiveProviderInstanceBinding<? extends T> binding) { + assertThat(binding.getContextSensitiveProviderInstance(), instanceOf(SelfProvider.class)); + assertThat(binding.getInjectionPoints(), hasSize(1)); + return true; + } + + @Override + protected Boolean visitOther(Binding<? extends T> binding) { + return false; + } + } + + private <T> boolean visit(Binding<T> binding) { + return binding.acceptTargetVisitor(new TestVisitor<T>()); + } + + @Test + public void testExtensionSpi() { + List<Element> elements = Elements.getElements(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder contextualBinder = ContextSensitiveBinder.create(binder()); + contextualBinder + .bind(String.class) + .annotatedWith(Names.named("key")) + .toContextSensitiveProvider(SelfProvider.class); + contextualBinder + .bind(String.class) + .annotatedWith(Names.named("instance")) + .toContextSensitiveProvider(new SelfProvider()); + } + }); + + int passed = 0; + for (Element element : elements) { + if (element instanceof Binding) { + if (visit(((Binding<?>)element))) { + ++passed; + } + } + } + assertThat(passed, equalTo(2)); + + Injector injector = Guice.createInjector(Elements.getModule(elements)); + assertThat(visit(injector.getBinding(new Key<String>(Names.named("key")) { })), is(true)); + assertThat(visit(injector.getBinding(new Key<String>(Names.named("instance")) { })), is(true)); + assertThat(visit(injector.getBinding(SelfProvider.class)), is(false)); + } } |