diff options
Diffstat (limited to 'sangria-contextual')
6 files changed, 735 insertions, 0 deletions
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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>com.tavianator</groupId> + <artifactId>sangria</artifactId> + <version>1.0-SNAPSHOT</version> + </parent> + + <artifactId>sangria-contextual</artifactId> + <packaging>jar</packaging> + <name>Sangria Contextual</name> + <description>Context-sensitive providers</description> + + <dependencies> + <dependency> + <groupId>com.tavianator</groupId> + <artifactId>sangria-core</artifactId> + </dependency> + + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + </dependency> + + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-integration</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> 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 <tavianator@tavianator.com> * + * * + * 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<T> extends ContextSensitiveBindingBuilder<T> { + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + ContextSensitiveBindingBuilder<T> annotatedWith(Class<? extends Annotation> annotationType); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + ContextSensitiveBindingBuilder<T> 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 <tavianator@tavianator.com> * + * * + * 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. + * + * <p> + * For example, to bind a custom logger provider, you can write this inside {@link AbstractModule#configure()}: + * </p> + * + * <pre> + * ContextSensitiveBinder.create(binder()) + * .bind(CustomLogger.class) + * .toContextSensitiveProvider(CustomLoggerProvider.class); + * </pre> + * + * @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 <T> AnnotatedContextSensitiveBindingBuilder<T> bind(Class<T> type) { + return new BindingBuilder<>(Key.get(type)); + } + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + public <T> AnnotatedContextSensitiveBindingBuilder<T> bind(TypeLiteral<T> type) { + return new BindingBuilder<>(Key.get(type)); + } + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + public <T> ContextSensitiveBindingBuilder<T> bind(Key<T> key) { + return new BindingBuilder<>(key); + } + + /** + * Fluent binding builder implementation. + */ + private class BindingBuilder<T> implements AnnotatedContextSensitiveBindingBuilder<T> { + private final Key<T> key; + private final DelayedError error; + + BindingBuilder(Key<T> key) { + this.key = key; + this.error = DelayedError.create(binder, "Missing call to toContextSensitiveProvider() for %s", key); + } + + @Override + public ContextSensitiveBindingBuilder<T> annotatedWith(Class<? extends Annotation> annotationType) { + error.cancel(); + return new BindingBuilder<>(Key.get(key.getTypeLiteral(), annotationType)); + } + + @Override + public ContextSensitiveBindingBuilder<T> annotatedWith(Annotation annotation) { + error.cancel(); + return new BindingBuilder<>(Key.get(key.getTypeLiteral(), annotation)); + } + + @Override + public void toContextSensitiveProvider(Class<? extends ContextSensitiveProvider<T>> type) { + toContextSensitiveProvider(Key.get(type)); + } + + @Override + public void toContextSensitiveProvider(TypeLiteral<? extends ContextSensitiveProvider<T>> type) { + toContextSensitiveProvider(Key.get(type)); + } + + @Override + public void toContextSensitiveProvider(Key<? extends ContextSensitiveProvider<T>> type) { + error.cancel(); + + binder.bind(key).toProvider(new ProviderAdapter<>(type)); + binder.bindListener(new BindingMatcher(key), new Trigger(key)); + } + + @Override + public void toContextSensitiveProvider(ContextSensitiveProvider<T> 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<T> implements Provider<T> { + private static final ThreadLocal<InjectionPoint> CURRENT_CONTEXT = new ThreadLocal<>(); + + private final Object equalityKey; + private final @Nullable Key<? extends ContextSensitiveProvider<T>> providerKey; + private Provider<? extends ContextSensitiveProvider<T>> provider; + + ProviderAdapter(Key<? extends ContextSensitiveProvider<T>> providerKey) { + this.equalityKey = providerKey; + this.providerKey = providerKey; + } + + ProviderAdapter(ContextSensitiveProvider<T> 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<T> 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<Binding<?>> { + 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 <T> void onProvision(ProvisionInvocation<T> 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 <tavianator@tavianator.com> * + * * + * 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<T> { + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(Class<? extends ContextSensitiveProvider<T>> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(TypeLiteral<? extends ContextSensitiveProvider<T>> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(Key<? extends ContextSensitiveProvider<T>> type); + + /** + * See the EDSL examples {@link ContextSensitiveBinder here}. + */ + void toContextSensitiveProvider(ContextSensitiveProvider<T> 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 <tavianator@tavianator.com> * + * * + * 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}. + * + * <p> + * 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. + * </p> + * + * @author Tavian Barnes (tavianator@tavianator.com) + * @version 1.0 + * @since 1.0 + */ +public interface ContextSensitiveProvider<T> { + /** + * 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. + * <p> + * The {@link InjectionPoint} may not be known in all cases, for example if a {@code Provider<T>} is used instead + * of + * a bare {@code T}. This method will be called in those cases. + * </p> + * <p> + * 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<T>} injections + * will fail. + * </p> + * + * @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 <tavianator@tavianator.com> * + * * + * 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<String> { + @Override + public String getInContext(InjectionPoint injectionPoint) { + return injectionPoint.getDeclaringType().getRawType().getSimpleName(); + } + + @Override + public String getInUnknownContext() { + return "<unknown>"; + } + } + + private static class HasSelf { + @Inject @Named("self") String self; + @Inject @Named("self") Provider<String> 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("<unknown>")); + } + + @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<SelfProvider>() { }); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("<unknown>")); + } + + @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<SelfProvider>(Names.named("self")) { }); + } + }); + + HasSelf hasSelf = injector.getInstance(HasSelf.class); + assertThat(hasSelf.self, equalTo("HasSelf")); + assertThat(hasSelf.selfProvider.get(), equalTo("<unknown>")); + } + + @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("<unknown>")); + } + + @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("<unknown>")); + } + + private static class RequiredContextProvider implements ContextSensitiveProvider<String> { + @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<Recursive> { + @Inject MembersInjector<Recursive> 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 = "<unknown>"; + 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("<unknown>")); + 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")); + } + }); + } +} |