From 1ce3feb50ef042c52233f0703d9fca708b0d5e30 Mon Sep 17 00:00:00 2001 From: Tavian Barnes Date: Tue, 1 Apr 2014 18:14:54 -0400 Subject: Add sangria-contextual module. --- pom.xml | 7 + sangria-contextual/pom.xml | 58 +++++ .../AnnotatedContextSensitiveBindingBuilder.java | 33 +++ .../sangria/contextual/ContextSensitiveBinder.java | 276 +++++++++++++++++++++ .../contextual/ContextSensitiveBindingBuilder.java | 44 ++++ .../contextual/ContextSensitiveProvider.java | 55 ++++ .../contextual/ContextSensitiveBinderTest.java | 269 ++++++++++++++++++++ 7 files changed, 742 insertions(+) create mode 100644 sangria-contextual/pom.xml create mode 100644 sangria-contextual/src/main/java/com/tavianator/sangria/contextual/AnnotatedContextSensitiveBindingBuilder.java create mode 100644 sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java create mode 100644 sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingBuilder.java create mode 100644 sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProvider.java create mode 100644 sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java diff --git a/pom.xml b/pom.xml index 12f28ae..72a158c 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,12 @@ 1.0-SNAPSHOT + + com.tavianator + sangria-contextual + 1.0-SNAPSHOT + + com.google.inject guice @@ -185,5 +191,6 @@ sangria-core + sangria-contextual diff --git a/sangria-contextual/pom.xml b/sangria-contextual/pom.xml new file mode 100644 index 0000000..7907f8c --- /dev/null +++ b/sangria-contextual/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.tavianator + sangria + 1.0-SNAPSHOT + + + sangria-contextual + jar + Sangria Contextual + Context-sensitive providers + + + + com.tavianator + sangria-core + + + + com.google.inject + guice + + + + com.google.guava + guava + + + + com.google.code.findbugs + jsr305 + true + + + + junit + junit + test + + + + org.hamcrest + hamcrest-integration + test + + + + org.mockito + mockito-core + test + + + diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/AnnotatedContextSensitiveBindingBuilder.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/AnnotatedContextSensitiveBindingBuilder.java new file mode 100644 index 0000000..d313bd6 --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/AnnotatedContextSensitiveBindingBuilder.java @@ -0,0 +1,33 @@ +/********************************************************************* + * Sangria * + * Copyright (C) 2014 Tavian Barnes * + * * + * This library is free software. It comes without any warranty, to * + * the extent permitted by applicable law. You can redistribute it * + * and/or modify it under the terms of the Do What The Fuck You Want * + * To Public License, Version 2, as published by Sam Hocevar. See * + * the COPYING file or http://www.wtfpl.net/ for more details. * + *********************************************************************/ + +package com.tavianator.sangria.contextual; + +import java.lang.annotation.Annotation; + +/** + * See the EDSL examples {@link ContextSensitiveBinder here}. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public interface AnnotatedContextSensitiveBindingBuilder extends ContextSensitiveBindingBuilder { + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + ContextSensitiveBindingBuilder annotatedWith(Class annotationType); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + ContextSensitiveBindingBuilder annotatedWith(Annotation annotation); +} 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 new file mode 100644 index 0000000..b3fe00f --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBinder.java @@ -0,0 +1,276 @@ +/********************************************************************* + * Sangria * + * Copyright (C) 2014 Tavian Barnes * + * * + * This library is free software. It comes without any warranty, to * + * the extent permitted by applicable law. You can redistribute it * + * and/or modify it under the terms of the Do What The Fuck You Want * + * To Public License, Version 2, as published by Sam Hocevar. See * + * the COPYING file or http://www.wtfpl.net/ for more details. * + *********************************************************************/ + +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); + } + } +} diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingBuilder.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingBuilder.java new file mode 100644 index 0000000..a558e8d --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveBindingBuilder.java @@ -0,0 +1,44 @@ +/********************************************************************* + * Sangria * + * Copyright (C) 2014 Tavian Barnes * + * * + * This library is free software. It comes without any warranty, to * + * the extent permitted by applicable law. You can redistribute it * + * and/or modify it under the terms of the Do What The Fuck You Want * + * To Public License, Version 2, as published by Sam Hocevar. See * + * the COPYING file or http://www.wtfpl.net/ for more details. * + *********************************************************************/ + +package com.tavianator.sangria.contextual; + +import com.google.inject.Key; +import com.google.inject.TypeLiteral; + +/** + * See the EDSL examples {@link ContextSensitiveBinder here}. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public interface ContextSensitiveBindingBuilder { + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(Class> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(TypeLiteral> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(Key> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(ContextSensitiveProvider provider); +} diff --git a/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProvider.java b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProvider.java new file mode 100644 index 0000000..7d368f1 --- /dev/null +++ b/sangria-contextual/src/main/java/com/tavianator/sangria/contextual/ContextSensitiveProvider.java @@ -0,0 +1,55 @@ +/********************************************************************* + * Sangria * + * Copyright (C) 2014 Tavian Barnes * + * * + * This library is free software. It comes without any warranty, to * + * the extent permitted by applicable law. You can redistribute it * + * and/or modify it under the terms of the Do What The Fuck You Want * + * To Public License, Version 2, as published by Sam Hocevar. See * + * the COPYING file or http://www.wtfpl.net/ for more details. * + *********************************************************************/ + +package com.tavianator.sangria.contextual; + +import com.google.inject.Provider; +import com.google.inject.spi.InjectionPoint; + +/** + * Like a {@link Provider}, but with knowledge of the target {@link InjectionPoint}. + * + *

+ * This interface, along with {@link ContextSensitiveBinder}, is useful for injecting custom logger types, among other + * things. However, context-sensitive injections can make maintenance and debugging more difficult. + *

+ * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public interface ContextSensitiveProvider { + /** + * Provide an instance of {@code T} for the given context. + * + * @param injectionPoint The {@link InjectionPoint} for this provision. + * @return An instance of {@code T}. + */ + T getInContext(InjectionPoint injectionPoint); + + /** + * Provide an instance of {@code T} for an unknown context. + *

+ * The {@link InjectionPoint} may not be known in all cases, for example if a {@code Provider} is used instead + * of + * a bare {@code T}. This method will be called in those cases. + *

+ *

+ * One reasonable implementation is to return a generically applicable instance, such as an anonymous logger. + * Another valid implementation is to throw an unchecked exception; in that case, {@code Provider} injections + * will fail. + *

+ * + * @return An instance of {@code T} + * @throws RuntimeException If injection without a context is not supported. + */ + T getInUnknownContext(); +} 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 new file mode 100644 index 0000000..24f073e --- /dev/null +++ b/sangria-contextual/src/test/java/com/tavianator/sangria/contextual/ContextSensitiveBinderTest.java @@ -0,0 +1,269 @@ +/********************************************************************* + * Sangria * + * Copyright (C) 2014 Tavian Barnes * + * * + * This library is free software. It comes without any warranty, to * + * the extent permitted by applicable law. You can redistribute it * + * and/or modify it under the terms of the Do What The Fuck You Want * + * To Public License, Version 2, as published by Sam Hocevar. See * + * the COPYING file or http://www.wtfpl.net/ for more details. * + *********************************************************************/ + +package com.tavianator.sangria.contextual; + +import javax.inject.Inject; +import javax.inject.Named; + +import com.google.inject.AbstractModule; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.MembersInjector; +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.InjectionPoint; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link ContextSensitiveBinder}. + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public class ContextSensitiveBinderTest { + public @Rule ExpectedException thrown = ExpectedException.none(); + + private static class SelfProvider implements ContextSensitiveProvider { + @Override + public String getInContext(InjectionPoint injectionPoint) { + return injectionPoint.getDeclaringType().getRawType().getSimpleName(); + } + + @Override + public String getInUnknownContext() { + return ""; + } + } + + private static class HasSelf { + @Inject @Named("self") String self; + @Inject @Named("self") Provider selfProvider; + } + + @Test + public void testProviderClass() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(SelfProvider.class); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("")); + } + + @Test + public void testProviderTypeLiteral() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(new TypeLiteral() { }); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("")); + } + + @Test + public void testProviderKey() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + bind(SelfProvider.class) + .annotatedWith(Names.named("self")) + .to(SelfProvider.class); + + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(new Key(Names.named("self")) { }); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("")); + } + + @Test + public void testProviderInstance() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(new SelfProvider()); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("")); + } + + @Test + public void testDeDuplication() { + 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.class); + contextualBinder + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(SelfProvider.class); + } + }); + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("")); + } + + private static class RequiredContextProvider implements ContextSensitiveProvider { + @Override + public String getInContext(InjectionPoint injectionPoint) { + return injectionPoint.getDeclaringType().getRawType().getSimpleName(); + } + + @Override + public String getInUnknownContext() { + throw new IllegalStateException("@Named(\"self\") injection not supported here"); + } + } + + @Test + public void testContextRequired() { + thrown.expect(ProvisionException.class); + + Injector injector = Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")) + .toContextSensitiveProvider(RequiredContextProvider.class); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + hasSelf.selfProvider.get(); + } + + private static class Recursive { + @Inject HasSelf hasSelf; + String self; + } + + private static class RecursiveProvider implements ContextSensitiveProvider { + @Inject MembersInjector membersInjector; + + @Override + public Recursive getInContext(InjectionPoint injectionPoint) { + Recursive result = new Recursive(); + membersInjector.injectMembers(result); + result.self = injectionPoint.getDeclaringType().getRawType().getSimpleName(); + return result; + } + + @Override + public Recursive getInUnknownContext() { + Recursive result = new Recursive(); + membersInjector.injectMembers(result); + result.self = ""; + return result; + } + } + + private static class HasRecursive { + @Inject Recursive recursive; + } + + @Test + public void testRecursiveProvision() { + 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.class); + contextualBinder + .bind(Recursive.class) + .toContextSensitiveProvider(new RecursiveProvider()); + } + }); + + HasRecursive hasRecursive = injector.getInstance(HasRecursive.class); + assertThat(hasRecursive.recursive.self, equalTo("HasRecursive")); + assertThat(hasRecursive.recursive.hasSelf.self, equalTo("HasSelf")); + + Recursive recursive = injector.getInstance(Recursive.class); + assertThat(recursive.self, equalTo("")); + assertThat(recursive.hasSelf.self, equalTo("HasSelf")); + } + + @Test + public void testIncompleteEdsl1() { + thrown.expect(CreationException.class); + thrown.expectMessage("Missing call to toContextSensitiveProvider() for java.lang.String"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class); + } + }); + } + + @Test + public void testIncompleteEdsl2() { + thrown.expect(CreationException.class); + thrown.expectMessage("Missing call to toContextSensitiveProvider() for java.lang.String annotated with " + + "@com.google.inject.name.Named(value=self)"); + + Guice.createInjector(new AbstractModule() { + @Override + protected void configure() { + ContextSensitiveBinder.create(binder()) + .bind(String.class) + .annotatedWith(Names.named("self")); + } + }); + } +} -- cgit v1.2.3