diff options
author | Sam Berlin <sberlin@gmail.com> | 2015-02-25 11:49:14 -0500 |
---|---|---|
committer | Sam Berlin <sberlin@gmail.com> | 2015-02-25 11:49:14 -0500 |
commit | 156c8cc762fab971efb727c7ab107fa243be2fc9 (patch) | |
tree | eb87961ff032167d5997c055e797e83e217136b8 | |
parent | 7ddc816ddc30f328690193be6e70a476a71899e7 (diff) | |
parent | 85f14e03ee00b20e20fd0f018a00ef52fcf909b1 (diff) | |
download | guice-156c8cc762fab971efb727c7ab107fa243be2fc9.tar.gz |
Merge pull request #905 from google/moe_writing_branch_from_b605a34702d8d8112983aca891e3e2b6987ec45e
Merge internal changes
48 files changed, 2180 insertions, 150 deletions
diff --git a/build.properties b/build.properties index f1e04745..cca000c0 100644 --- a/build.properties +++ b/build.properties @@ -8,6 +8,7 @@ jmx.src.dir=extensions/jmx/src jndi.src.dir=extensions/jndi/src throwingproviders.src.dir=extensions/throwingproviders/src multibindings.src.dir=extensions/multibindings/src +daggeradapter.src.dir=extensions/dagger-adapter/src privatemodules.src.dir=extensions/privatemodules/src lifecycle.src.dir=extensions/lifecycle/src persist.src.dir=extensions/persist/src @@ -26,6 +27,7 @@ javadoc.packagenames=com.google.inject,com.google.inject.spi,\ com.google.inject.assistedinject,\ com.google.inject.throwingproviders,\ com.google.inject.multibindings,\ + com.google.inject.daggeradapter,\ com.google.inject.privatemodules,\ com.google.inject.util,\ com.google.inject.persist,\ @@ -10,9 +10,9 @@ </path> <path id="javadoc.classpath"> - <path refid="compile.classpath"/> + <path refid="compile.classpath"/> <fileset dir="extensions"> - <include name="*/lib/*.jar"/> + <include name="*/lib/*.jar"/> </fileset> <pathelement location="${build.dir}/classes"/> </path> @@ -35,6 +35,7 @@ <ant antfile="extensions/jndi/build.xml" target="distjars" inheritAll="false"/> <ant antfile="extensions/throwingproviders/build.xml" target="distjars" inheritAll="false"/> <ant antfile="extensions/multibindings/build.xml" target="distjars" inheritAll="false"/> + <ant antfile="extensions/dagger-adapter/build.xml" target="distjars" inheritAll="false"/> <ant antfile="extensions/persist/build.xml" target="distjars" inheritAll="false"/> <ant antfile="extensions/grapher/build.xml" target="distjars" inheritAll="false"/> <ant antfile="extensions/testlib/build.xml" target="distjars" inheritAll="false"/> @@ -64,6 +65,9 @@ <fileset dir="extensions/multibindings/build" includes="*.jar"/> </copy> <copy toDir="${build.dir}/dist"> + <fileset dir="extensions/dagger-adapter/build" includes="*.jar"/> + </copy> + <copy toDir="${build.dir}/dist"> <fileset dir="extensions/persist/build" includes="*.jar"/> </copy> <copy toDir="${build.dir}/dist"> @@ -159,6 +163,7 @@ <fileset dir="${jndi.src.dir}"/> <fileset dir="${throwingproviders.src.dir}"/> <fileset dir="${multibindings.src.dir}"/> + <fileset dir="${daggeradapter.src.dir}"/> <fileset dir="${persist.src.dir}"/> <fileset dir="${struts2.src.dir}"/> <fileset dir="${grapher.src.dir}"/> @@ -214,6 +219,9 @@ <group title="Multibinder Extension" packages="com.google.inject.multibindings"/> <fileset dir="${multibindings.src.dir}"/> + <group title="Dagger Adapter" packages="com.google.inject.daggeradapter"/> + <fileset dir="${daggeradapter.src.dir}"/> + <group title="ThrowingProviders Extension" packages="com.google.inject.throwingproviders"/> <fileset dir="${throwingproviders.src.dir}"/> @@ -302,6 +310,7 @@ <ant dir="extensions/jndi" antfile="build.xml" target="clean"/> <ant dir="extensions/throwingproviders" antfile="build.xml" target="clean"/> <ant dir="extensions/multibindings" antfile="build.xml" target="clean"/> + <ant dir="extensions/dagger-adapter" antfile="build.xml" target="clean"/> <ant dir="extensions/persist" antfile="build.xml" target="clean"/> <ant dir="extensions/grapher" antfile="build.xml" target="clean"/> <ant dir="extensions/testlib" antfile="build.xml" target="clean"/> diff --git a/core/src/com/google/inject/Binder.java b/core/src/com/google/inject/Binder.java index e8957593..e930c3be 100644 --- a/core/src/com/google/inject/Binder.java +++ b/core/src/com/google/inject/Binder.java @@ -22,6 +22,7 @@ import com.google.inject.binder.LinkedBindingBuilder; import com.google.inject.matcher.Matcher; import com.google.inject.spi.Dependency; import com.google.inject.spi.Message; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; import com.google.inject.spi.ProvisionListener; import com.google.inject.spi.TypeConverter; import com.google.inject.spi.TypeListener; @@ -507,4 +508,15 @@ public interface Binder { * @since 4.0 */ void requireExactBindingAnnotations(); + + /** + * Adds a scanner that will look in all installed modules for annotations the scanner can parse, + * and binds them like {@literal @}Provides methods. Scanners apply to all modules installed in + * the injector. Scanners installed in child injectors or private modules do not impact modules in + * siblings or parents, however scanners installed in parents do apply to all child injectors and + * private modules. + * + * @since 4.0 + */ + void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner); } diff --git a/core/src/com/google/inject/internal/InheritingState.java b/core/src/com/google/inject/internal/InheritingState.java index 7d1eb66e..db6a7a67 100644 --- a/core/src/com/google/inject/internal/InheritingState.java +++ b/core/src/com/google/inject/internal/InheritingState.java @@ -26,6 +26,7 @@ import com.google.inject.Binding; import com.google.inject.Key; import com.google.inject.Scope; import com.google.inject.TypeLiteral; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; import com.google.inject.spi.ProvisionListenerBinding; import com.google.inject.spi.ScopeBinding; import com.google.inject.spi.TypeConverterBinding; @@ -55,7 +56,8 @@ final class InheritingState implements State { private final List<MethodAspect> methodAspects = Lists.newArrayList(); /*end[AOP]*/ private final List<TypeListenerBinding> typeListenerBindings = Lists.newArrayList(); - private final List<ProvisionListenerBinding> provisionListenerBindings = Lists.newArrayList(); + private final List<ProvisionListenerBinding> provisionListenerBindings = Lists.newArrayList(); + private final List<ModuleAnnotatedMethodScannerBinding> scannerBindings = Lists.newArrayList(); private final WeakKeySet blacklistedKeys; private final Object lock; private final Object singletonCreationLock; @@ -138,8 +140,8 @@ final class InheritingState implements State { public List<TypeListenerBinding> getTypeListenerBindings() { List<TypeListenerBinding> parentBindings = parent.getTypeListenerBindings(); - List<TypeListenerBinding> result - = new ArrayList<TypeListenerBinding>(parentBindings.size() + 1); + List<TypeListenerBinding> result = + Lists.newArrayListWithCapacity(parentBindings.size() + typeListenerBindings.size()); result.addAll(parentBindings); result.addAll(typeListenerBindings); return result; @@ -151,13 +153,26 @@ final class InheritingState implements State { public List<ProvisionListenerBinding> getProvisionListenerBindings() { List<ProvisionListenerBinding> parentBindings = parent.getProvisionListenerBindings(); - List<ProvisionListenerBinding> result - = new ArrayList<ProvisionListenerBinding>(parentBindings.size() + 1); + List<ProvisionListenerBinding> result = + Lists.newArrayListWithCapacity(parentBindings.size() + provisionListenerBindings.size()); result.addAll(parentBindings); result.addAll(provisionListenerBindings); return result; } + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + scannerBindings.add(scanner); + } + + public List<ModuleAnnotatedMethodScannerBinding> getScannerBindings() { + List<ModuleAnnotatedMethodScannerBinding> parentBindings = parent.getScannerBindings(); + List<ModuleAnnotatedMethodScannerBinding> result = + Lists.newArrayListWithCapacity(parentBindings.size() + scannerBindings.size()); + result.addAll(parentBindings); + result.addAll(scannerBindings); + return result; + } + public void blacklist(Key<?> key, State state, Object source) { parent.blacklist(key, state, source); blacklistedKeys.add(key, state, source); diff --git a/core/src/com/google/inject/internal/InjectorShell.java b/core/src/com/google/inject/internal/InjectorShell.java index 61008798..5982bb39 100644 --- a/core/src/com/google/inject/internal/InjectorShell.java +++ b/core/src/com/google/inject/internal/InjectorShell.java @@ -35,6 +35,7 @@ import com.google.inject.spi.Dependency; import com.google.inject.spi.Element; import com.google.inject.spi.Elements; import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; import com.google.inject.spi.PrivateElements; import com.google.inject.spi.ProvisionListenerBinding; import com.google.inject.spi.TypeListenerBinding; @@ -131,6 +132,8 @@ final class InjectorShell { // bind Singleton if this is a top-level injector if (parent == null) { modules.add(0, new RootModule()); + } else { + modules.add(0, new InheritedScannersModule(parent.state)); } elements.addAll(Elements.getElements(stage, modules)); @@ -184,6 +187,9 @@ final class InjectorShell { new UntargettedBindingProcessor(errors, bindingData).process(injector, elements); stopwatch.resetAndLog("Binding creation"); + new ModuleAnnotatedMethodScannerProcessor(errors).process(injector, elements); + stopwatch.resetAndLog("Module annotated method scanners creation"); + List<InjectorShell> injectorShells = Lists.newArrayList(); injectorShells.add(new InjectorShell(this, elements, injector)); @@ -289,4 +295,18 @@ final class InjectorShell { binder.bindScope(javax.inject.Singleton.class, SINGLETON); } } + + private static class InheritedScannersModule implements Module { + private final State state; + + InheritedScannersModule(State state) { + this.state = state; + } + + public void configure(Binder binder) { + for (ModuleAnnotatedMethodScannerBinding binding : state.getScannerBindings()) { + binding.applyTo(binder); + } + } + } } diff --git a/core/src/com/google/inject/internal/InternalContext.java b/core/src/com/google/inject/internal/InternalContext.java index 71d953c5..6a6f7efc 100644 --- a/core/src/com/google/inject/internal/InternalContext.java +++ b/core/src/com/google/inject/internal/InternalContext.java @@ -43,11 +43,11 @@ final class InternalContext { /** * Keeps track of the hierarchy of types needed during injection. * - * <p>This is a pairwise combination of dependencies and sources, with dependencies on even - * indices, and sources on odd indices. This structure is to avoid the memory overhead of + * <p>This is a pairwise combination of dependencies and sources, with dependencies or keys on + * even indices, and sources on odd indices. This structure is to avoid the memory overhead of * DependencyAndSource objects, which can add to several tens of megabytes in large applications. */ - private final List<Object> state = Lists.newArrayList(); + private final DependencyStack state = new DependencyStack(); @SuppressWarnings("unchecked") public <T> ConstructionContext<T> getConstructionContext(Object key) { @@ -75,29 +75,41 @@ final class InternalContext { /** Pops the current state & sets the new dependency. */ public void popStateAndSetDependency(Dependency<?> newDependency) { - popState(); + state.pop(); this.dependency = newDependency; } /** Adds to the state without setting the dependency. */ public void pushState(Key<?> key, Object source) { - state.add(key == null ? null : Dependency.get(key)); + state.add(key); state.add(source); } /** Pops from the state without setting a dependency. */ public void popState() { - state.remove(state.size() - 1); - state.remove(state.size() - 1); + state.pop(); } /** Returns the current dependency chain (all the state). */ public List<DependencyAndSource> getDependencyChain() { ImmutableList.Builder<DependencyAndSource> builder = ImmutableList.builder(); for (int i = 0; i < state.size(); i += 2) { - builder.add(new DependencyAndSource( - (Dependency<?>) state.get(i), state.get(i + 1))); + Object evenEntry = state.get(i); + Dependency<?> dependency; + if (evenEntry instanceof Key) { + dependency = Dependency.get((Key<?>) evenEntry); + } else { + dependency = (Dependency<?>) evenEntry; + } + builder.add(new DependencyAndSource(dependency, state.get(i + 1))); } return builder.build(); } + + private static final class DependencyStack extends ArrayList<Object> { + void pop() { + int sz = size(); + removeRange(sz - 2, sz); + } + } } diff --git a/core/src/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java b/core/src/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java new file mode 100644 index 00000000..85c721d8 --- /dev/null +++ b/core/src/com/google/inject/internal/ModuleAnnotatedMethodScannerProcessor.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.internal; + +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; + +/** + * Handles {@code Binder.scanModulesForAnnotatedMethods} commands. + * + * @author sameb@google.com (Sam Berlin) + */ +final class ModuleAnnotatedMethodScannerProcessor extends AbstractProcessor { + + ModuleAnnotatedMethodScannerProcessor(Errors errors) { + super(errors); + } + + @Override public Boolean visit(ModuleAnnotatedMethodScannerBinding command) { + injector.state.addScanner(command); + return true; + } +} diff --git a/core/src/com/google/inject/internal/ProviderMethodsModule.java b/core/src/com/google/inject/internal/ProviderMethodsModule.java index 98eb45d3..7682dab5 100644 --- a/core/src/com/google/inject/internal/ProviderMethodsModule.java +++ b/core/src/com/google/inject/internal/ProviderMethodsModule.java @@ -29,10 +29,10 @@ import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Provides; import com.google.inject.TypeLiteral; -import com.google.inject.spi.ModuleAnnotatedMethodScanner; import com.google.inject.spi.Dependency; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.Message; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; import com.google.inject.util.Modules; import java.lang.annotation.Annotation; @@ -55,8 +55,8 @@ public final class ProviderMethodsModule implements Module { private static ModuleAnnotatedMethodScanner PROVIDES_BUILDER = new ModuleAnnotatedMethodScanner() { @Override - public <T> Key<T> prepareMethod(Binder binder, Annotation annotation, Key<T> key, - InjectionPoint injectionPoint) { + public <T> Key<T> prepareMethod( + Binder binder, Annotation annotation, Key<T> key, InjectionPoint injectionPoint) { return key; } @@ -89,7 +89,7 @@ public final class ProviderMethodsModule implements Module { /** * Returns a module which creates bindings methods in the module that match the scanner. */ - public static Module forModule(Module module, ModuleAnnotatedMethodScanner scanner) { + public static Module forModule(Object module, ModuleAnnotatedMethodScanner scanner) { return forObject(module, false, scanner); } @@ -114,6 +114,10 @@ public final class ProviderMethodsModule implements Module { return new ProviderMethodsModule(object, skipFastClassGeneration, scanner); } + public Object getDelegateModule() { + return delegate; + } + @Override public synchronized void configure(Binder binder) { for (ProviderMethod<?> providerMethod : getProviderMethods(binder)) { @@ -258,7 +262,11 @@ public final class ProviderMethodsModule implements Module { @SuppressWarnings("unchecked") // Define T as the method's return type. TypeLiteral<T> returnType = (TypeLiteral<T>) typeLiteral.getReturnType(method); Key<T> key = getKey(errors, returnType, method, method.getAnnotations()); - key = scanner.prepareMethod(binder, annotation, key, point); + try { + key = scanner.prepareMethod(binder, annotation, key, point); + } catch(Throwable t) { + binder.addError(t); + } Class<? extends Annotation> scopeAnnotation = Annotations.findScopeAnnotation(errors, method.getAnnotations()); for (Message message : errors.getMessages()) { @@ -275,7 +283,8 @@ public final class ProviderMethodsModule implements Module { @Override public boolean equals(Object o) { return o instanceof ProviderMethodsModule - && ((ProviderMethodsModule) o).delegate == delegate; + && ((ProviderMethodsModule) o).delegate == delegate + && ((ProviderMethodsModule) o).scanner == scanner; } @Override public int hashCode() { diff --git a/core/src/com/google/inject/internal/State.java b/core/src/com/google/inject/internal/State.java index d0ee4fbc..8a828f2e 100644 --- a/core/src/com/google/inject/internal/State.java +++ b/core/src/com/google/inject/internal/State.java @@ -23,6 +23,8 @@ import com.google.inject.Binding; import com.google.inject.Key; import com.google.inject.Scope; import com.google.inject.TypeLiteral; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; import com.google.inject.spi.ProvisionListenerBinding; import com.google.inject.spi.ScopeBinding; import com.google.inject.spi.TypeConverterBinding; @@ -105,6 +107,14 @@ interface State { return ImmutableList.of(); } + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + throw new UnsupportedOperationException(); + } + + public List<ModuleAnnotatedMethodScannerBinding> getScannerBindings() { + return ImmutableList.of(); + } + public void blacklist(Key<?> key, State state, Object source) { } @@ -166,6 +176,10 @@ interface State { List<ProvisionListenerBinding> getProvisionListenerBindings(); + void addScanner(ModuleAnnotatedMethodScannerBinding scanner); + + List<ModuleAnnotatedMethodScannerBinding> getScannerBindings(); + /** * Forbids the corresponding injector from creating a binding to {@code key}. Child injectors * blacklist their bound keys on their parent injectors to prevent just-in-time bindings on the diff --git a/core/src/com/google/inject/spi/DefaultElementVisitor.java b/core/src/com/google/inject/spi/DefaultElementVisitor.java index 0780bf80..1bbea0d2 100644 --- a/core/src/com/google/inject/spi/DefaultElementVisitor.java +++ b/core/src/com/google/inject/spi/DefaultElementVisitor.java @@ -102,4 +102,8 @@ public abstract class DefaultElementVisitor<V> implements ElementVisitor<V> { public V visit(RequireExactBindingAnnotationsOption option) { return visitOther(option); } + + public V visit(ModuleAnnotatedMethodScannerBinding binding) { + return visitOther(binding); + } } diff --git a/core/src/com/google/inject/spi/ElementVisitor.java b/core/src/com/google/inject/spi/ElementVisitor.java index 5e990866..f0d9d138 100644 --- a/core/src/com/google/inject/spi/ElementVisitor.java +++ b/core/src/com/google/inject/spi/ElementVisitor.java @@ -16,6 +16,7 @@ package com.google.inject.spi; +import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.Inject; @@ -120,4 +121,11 @@ public interface ElementVisitor<V> { * @since 4.0 */ V visit(RequireExactBindingAnnotationsOption option); + + /** + * Visits a {@link Binder#scanModulesForAnnotatedMethods} command. + * + * @since 4.0 + */ + V visit(ModuleAnnotatedMethodScannerBinding binding); } diff --git a/core/src/com/google/inject/spi/Elements.java b/core/src/com/google/inject/spi/Elements.java index 986582e2..46072e30 100644 --- a/core/src/com/google/inject/spi/Elements.java +++ b/core/src/com/google/inject/spi/Elements.java @@ -17,13 +17,12 @@ package com.google.inject.spi; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.inject.internal.InternalFlags.IncludeStackTraceOption; import static com.google.inject.internal.InternalFlags.getIncludeStackTraceOption; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; - import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Binding; @@ -44,6 +43,7 @@ import com.google.inject.internal.BindingBuilder; import com.google.inject.internal.ConstantBindingBuilderImpl; import com.google.inject.internal.Errors; import com.google.inject.internal.ExposureBuilder; +import com.google.inject.internal.InternalFlags.IncludeStackTraceOption; import com.google.inject.internal.PrivateElementsImpl; import com.google.inject.internal.ProviderMethodsModule; import com.google.inject.internal.util.SourceProvider; @@ -56,6 +56,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -107,11 +108,15 @@ public final class Elements { for (Module module : modules) { binder.install(module); } + binder.scanForAnnotatedMethods(); + for (RecordingBinder child : binder.privateBinders) { + child.scanForAnnotatedMethods(); + } // Free the memory consumed by the stack trace elements cache StackTraceElements.clearCache(); return Collections.unmodifiableList(binder.elements); } - + private static class ElementsAsModule implements Module { private final Iterable<? extends Element> elements; @@ -119,6 +124,7 @@ public final class Elements { this.elements = elements; } + @Override public void configure(Binder binder) { for (Element element : elements) { element.applyTo(binder); @@ -138,22 +144,37 @@ public final class Elements { return (BindingTargetVisitor<T, T>) GET_INSTANCE_VISITOR; } + private static class ModuleInfo { + private final Binder binder; + private final ModuleSource moduleSource; + + private ModuleInfo(Binder binder, ModuleSource moduleSource) { + this.binder = binder; + this.moduleSource = moduleSource; + } + } + private static class RecordingBinder implements Binder, PrivateBinder { private final Stage stage; - private final Set<Module> modules; + private final Map<Module, ModuleInfo> modules; private final List<Element> elements; private final Object source; /** The current modules stack */ private ModuleSource moduleSource = null; private final SourceProvider sourceProvider; + private final Set<ModuleAnnotatedMethodScanner> scanners; /** The binder where exposed bindings will be created */ private final RecordingBinder parent; private final PrivateElementsImpl privateElements; + /** All children private binders, so we can scan through them. */ + private final List<RecordingBinder> privateBinders; + private RecordingBinder(Stage stage) { this.stage = stage; - this.modules = Sets.newHashSet(); + this.modules = Maps.newLinkedHashMap(); + this.scanners = Sets.newLinkedHashSet(); this.elements = Lists.newArrayList(); this.source = null; this.sourceProvider = SourceProvider.DEFAULT_INSTANCE.plusSkippedClasses( @@ -161,6 +182,7 @@ public final class Elements { ConstantBindingBuilderImpl.class, AbstractBindingBuilder.class, BindingBuilder.class); this.parent = null; this.privateElements = null; + this.privateBinders = Lists.newArrayList(); } /** Creates a recording binder that's backed by {@code prototype}. */ @@ -171,26 +193,31 @@ public final class Elements { this.stage = prototype.stage; this.modules = prototype.modules; this.elements = prototype.elements; + this.scanners = prototype.scanners; this.source = source; this.moduleSource = prototype.moduleSource; this.sourceProvider = sourceProvider; this.parent = prototype.parent; this.privateElements = prototype.privateElements; + this.privateBinders = prototype.privateBinders; } /** Creates a private recording binder. */ private RecordingBinder(RecordingBinder parent, PrivateElementsImpl privateElements) { this.stage = parent.stage; - this.modules = Sets.newHashSet(); + this.modules = Maps.newLinkedHashMap(); + this.scanners = Sets.newLinkedHashSet(parent.scanners); this.elements = privateElements.getElementsMutable(); this.source = parent.source; this.moduleSource = parent.moduleSource; this.sourceProvider = parent.sourceProvider; this.parent = parent; this.privateElements = privateElements; + this.privateBinders = parent.privateBinders; } /*if[AOP]*/ + @Override public void bindInterceptor( Matcher<? super Class<?>> classMatcher, Matcher<? super Method> methodMatcher, @@ -200,19 +227,23 @@ public final class Elements { } /*end[AOP]*/ + @Override public void bindScope(Class<? extends Annotation> annotationType, Scope scope) { elements.add(new ScopeBinding(getElementSource(), annotationType, scope)); } + @Override @SuppressWarnings("unchecked") // it is safe to use the type literal for the raw type public void requestInjection(Object instance) { requestInjection((TypeLiteral<Object>) TypeLiteral.get(instance.getClass()), instance); } + @Override public <T> void requestInjection(TypeLiteral<T> type, T instance) { elements.add(new InjectionRequest<T>(getElementSource(), type, instance)); } + @Override public <T> MembersInjector<T> getMembersInjector(final TypeLiteral<T> typeLiteral) { final MembersInjectorLookup<T> element = new MembersInjectorLookup<T>(getElementSource(), typeLiteral); @@ -239,16 +270,60 @@ public final class Elements { } } + void scanForAnnotatedMethods() { + for (ModuleAnnotatedMethodScanner scanner : scanners) { + // Note: we must iterate over a copy of the modules because calling install(..) + // will mutate modules, otherwise causing a ConcurrentModificationException. + for (Map.Entry<Module, ModuleInfo> entry : Maps.newLinkedHashMap(modules).entrySet()) { + Module module = entry.getKey(); + // If this was from a child private binder, skip it... we'll process it later. + if (entry.getValue().binder != this) { + continue; + } + moduleSource = entry.getValue().moduleSource; + try { + install(ProviderMethodsModule.forModule(module, scanner)); + } catch(RuntimeException e) { + Collection<Message> messages = Errors.getMessagesFromThrowable(e); + if (!messages.isEmpty()) { + elements.addAll(messages); + } else { + addError(e); + } + } + } + } + moduleSource = null; + } + public void install(Module module) { - if (modules.add(module)) { + if (!modules.containsKey(module)) { Binder binder = this; + boolean unwrapModuleSource = false; // Update the module source for the new module - if (!(module instanceof ProviderMethodsModule)) { + if (module instanceof ProviderMethodsModule) { + // There are two reason's we'd want to get the module source in a ProviderMethodsModule. + // ModuleAnnotatedMethodScanner lets users scan their own modules for @Provides-like + // bindings. If they install the module at a top-level, then moduleSource can be null. + // Also, if they pass something other than 'this' to it, we'd have the wrong source. + Object delegate = ((ProviderMethodsModule) module).getDelegateModule(); + if (moduleSource == null + || !moduleSource.getModuleClassName().equals(delegate.getClass().getName())) { + moduleSource = getModuleSource(delegate); + unwrapModuleSource = true; + } + } else { moduleSource = getModuleSource(module); + unwrapModuleSource = true; } if (module instanceof PrivateModule) { binder = binder.newPrivateBinder(); - } + // Store the module in the private binder too. + ((RecordingBinder) binder).modules.put(module, new ModuleInfo(binder, moduleSource)); + } + // Always store this in the parent binder (even if it was a private module) + // so that we know not to process it again, and so that scanners inherit down. + modules.put(module, new ModuleInfo(binder, moduleSource)); try { module.configure(binder); } catch (RuntimeException e) { @@ -261,7 +336,7 @@ public final class Elements { } binder.install(ProviderMethodsModule.forModule(module)); // We are done with this module, so undo module source change - if (!(module instanceof ProviderMethodsModule)) { + if (unwrapModuleSource) { moduleSource = moduleSource.getParent(); } } @@ -334,37 +409,51 @@ public final class Elements { return new RecordingBinder(this, null, newSourceProvider); } + @Override public PrivateBinder newPrivateBinder() { PrivateElementsImpl privateElements = new PrivateElementsImpl(getElementSource()); RecordingBinder binder = new RecordingBinder(this, privateElements); + privateBinders.add(binder); elements.add(privateElements); return binder; } - + + @Override public void disableCircularProxies() { elements.add(new DisableCircularProxiesOption(getElementSource())); } - + + @Override public void requireExplicitBindings() { - elements.add(new RequireExplicitBindingsOption(getElementSource())); + elements.add(new RequireExplicitBindingsOption(getElementSource())); } - + + @Override public void requireAtInjectOnConstructors() { elements.add(new RequireAtInjectOnConstructorsOption(getElementSource())); } + @Override public void requireExactBindingAnnotations() { elements.add(new RequireExactBindingAnnotationsOption(getElementSource())); } + @Override + public void scanModulesForAnnotatedMethods(ModuleAnnotatedMethodScanner scanner) { + scanners.add(scanner); + elements.add(new ModuleAnnotatedMethodScannerBinding(getElementSource(), scanner)); + } + public void expose(Key<?> key) { exposeInternal(key); } + @Override public AnnotatedElementBuilder expose(Class<?> type) { return exposeInternal(Key.get(type)); } + @Override public AnnotatedElementBuilder expose(TypeLiteral<?> type) { return exposeInternal(Key.get(type)); } @@ -374,7 +463,9 @@ public final class Elements { addError("Cannot expose %s on a standard binder. " + "Exposed bindings are only applicable to private binders.", key); return new AnnotatedElementBuilder() { + @Override public void annotatedWith(Class<? extends Annotation> annotationType) {} + @Override public void annotatedWith(Annotation annotation) {} }; } @@ -384,7 +475,7 @@ public final class Elements { return builder; } - private ModuleSource getModuleSource(Module module) { + private ModuleSource getModuleSource(Object module) { StackTraceElement[] partialCallStack; if (getIncludeStackTraceOption() == IncludeStackTraceOption.COMPLETE) { partialCallStack = getPartialCallStack(new Throwable().getStackTrace()); @@ -412,7 +503,7 @@ public final class Elements { } IncludeStackTraceOption stackTraceOption = getIncludeStackTraceOption(); if (stackTraceOption == IncludeStackTraceOption.COMPLETE || - (stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE + (stackTraceOption == IncludeStackTraceOption.ONLY_FOR_DECLARING_SOURCE && declaringSource == null)) { callStack = new Throwable().getStackTrace(); } @@ -436,9 +527,9 @@ public final class Elements { } /** - * Removes the {@link #moduleSource} call stack from the beginning of current call stack. It - * also removes the last two elements in order to make {@link #install(Module)} the last call - * in the call stack. + * Removes the {@link #moduleSource} call stack from the beginning of current call stack. It + * also removes the last two elements in order to make {@link #install(Module)} the last call + * in the call stack. */ private StackTraceElement[] getPartialCallStack(StackTraceElement[] callStack) { int toSkip = 0; @@ -452,7 +543,7 @@ public final class Elements { System.arraycopy(callStack, 1, partialCallStack, 0, chunkSize); return partialCallStack; } - + @Override public String toString() { return "Binder"; } diff --git a/core/src/com/google/inject/spi/ModuleAnnotatedMethodScanner.java b/core/src/com/google/inject/spi/ModuleAnnotatedMethodScanner.java index 99b6d91a..36adc852 100644 --- a/core/src/com/google/inject/spi/ModuleAnnotatedMethodScanner.java +++ b/core/src/com/google/inject/spi/ModuleAnnotatedMethodScanner.java @@ -18,8 +18,6 @@ package com.google.inject.spi; import com.google.inject.Binder; import com.google.inject.Key; -import com.google.inject.Module; -import com.google.inject.internal.ProviderMethodsModule; import java.lang.annotation.Annotation; import java.util.Set; @@ -29,14 +27,6 @@ import java.util.Set; * as providers, similar to {@code @Provides} methods. */ public abstract class ModuleAnnotatedMethodScanner { - - /** - * Scans the module for methods and returns a module that will bind the methods - * that match this scanner. - */ - public final Module forModule(Module module) { - return ProviderMethodsModule.forModule(module, this); - } /** * Returns the annotations this should scan for. Every method in the module that has one of these diff --git a/core/src/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java b/core/src/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java new file mode 100644 index 00000000..d6324206 --- /dev/null +++ b/core/src/com/google/inject/spi/ModuleAnnotatedMethodScannerBinding.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.spi; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.inject.Binder; +import com.google.inject.internal.Errors; + +/** + * Represents a call to {@link Binder#scanModulesForAnnotatedMethods} in a module. + * + * @author sameb@google.com (Sam Berlin) + * @since 4.0 + */ +public final class ModuleAnnotatedMethodScannerBinding implements Element { + private final Object source; + private final ModuleAnnotatedMethodScanner scanner; + + public ModuleAnnotatedMethodScannerBinding(Object source, ModuleAnnotatedMethodScanner scanner) { + this.source = checkNotNull(source, "source"); + this.scanner = checkNotNull(scanner, "scanner"); + } + + public Object getSource() { + return source; + } + + public ModuleAnnotatedMethodScanner getScanner() { + return scanner; + } + + public <T> T acceptVisitor(ElementVisitor<T> visitor) { + return visitor.visit(this); + } + + public void applyTo(Binder binder) { + binder.withSource(getSource()).scanModulesForAnnotatedMethods(scanner); + } + + @Override public String toString() { + return scanner + " which scans for " + scanner.annotationClasses() + + " (bound at " + Errors.convert(source) + ")"; + } +} diff --git a/core/src/com/google/inject/spi/ModuleSource.java b/core/src/com/google/inject/spi/ModuleSource.java index 19add7ee..1e07de2b 100644 --- a/core/src/com/google/inject/spi/ModuleSource.java +++ b/core/src/com/google/inject/spi/ModuleSource.java @@ -27,7 +27,7 @@ import java.util.List; /** * Associated to a {@link Module module}, provides the module class name, the parent module {@link * ModuleSource source}, and the call stack that ends just before the module {@link - * Module#configure(Binder) configure(Binder)} method invocation. + * Module#configure(Binder) configure(Binder)} method invocation. */ final class ModuleSource { @@ -35,16 +35,16 @@ final class ModuleSource { * The class name of module that this {@link ModuleSource} associated to. */ private final String moduleClassName; - + /** * The parent {@link ModuleSource module source}. */ private final ModuleSource parent; - - /** - * The chunk of call stack that starts from the parent module {@link Module#configure(Binder) - * configure(Binder)} call and ends just before the module {@link Module#configure(Binder) - * configure(Binder)} method invocation. For a module without a parent module the chunk starts + + /** + * The chunk of call stack that starts from the parent module {@link Module#configure(Binder) + * configure(Binder)} call and ends just before the module {@link Module#configure(Binder) + * configure(Binder)} method invocation. For a module without a parent module the chunk starts * from the bottom of call stack. The array is non-empty if stack trace collection is on. */ private final InMemoryStackTraceElement[] partialCallStack; @@ -52,32 +52,32 @@ final class ModuleSource { /** * Creates a new {@link ModuleSource} with a {@literal null} parent. * @param module the corresponding module - * @param partialCallStack the chunk of call stack that starts from the parent module {@link - * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link * Module#configure(Binder) configure(Binder)} method invocation */ - ModuleSource(Module module, StackTraceElement[] partialCallStack) { + ModuleSource(Object module, StackTraceElement[] partialCallStack) { this(null, module, partialCallStack); - } - + } + /** * Creates a new {@link ModuleSource} Object. - * @param parent the parent module {@link ModuleSource source} + * @param parent the parent module {@link ModuleSource source} * @param module the corresponding module - * @param partialCallStack the chunk of call stack that starts from the parent module {@link - * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link * Module#configure(Binder) configure(Binder)} method invocation */ private ModuleSource( - /* @Nullable */ ModuleSource parent, Module module, StackTraceElement[] partialCallStack) { + /* @Nullable */ ModuleSource parent, Object module, StackTraceElement[] partialCallStack) { Preconditions.checkNotNull(module, "module cannot be null."); Preconditions.checkNotNull(partialCallStack, "partialCallStack cannot be null."); this.parent = parent; this.moduleClassName = module.getClass().getName(); this.partialCallStack = StackTraceElements.convertToInMemoryStackTraceElement(partialCallStack); } - - /** + + /** * Returns the corresponding module class name. * * @see Class#getName() @@ -95,27 +95,27 @@ final class ModuleSource { StackTraceElement[] getPartialCallStack() { return StackTraceElements.convertToStackTraceElement(partialCallStack); } - + /** * Returns the size of partial call stack if stack trace collection is on otherwise zero. */ int getPartialCallStackSize() { return partialCallStack.length; } - - /** + + /** * Creates and returns a child {@link ModuleSource} corresponding to the {@link Module module}. * @param module the corresponding module - * @param partialCallStack the chunk of call stack that starts from the parent module {@link - * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link + * @param partialCallStack the chunk of call stack that starts from the parent module {@link + * Module#configure(Binder) configure(Binder)} call and ends just before the module {@link * Module#configure(Binder) configure(Binder)} method invocation */ - ModuleSource createChild(Module module, StackTraceElement[] partialCallStack) { + ModuleSource createChild(Object module, StackTraceElement[] partialCallStack) { return new ModuleSource(this, module, partialCallStack); } - /** - * Returns the parent module {@link ModuleSource source}. + /** + * Returns the parent module {@link ModuleSource source}. */ ModuleSource getParent() { return parent; @@ -123,7 +123,7 @@ final class ModuleSource { /** * Returns the class names of modules in this module source. The first element (index 0) is filled - * by this object {@link #getModuleClassName()}. The second element is filled by the parent's + * by this object {@link #getModuleClassName()}. The second element is filled by the parent's * {@link #getModuleClassName()} and so on. */ List<String> getModuleClassNames() { @@ -138,7 +138,7 @@ final class ModuleSource { } /** - * Returns the size of {@link ModuleSource ModuleSources} chain (all parents) that ends at this + * Returns the size of {@link ModuleSource ModuleSources} chain (all parents) that ends at this * object. */ int size() { @@ -147,7 +147,7 @@ final class ModuleSource { } return parent.size() + 1; } - + /** * Returns the size of call stack that ends just before the module {@link Module#configure(Binder) * configure(Binder)} method invocation (see {@link #getStackTrace()}). @@ -170,7 +170,7 @@ final class ModuleSource { int cursor = 0; ModuleSource current = this; while (current != null) { - StackTraceElement[] chunk = + StackTraceElement[] chunk = StackTraceElements.convertToStackTraceElement(current.partialCallStack); int chunkSize = chunk.length; System.arraycopy(chunk, 0, callStack, cursor, chunkSize); diff --git a/core/src/com/google/inject/util/Modules.java b/core/src/com/google/inject/util/Modules.java index c166b8e2..08ec92c7 100644 --- a/core/src/com/google/inject/util/Modules.java +++ b/core/src/com/google/inject/util/Modules.java @@ -16,6 +16,7 @@ package com.google.inject.util; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -33,7 +34,9 @@ import com.google.inject.internal.Errors; import com.google.inject.spi.DefaultBindingScopingVisitor; import com.google.inject.spi.DefaultElementVisitor; import com.google.inject.spi.Element; +import com.google.inject.spi.ElementVisitor; import com.google.inject.spi.Elements; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; import com.google.inject.spi.PrivateElements; import com.google.inject.spi.ScopeBinding; @@ -191,7 +194,9 @@ public final class Modules { final Binder binder = baseBinder.skipSources(this.getClass()); final LinkedHashSet<Element> elements = new LinkedHashSet<Element>(baseElements); - final List<Element> overrideElements = Elements.getElements(currentStage(), overrides); + final Module scannersModule = extractScanners(elements); + final List<Element> overrideElements = Elements.getElements(currentStage(), + ImmutableList.<Module>builder().addAll(overrides).add(scannersModule).build()); final Set<Key<?>> overriddenKeys = Sets.newHashSet(); final Map<Class<? extends Annotation>, ScopeBinding> overridesScopeAnnotations = @@ -331,4 +336,24 @@ public final class Modules { } } } + + private static Module extractScanners(Iterable<Element> elements) { + final List<ModuleAnnotatedMethodScannerBinding> scanners = Lists.newArrayList(); + ElementVisitor<Void> visitor = new DefaultElementVisitor<Void>() { + @Override public Void visit(ModuleAnnotatedMethodScannerBinding binding) { + scanners.add(binding); + return null; + } + }; + for (Element element : elements) { + element.acceptVisitor(visitor); + } + return new AbstractModule() { + @Override protected void configure() { + for (ModuleAnnotatedMethodScannerBinding scanner : scanners) { + scanner.applyTo(binder()); + } + } + }; + } } diff --git a/core/test/com/google/inject/internal/WeakKeySetTest.java b/core/test/com/google/inject/internal/WeakKeySetTest.java index b003fb5f..3797d889 100644 --- a/core/test/com/google/inject/internal/WeakKeySetTest.java +++ b/core/test/com/google/inject/internal/WeakKeySetTest.java @@ -41,6 +41,7 @@ import com.google.inject.internal.MethodAspect; /*end[AOP]*/ import com.google.inject.internal.State; import com.google.inject.internal.WeakKeySet; +import com.google.inject.spi.ModuleAnnotatedMethodScannerBinding; import com.google.inject.spi.ProvisionListenerBinding; import com.google.inject.spi.ScopeBinding; import com.google.inject.spi.TypeConverterBinding; @@ -500,6 +501,14 @@ public class WeakKeySetTest extends TestCase { return ImmutableList.of(); } + public void addScanner(ModuleAnnotatedMethodScannerBinding scanner) { + throw new UnsupportedOperationException(); + } + + public List<ModuleAnnotatedMethodScannerBinding> getScannerBindings() { + return ImmutableList.of(); + } + public void blacklist(Key<?> key, State state, Object source) { } diff --git a/core/test/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java b/core/test/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java index 62f82203..e73a9afc 100644 --- a/core/test/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java +++ b/core/test/com/google/inject/spi/ModuleAnnotatedMethodScannerTest.java @@ -22,14 +22,17 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Binding; import com.google.inject.CreationException; +import com.google.inject.Exposed; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; +import com.google.inject.PrivateModule; import com.google.inject.internal.util.StackTraceElements; import com.google.inject.name.Named; import com.google.inject.name.Names; @@ -44,32 +47,27 @@ import java.util.Set; /** Tests for {@link ModuleAnnotatedMethodScanner} usage. */ public class ModuleAnnotatedMethodScannerTest extends TestCase { - + public void testScanning() throws Exception { Module module = new AbstractModule() { - @Override protected void configure() { - install(new NamedMunger().forModule(this)); - } - + @Override protected void configure() {} + @TestProvides @Named("foo") String foo() { return "foo"; } - + @TestProvides @Named("foo2") String foo2() { return "foo2"; } }; - Injector injector = Guice.createInjector(module); + Injector injector = Guice.createInjector(module, NamedMunger.module()); // assert no bindings named "foo" or "foo2" exist -- they were munged. - assertNull(injector.getExistingBinding(Key.get(String.class, named("foo")))); - assertNull(injector.getExistingBinding(Key.get(String.class, named("foo2")))); + assertMungedBinding(injector, String.class, "foo", "foo"); + assertMungedBinding(injector, String.class, "foo2", "foo2"); Binding<String> fooBinding = injector.getBinding(Key.get(String.class, named("foo-munged"))); Binding<String> foo2Binding = injector.getBinding(Key.get(String.class, named("foo2-munged"))); - assertEquals("foo", fooBinding.getProvider().get()); - assertEquals("foo2", foo2Binding.getProvider().get()); - // Validate the provider has a sane toString assertEquals(methodName(TestProvides.class, "foo", module), fooBinding.getProvider().toString()); @@ -78,41 +76,51 @@ public class ModuleAnnotatedMethodScannerTest extends TestCase { } public void testMoreThanOneClaimedAnnotationFails() throws Exception { - final NamedMunger scanner = new NamedMunger(); Module module = new AbstractModule() { - @Override protected void configure() { - install(scanner.forModule(this)); - } - + @Override protected void configure() {} + @TestProvides @TestProvides2 String foo() { return "foo"; } }; try { - Guice.createInjector(module); + Guice.createInjector(module, NamedMunger.module()); fail(); } catch(CreationException expected) { assertEquals(1, expected.getErrorMessages().size()); assertContains(expected.getMessage(), - "More than one annotation claimed by " + scanner + " on method " + "More than one annotation claimed by NamedMunger on method " + module.getClass().getName() + ".foo(). Methods can only have " + "one annotation claimed per scanner."); } } - + private String methodName(Class<? extends Annotation> annotation, String method, Object container) throws Exception { return "@" + annotation.getName() + " " + StackTraceElements.forMember(container.getClass().getDeclaredMethod(method)); } - + @Documented @Target(METHOD) @Retention(RUNTIME) private @interface TestProvides {} @Documented @Target(METHOD) @Retention(RUNTIME) private @interface TestProvides2 {} - + private static class NamedMunger extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new NamedMunger()); + } + }; + } + + @Override + public String toString() { + return "NamedMunger"; + } + @Override public Set<? extends Class<? extends Annotation>> annotationClasses() { return ImmutableSet.of(TestProvides.class, TestProvides2.class); @@ -125,4 +133,160 @@ public class ModuleAnnotatedMethodScannerTest extends TestCase { Names.named(((Named) key.getAnnotation()).value() + "-munged")); } } + + private void assertMungedBinding(Injector injector, Class<?> clazz, String originalName, + Object expectedValue) { + assertNull(injector.getExistingBinding(Key.get(clazz, named(originalName)))); + Binding<?> fooBinding = injector.getBinding(Key.get(clazz, named(originalName + "-munged"))); + assertEquals(expectedValue, fooBinding.getProvider().get()); + } + + public void testFailingScanner() { + try { + Guice.createInjector(new SomeModule(), FailingScanner.module()); + fail(); + } catch (CreationException expected) { + Message m = Iterables.getOnlyElement(expected.getErrorMessages()); + assertEquals( + "An exception was caught and reported. Message: Failing in the scanner.", + m.getMessage()); + assertEquals(IllegalStateException.class, m.getCause().getClass()); + ElementSource source = (ElementSource) Iterables.getOnlyElement(m.getSources()); + assertEquals(SomeModule.class.getName(), + Iterables.getOnlyElement(source.getModuleClassNames())); + assertEquals(String.class.getName() + " " + SomeModule.class.getName() + ".aString()", + source.toString()); + } + } + + public static class FailingScanner extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new FailingScanner()); + } + }; + } + + @Override public Set<? extends Class<? extends Annotation>> annotationClasses() { + return ImmutableSet.of(TestProvides.class); + } + + @Override public <T> Key<T> prepareMethod( + Binder binder, Annotation rawAnnotation, Key<T> key, InjectionPoint injectionPoint) { + throw new IllegalStateException("Failing in the scanner."); + } + } + + static class SomeModule extends AbstractModule { + @TestProvides String aString() { + return "Foo"; + } + + @Override protected void configure() {} + } + + public void testChildInjectorInheritsScanner() { + Injector parent = Guice.createInjector(NamedMunger.module()); + Injector child = parent.createChildInjector(new AbstractModule() { + @Override protected void configure() {} + + @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(child, String.class, "foo", "foo"); + } + + public void testChildInjectorScannersDontImpactSiblings() { + Module module = new AbstractModule() { + @Override + protected void configure() {} + + @TestProvides @Named("foo") String foo() { + return "foo"; + } + }; + Injector parent = Guice.createInjector(); + Injector child = parent.createChildInjector(NamedMunger.module(), module); + assertMungedBinding(child, String.class, "foo", "foo"); + + // no foo nor foo-munged in sibling, since scanner never saw it. + Injector sibling = parent.createChildInjector(module); + assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo")))); + assertNull(sibling.getExistingBinding(Key.get(String.class, named("foo-munged")))); + } + + public void testPrivateModuleInheritScanner_usingPrivateModule() { + Injector injector = Guice.createInjector(NamedMunger.module(), new PrivateModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleInheritScanner_usingPrivateBinder() { + Injector injector = Guice.createInjector(NamedMunger.module(), new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() {} + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleScannersDontImpactSiblings_usingPrivateModule() { + Injector injector = Guice.createInjector(new PrivateModule() { + @Override protected void configure() { + install(NamedMunger.module()); + } + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }, new PrivateModule() { + @Override protected void configure() {} + + // ignored! (because the scanner doesn't run over this module) + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + assertMungedBinding(injector, String.class, "foo", "foo"); + } + + public void testPrivateModuleScannersDontImpactSiblings_usingPrivateBinder() { + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() { + install(NamedMunger.module()); + } + + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + } + }, new AbstractModule() { + @Override protected void configure() { + binder().newPrivateBinder().install(new AbstractModule() { + @Override protected void configure() {} + + // ignored! (because the scanner doesn't run over this module) + @Exposed @TestProvides @Named("foo") String foo() { + return "foo"; + } + }); + }}); + assertMungedBinding(injector, String.class, "foo", "foo"); + } } diff --git a/core/test/com/google/inject/util/OverrideModuleTest.java b/core/test/com/google/inject/util/OverrideModuleTest.java index 3b8e05bc..16d7a2ce 100644 --- a/core/test/com/google/inject/util/OverrideModuleTest.java +++ b/core/test/com/google/inject/util/OverrideModuleTest.java @@ -20,12 +20,15 @@ import static com.google.inject.Asserts.asModuleChain; import static com.google.inject.Asserts.assertContains; import static com.google.inject.Guice.createInjector; import static com.google.inject.name.Names.named; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; import com.google.inject.AbstractModule; import com.google.inject.Binder; +import com.google.inject.Binding; import com.google.inject.CreationException; import com.google.inject.Exposed; import com.google.inject.Guice; @@ -39,13 +42,18 @@ import com.google.inject.Scope; import com.google.inject.ScopeAnnotation; import com.google.inject.Stage; import com.google.inject.name.Named; -import com.google.inject.util.Modules; +import com.google.inject.name.Names; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; import junit.framework.TestCase; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Date; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; /** @@ -678,4 +686,46 @@ public class OverrideModuleTest extends TestCase { }); Guice.createInjector(stage, module); } + + public void testOverridesApplyOriginalScanners() { + Injector injector = + Guice.createInjector(Modules.override(NamedMunger.module()).with(new AbstractModule() { + @Override protected void configure() {} + @TestProvides @Named("test") String provideString() { return "foo"; } + })); + + assertNull(injector.getExistingBinding(Key.get(String.class, named("test")))); + Binding<String> binding = injector.getBinding(Key.get(String.class, named("test-munged"))); + assertEquals("foo", binding.getProvider().get()); + } + + @Documented @Target(METHOD) @Retention(RUNTIME) + private @interface TestProvides {} + + private static class NamedMunger extends ModuleAnnotatedMethodScanner { + static Module module() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(new NamedMunger()); + } + }; + } + + @Override + public String toString() { + return "NamedMunger"; + } + + @Override + public Set<? extends Class<? extends Annotation>> annotationClasses() { + return ImmutableSet.of(TestProvides.class); + } + + @Override + public <T> Key<T> prepareMethod(Binder binder, Annotation annotation, Key<T> key, + InjectionPoint injectionPoint) { + return Key.get(key.getTypeLiteral(), + Names.named(((Named) key.getAnnotation()).value() + "-munged")); + } + } } diff --git a/extensions/assistedinject/.gitignore b/extensions/assistedinject/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/assistedinject/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java index 6ec0cd4a..b40e19f3 100644 --- a/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java +++ b/extensions/assistedinject/src/com/google/inject/assistedinject/FactoryProvider2.java @@ -20,11 +20,13 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; import com.google.common.base.Objects; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.inject.AbstractModule; import com.google.inject.Binder; @@ -57,6 +59,7 @@ import com.google.inject.util.Providers; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; @@ -183,6 +186,9 @@ final class FactoryProvider2 <F> implements InvocationHandler, /** Mapping from method to the data about how the method will be assisted. */ private final ImmutableMap<Method, AssistData> assistDataByMethod; + /** Mapping from method to method handle, for generated default methods. */ + private final ImmutableMap<Method, MethodHandleWrapper> methodHandleByMethod; + /** the hosting injector, or null if we haven't been initialized yet */ private Injector injector; @@ -214,10 +220,22 @@ final class FactoryProvider2 <F> implements InvocationHandler, if(!factoryRawType.isInterface()) { throw errors.addMessage("%s must be an interface.", factoryRawType).toException(); } - + + Multimap<String, Method> defaultMethods = HashMultimap.create(); + Multimap<String, Method> otherMethods = HashMultimap.create(); ImmutableMap.Builder<Method, AssistData> assistDataBuilder = ImmutableMap.builder(); // TODO: also grab methods from superinterfaces for (Method method : factoryRawType.getMethods()) { + // Skip default methods that java8 may have created. + if (isDefault(method) && (method.isBridge() || method.isSynthetic())) { + // Even synthetic default methods need the return type validation... + // unavoidable consequence of javac8. :-( + validateFactoryReturnType(errors, method.getReturnType(), factoryRawType); + defaultMethods.put(method.getName(), method); + continue; + } + otherMethods.put(method.getName(), method); + TypeLiteral<?> returnTypeLiteral = factoryType.getReturnType(method); Key<?> returnType; try { @@ -289,9 +307,51 @@ final class FactoryProvider2 <F> implements InvocationHandler, providers = providerListBuilder.build(); optimized = true; } - assistDataBuilder.put(method, - new AssistData(constructor, returnType, immutableParamList, implementation, - method, removeAssistedDeps(deps), optimized, providers)); + + AssistData data = new AssistData(constructor, + returnType, + immutableParamList, + implementation, + method, + removeAssistedDeps(deps), + optimized, + providers); + assistDataBuilder.put(method, data); + } + + factory = factoryRawType.cast(Proxy.newProxyInstance( + BytecodeGen.getClassLoader(factoryRawType), new Class<?>[] {factoryRawType}, this)); + + // Now go back through default methods. Try to use MethodHandles to make things + // work. If that doesn't work, fallback to trying to find compatible method + // signatures. + Map<Method, AssistData> dataSoFar = assistDataBuilder.build(); + ImmutableMap.Builder<Method, MethodHandleWrapper> methodHandleBuilder = ImmutableMap.builder(); + for (Map.Entry<String, Method> entry : defaultMethods.entries()) { + Method defaultMethod = entry.getValue(); + MethodHandleWrapper handle = MethodHandleWrapper.create(defaultMethod, factory); + if (handle != null) { + methodHandleBuilder.put(defaultMethod, handle); + } else { + boolean foundMatch = false; + for (Method otherMethod : otherMethods.get(defaultMethod.getName())) { + if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) { + if (foundMatch) { + errors.addMessage("Generated default method %s with parameters %s is" + + " signature-compatible with more than one non-default method." + + " Unable to create factory. As a workaround, remove the override" + + " so javac stops generating a default method.", + defaultMethod, Arrays.asList(defaultMethod.getParameterTypes())); + } else { + assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod)); + foundMatch = true; + } + } + } + if (!foundMatch) { + throw new IllegalStateException("Can't find method compatible with: " + defaultMethod); + } + } } // If we generated any errors (from finding matching constructors, for instance), throw an exception. @@ -300,12 +360,35 @@ final class FactoryProvider2 <F> implements InvocationHandler, } assistDataByMethod = assistDataBuilder.build(); + methodHandleByMethod = methodHandleBuilder.build(); } catch (ErrorsException e) { throw new ConfigurationException(e.getErrors().getMessages()); } + } + + static boolean isDefault(Method method) { + // Per the javadoc, default methods are non-abstract, public, non-static. + // They're also in interfaces, but we can guarantee that already since we only act + // on interfaces. + return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) + == Modifier.PUBLIC; + } - factory = factoryRawType.cast(Proxy.newProxyInstance(BytecodeGen.getClassLoader(factoryRawType), - new Class[] { factoryRawType }, this)); + private boolean isCompatible(Method src, Method dst) { + if (!src.getReturnType().isAssignableFrom(dst.getReturnType())) { + return false; + } + Class<?>[] srcParams = src.getParameterTypes(); + Class<?>[] dstParams = dst.getParameterTypes(); + if (srcParams.length != dstParams.length) { + return false; + } + for (int i = 0; i < srcParams.length; i++) { + if (!srcParams[i].isAssignableFrom(dstParams[i])) { + return false; + } + } + return true; } public F get() { @@ -654,6 +737,13 @@ final class FactoryProvider2 <F> implements InvocationHandler, * use that to get an instance of the return type. */ public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { + // If we setup a method handle earlier for this method, call it. + // This is necessary for default methods that java8 creates, so we + // can call the default method implementation (and not our proxied version of it). + if (methodHandleByMethod.containsKey(method)) { + return methodHandleByMethod.get(method).invokeWithArguments(args); + } + if (method.getDeclaringClass().equals(Object.class)) { if ("equals".equals(method.getName())) { return proxy == args[0]; @@ -665,6 +755,7 @@ final class FactoryProvider2 <F> implements InvocationHandler, } AssistData data = assistDataByMethod.get(method); + checkState(data != null, "No data for method: %s", method); Provider<?> provider; if(data.cachedBinding != null) { // Try to get optimized form... provider = data.cachedBinding.getProvider(); @@ -735,4 +826,84 @@ final class FactoryProvider2 <F> implements InvocationHandler, + " (This should never happen. If it does, please report it.)"); } } + + /** Wrapper around MethodHandles/MethodHandle, so we can compile+run on java6. */ + private static class MethodHandleWrapper { + static final int ALL_MODES = Modifier.PRIVATE + | Modifier.STATIC /* package */ + | Modifier.PUBLIC + | Modifier.PROTECTED; + + static final Method unreflectSpecial; + static final Method bindTo; + static final Method invokeWithArguments; + static final Constructor<?> lookupCxtor; + static final boolean valid; + + static { + Method unreflectSpecialTmp = null; + Method bindToTmp = null; + Method invokeWithArgumentsTmp = null; + boolean validTmp = false; + Constructor<?> lookupCxtorTmp = null; + try { + Class<?> lookupClass = Class.forName("java.lang.invoke.MethodHandles$Lookup"); + unreflectSpecialTmp = lookupClass.getMethod("unreflectSpecial", Method.class, Class.class); + Class<?> methodHandleClass = Class.forName("java.lang.invoke.MethodHandle"); + bindToTmp = methodHandleClass.getMethod("bindTo", Object.class); + invokeWithArgumentsTmp = methodHandleClass.getMethod("invokeWithArguments", Object[].class); + lookupCxtorTmp = lookupClass.getDeclaredConstructor(Class.class, int.class); + lookupCxtorTmp.setAccessible(true); + validTmp = true; + } catch (Exception invalid) { + // Ignore the exception, store the values & exit early in create(..) if invalid. + } + + // Store refs to later. + valid = validTmp; + unreflectSpecial = unreflectSpecialTmp; + bindTo = bindToTmp; + invokeWithArguments = invokeWithArgumentsTmp; + lookupCxtor = lookupCxtorTmp; + } + + static MethodHandleWrapper create(Method method, Object proxy) { + if (!valid) { + return null; + } + try { + Class<?> declaringClass = method.getDeclaringClass(); + // Note: this isn't a public API, but we need to use it in order to call default methods. + Object lookup = lookupCxtor.newInstance(declaringClass, ALL_MODES); + method.setAccessible(true); + // These are part of the public API, but we use reflection since we run on java6 + // and they were introduced in java7. + lookup = unreflectSpecial.invoke(lookup, method, declaringClass); + Object handle = bindTo.invoke(lookup, proxy); + return new MethodHandleWrapper(handle); + } catch (InvocationTargetException ite) { + return null; + } catch (IllegalAccessException iae) { + return null; + } catch (InstantiationException ie) { + return null; + } + } + + final Object handle; + + MethodHandleWrapper(Object handle) { + this.handle = handle; + } + + Object invokeWithArguments(Object[] args) throws Exception { + // We must cast the args to an object so the Object[] is the first param, + // as opposed to each individual varargs param. + return invokeWithArguments.invoke(handle, (Object) args); + } + + @Override public String toString() { + return handle.toString(); + } + } } diff --git a/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProvider2Test.java b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProvider2Test.java index 8903042d..c0e9bbd2 100644 --- a/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProvider2Test.java +++ b/extensions/assistedinject/test/com/google/inject/assistedinject/FactoryProvider2Test.java @@ -1131,4 +1131,90 @@ public class FactoryProvider2Test extends TestCase { this.delegate = null; } } + + public static abstract class AbstractAssisted { + interface Factory<O extends AbstractAssisted, I extends CharSequence> { + O create(I string); + } + } + + static class ConcreteAssisted extends AbstractAssisted { + @Inject ConcreteAssisted(@SuppressWarnings("unused") @Assisted String string) {} + } + + static class ConcreteAssistedWithOverride extends AbstractAssisted { + @AssistedInject + ConcreteAssistedWithOverride(@SuppressWarnings("unused") @Assisted String string) {} + + @AssistedInject + ConcreteAssistedWithOverride(@SuppressWarnings("unused") @Assisted StringBuilder sb) {} + + interface Factory extends AbstractAssisted.Factory<ConcreteAssistedWithOverride, String> { + @Override ConcreteAssistedWithOverride create(String string); + } + + interface Factory2 extends AbstractAssisted.Factory<ConcreteAssistedWithOverride, String> { + @Override ConcreteAssistedWithOverride create(String string); + ConcreteAssistedWithOverride create(StringBuilder sb); + } + } + + static class ConcreteAssistedWithoutOverride extends AbstractAssisted { + @Inject ConcreteAssistedWithoutOverride(@SuppressWarnings("unused") @Assisted String string) {} + interface Factory extends AbstractAssisted.Factory<ConcreteAssistedWithoutOverride, String> {} + } + + public static class Public extends AbstractAssisted { + @AssistedInject Public(@SuppressWarnings("unused") @Assisted String string) {} + @AssistedInject Public(@SuppressWarnings("unused") @Assisted StringBuilder sb) {} + + public interface Factory extends AbstractAssisted.Factory<Public, String> { + @Override Public create(String string); + Public create(StringBuilder sb); + } + } + + // See https://github.com/google/guice/issues/904 + public void testGeneratedDefaultMethodsForwardCorrectly() { + final Key<AbstractAssisted.Factory<ConcreteAssisted, String>> concreteKey = + new Key<AbstractAssisted.Factory<ConcreteAssisted, String>>() {}; + Injector injector = Guice.createInjector(new AbstractModule() { + @Override protected void configure() { + install(new FactoryModuleBuilder().build(ConcreteAssistedWithOverride.Factory.class)); + install(new FactoryModuleBuilder().build(ConcreteAssistedWithOverride.Factory2.class)); + install(new FactoryModuleBuilder().build(ConcreteAssistedWithoutOverride.Factory.class)); + install(new FactoryModuleBuilder().build(Public.Factory.class)); + install(new FactoryModuleBuilder().build(concreteKey)); + } + }); + + ConcreteAssistedWithOverride.Factory factory1 = + injector.getInstance(ConcreteAssistedWithOverride.Factory.class); + factory1.create("foo"); + AbstractAssisted.Factory<ConcreteAssistedWithOverride, String> factory1Abstract = factory1; + factory1Abstract.create("foo"); + + ConcreteAssistedWithOverride.Factory2 factory2 = + injector.getInstance(ConcreteAssistedWithOverride.Factory2.class); + factory2.create("foo"); + factory2.create(new StringBuilder("foo")); + AbstractAssisted.Factory<ConcreteAssistedWithOverride, String> factory2Abstract = factory2; + factory2Abstract.create("foo"); + + ConcreteAssistedWithoutOverride.Factory factory3 = + injector.getInstance(ConcreteAssistedWithoutOverride.Factory.class); + factory3.create("foo"); + AbstractAssisted.Factory<ConcreteAssistedWithoutOverride, String> factory3Abstract = factory3; + factory3Abstract.create("foo"); + + Public.Factory factory4 = injector.getInstance(Public.Factory.class); + factory4.create("foo"); + factory4.create(new StringBuilder("foo")); + AbstractAssisted.Factory<Public, String> factory4Abstract = factory4; + factory4Abstract.create("foo"); + + AbstractAssisted.Factory<ConcreteAssisted, String> factory5 = + injector.getInstance(concreteKey); + factory5.create("foo"); + } } diff --git a/extensions/dagger-adapter/build.properties b/extensions/dagger-adapter/build.properties new file mode 100644 index 00000000..97e53c23 --- /dev/null +++ b/extensions/dagger-adapter/build.properties @@ -0,0 +1,8 @@ +lib.dir=../../lib +ext.lib.dir=lib +src.dir=src +test.dir=test +build.dir=build +test.class=com.google.inject.daggeradapter.DaggerAdapterTest +module=com.google.inject.daggeradapter +fragment=true diff --git a/extensions/dagger-adapter/build.xml b/extensions/dagger-adapter/build.xml new file mode 100644 index 00000000..27eb4290 --- /dev/null +++ b/extensions/dagger-adapter/build.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<project name="guice-dagger-adapter" basedir="." default="jar"> + + <import file="../../common.xml"/> + + <path id="compile.classpath"> + <fileset dir="${lib.dir}" includes="*.jar"/> + <fileset dir="${ext.lib.dir}" includes="*.jar"/> + <pathelement path="../../build/classes"/> + <fileset dir="../multibindings/build" includes="*.jar"/> + </path> + + <target name="jar" depends="compile, manifest" description="Build jar."> + <jar destfile="${build.dir}/${ant.project.name}-${version}.jar" + manifest="${build.dir}/META-INF/MANIFEST.MF"> + <fileset dir="${build.dir}/classes" /> + </jar> + </target> + +</project> diff --git a/extensions/dagger-adapter/lib/dagger-2.0-20150205.014011-14.jar b/extensions/dagger-adapter/lib/dagger-2.0-20150205.014011-14.jar Binary files differnew file mode 100644 index 00000000..3690cdc7 --- /dev/null +++ b/extensions/dagger-adapter/lib/dagger-2.0-20150205.014011-14.jar diff --git a/extensions/dagger-adapter/pom.xml b/extensions/dagger-adapter/pom.xml new file mode 100644 index 00000000..10af044f --- /dev/null +++ b/extensions/dagger-adapter/pom.xml @@ -0,0 +1,24 @@ +<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/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.google.inject.extensions</groupId> + <artifactId>extensions-parent</artifactId> + <version>4.0-SNAPSHOT</version> + </parent> + <artifactId>dagger-interop</artifactId> + <name>Google Guice - Extensions - Dagger Interop</name> + <dependencies> + <dependency> + <groupId>com.google.inject.extensions</groupId> + <artifactId>guice-multibindings</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.google.dagger</groupId> + <artifactId>dagger</artifactId> + <version>2.0-SNAPSHOT</version> + </dependency> + </dependencies> +</project> diff --git a/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerAdapter.java b/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerAdapter.java new file mode 100644 index 00000000..eb7aac5f --- /dev/null +++ b/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerAdapter.java @@ -0,0 +1,90 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.daggeradapter; + +import com.google.common.base.Objects; +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.internal.ProviderMethodsModule; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; + +import java.util.Arrays; + +/** + * A utility to adapt classes annotated with {@link @dagger.Module} such that their + * {@link @dagger.Provides} methods can be properly invoked by Guice to perform their + * provision operations. + * + * <p>Simple example: <pre>{@code + * Guice.createInjector(...other modules..., DaggerAdapter.from(new SomeDaggerAdapter())); + * }</pre> + * + * <p>Some notes on usage and compatibility. + * <ul> + * <li>Dagger provider methods have a "SET_VALUES" provision mode not supported by Guice. + * <li>MapBindings are not yet implemented (pending). + * <li>Be careful about stateful modules. In contrast to Dagger (where components are + * expected to be recreated on-demand with new Module instances), Guice typically + * has a single injector with a long lifetime, so your module instance will be used + * throughout the lifetime of the entire app. + * <li>Dagger 1.x uses {@link @Singleton} for all scopes, including shorter-lived scopes + * like per-request or per-activity. Using modules written with Dagger 1.x usage + * in mind may result in mis-scoped objects. + * <li>Dagger 2.x supports custom scope annotations, but for use in Guice, a custom scope + * implementation must be registered in order to support the custom lifetime of that + * annotation. + * </ul> + * + * @author cgruber@google.com (Christian Gruber) + */ +public final class DaggerAdapter { + /** + * Returns a guice module from a dagger module. + * + * <p>Note: At present, it does not honor {@code @Module(includes=...)} directives. + */ + public static Module from(Object... daggerModuleObjects) { + // TODO(cgruber): Gather injects=, dedupe, factor out instances, instantiate the rest, and go. + return new DaggerCompatibilityModule(daggerModuleObjects); + } + + /** + * A Module that adapts Dagger {@code @Module}-annotated types to contribute configuration + * to an {@link com.google.inject.Injector} using a dagger-specific + * {@link ModuleAnnotatedMethodScanner}. + */ + private static final class DaggerCompatibilityModule implements Module { + private final Object[] daggerModuleObjects; + + private DaggerCompatibilityModule(Object... daggerModuleObjects) { + this.daggerModuleObjects = daggerModuleObjects; + } + + @Override public void configure(Binder binder) { + for (Object module : daggerModuleObjects) { + binder.install(ProviderMethodsModule.forModule(module, DaggerMethodScanner.INSTANCE)); + } + } + + @Override public String toString() { + return Objects.toStringHelper(this) + .add("modules", Arrays.asList(daggerModuleObjects)) + .toString(); + } + } + + private DaggerAdapter() {} +} diff --git a/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerMethodScanner.java b/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerMethodScanner.java new file mode 100644 index 00000000..0dbdda21 --- /dev/null +++ b/extensions/dagger-adapter/src/com/google/inject/daggeradapter/DaggerMethodScanner.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.daggeradapter; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.internal.UniqueAnnotations; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; + +import dagger.Provides; +import dagger.Provides.Type; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * A scanner to process provider methods on Dagger modules. + * + * @author cgruber@google.com (Christian Gruber) + */ +final class DaggerMethodScanner extends ModuleAnnotatedMethodScanner { + static DaggerMethodScanner INSTANCE = new DaggerMethodScanner(); + + @Override public Set<? extends Class<? extends Annotation>> annotationClasses() { + return ImmutableSet.of(dagger.Provides.class); + } + + @Override public <T> Key<T> prepareMethod( + Binder binder, Annotation rawAnnotation, Key<T> key, InjectionPoint injectionPoint) { + Method providesMethod = (Method) injectionPoint.getMember(); + Provides annotation = (Provides) rawAnnotation; + switch (annotation.type()) { + case UNIQUE: + return key; + case MAP: + /* TODO(cgruber) implement map bindings */ + binder.addError("Map bindings are not yet supported."); + case SET: + return processSetBinding(binder, key); + case SET_VALUES: + binder.addError(Type.SET_VALUES.name() + " contributions are not supported by Guice.", + providesMethod); + return key; + default: + binder.addError("Unknown @Provides type " + annotation.type() + ".", providesMethod); + return key; + } + } + + private static <T> Key<T> processSetBinding(Binder binder, Key<T> key) { + Multibinder<T> setBinder = Multibinder.newSetBinder(binder, key.getTypeLiteral()); + Key<T> newKey = Key.get(key.getTypeLiteral(), UniqueAnnotations.create()); + setBinder.addBinding().to(newKey); + return newKey; + } + + private DaggerMethodScanner() {} +}
\ No newline at end of file diff --git a/extensions/dagger-adapter/test/com/google/inject/daggeradapter/DaggerAdapterTest.java b/extensions/dagger-adapter/test/com/google/inject/daggeradapter/DaggerAdapterTest.java new file mode 100644 index 00000000..30e1d186 --- /dev/null +++ b/extensions/dagger-adapter/test/com/google/inject/daggeradapter/DaggerAdapterTest.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.daggeradapter; + +import static dagger.Provides.Type.SET; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.multibindings.Multibinder; +import com.google.inject.util.Providers; + +import junit.framework.TestCase; + +import java.util.Set; + +/** + * Tests for {@link DaggerAdapter}. + * + * @author cgruber@google.com (Christian Gruber) + */ + +public class DaggerAdapterTest extends TestCase { + @dagger.Module static class SimpleDaggerModule { + @dagger.Provides Integer anInteger() { + return 1; + } + } + + public void testSimpleModule() { + Injector i = Guice.createInjector(DaggerAdapter.from(new SimpleDaggerModule())); + assertEquals((Integer) 1, i.getInstance(Integer.class)); + } + + static class SimpleGuiceModule extends AbstractModule { + @Provides String aString(Integer i) { + return i.toString(); + } + @Override protected void configure() {} + } + + public void testInteractionWithGuiceModules() { + Injector i = Guice.createInjector( + new SimpleGuiceModule(), + DaggerAdapter.from(new SimpleDaggerModule())); + assertEquals("1", i.getInstance(String.class)); + } + + @dagger.Module static class SetBindingDaggerModule1 { + @dagger.Provides(type=SET) Integer anInteger() { + return 5; + } + } + + @dagger.Module static class SetBindingDaggerModule2 { + @dagger.Provides(type=SET) Integer anInteger() { + return 3; + } + } + + public void testSetBindings() { + Injector i = Guice.createInjector( + DaggerAdapter.from(new SetBindingDaggerModule1(), new SetBindingDaggerModule2())); + assertEquals(ImmutableSet.of(3, 5), i.getInstance(new Key<Set<Integer>>() {})); + } + + static class MultibindingGuiceModule implements Module { + @Override public void configure(Binder binder) { + Multibinder<Integer> mb = Multibinder.newSetBinder(binder, Integer.class); + mb.addBinding().toInstance(13); + mb.addBinding().toProvider(Providers.of(8)); // mix'n'match. + } + } + + public void testSetBindingsWithGuiceModule() { + Injector i = Guice.createInjector( + new MultibindingGuiceModule(), + DaggerAdapter.from(new SetBindingDaggerModule1(), new SetBindingDaggerModule2())); + assertEquals(ImmutableSet.of(13, 3, 5, 8), i.getInstance(new Key<Set<Integer>>() {})); + } +} diff --git a/extensions/jmx/.gitignore b/extensions/jmx/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/jmx/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/extensions/mini/.gitignore b/extensions/mini/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/mini/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/extensions/multibindings/.gitignore b/extensions/multibindings/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/multibindings/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java new file mode 100644 index 00000000..47c8c17d --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/ClassMapKey.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Allows {@literal @}{@link ProvidesIntoMap} to specify a class map key. + */ +@MapKey(unwrapValue = true) +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ClassMapKey { + Class<?> value(); +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java index 89df7713..fc3d74f5 100644 --- a/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java +++ b/extensions/multibindings/src/com/google/inject/multibindings/MapBinder.java @@ -143,7 +143,7 @@ public abstract class MapBinder<K, V> { public static <K, V> MapBinder<K, V> newMapBinder(Binder binder, TypeLiteral<K> keyType, TypeLiteral<V> valueType) { binder = binder.skipSources(MapBinder.class, RealMapBinder.class); - return newMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType)), + return newRealMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType)), Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType))); } @@ -163,7 +163,7 @@ public abstract class MapBinder<K, V> { public static <K, V> MapBinder<K, V> newMapBinder(Binder binder, TypeLiteral<K> keyType, TypeLiteral<V> valueType, Annotation annotation) { binder = binder.skipSources(MapBinder.class, RealMapBinder.class); - return newMapBinder(binder, keyType, valueType, + return newRealMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType), annotation), Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotation)); } @@ -184,7 +184,7 @@ public abstract class MapBinder<K, V> { public static <K, V> MapBinder<K, V> newMapBinder(Binder binder, TypeLiteral<K> keyType, TypeLiteral<V> valueType, Class<? extends Annotation> annotationType) { binder = binder.skipSources(MapBinder.class, RealMapBinder.class); - return newMapBinder(binder, keyType, valueType, + return newRealMapBinder(binder, keyType, valueType, Key.get(mapOf(keyType, valueType), annotationType), Multibinder.newSetBinder(binder, entryOfProviderOf(keyType, valueType), annotationType)); } @@ -236,7 +236,19 @@ public abstract class MapBinder<K, V> { Map.class, Entry.class, keyType.getType(), Types.providerOf(valueType.getType()))); } - private static <K, V> MapBinder<K, V> newMapBinder(Binder binder, + // Note: We use valueTypeAndAnnotation effectively as a Pair<TypeLiteral, Annotation|Class> + // since it's an easy way to group a type and an optional annotation type or instance. + static <K, V> RealMapBinder<K, V> newRealMapBinder(Binder binder, TypeLiteral<K> keyType, + Key<V> valueTypeAndAnnotation) { + binder = binder.skipSources(MapBinder.class, RealMapBinder.class); + TypeLiteral<V> valueType = valueTypeAndAnnotation.getTypeLiteral(); + return newRealMapBinder(binder, keyType, valueType, + valueTypeAndAnnotation.ofType(mapOf(keyType, valueType)), + Multibinder.newSetBinder(binder, + valueTypeAndAnnotation.ofType(entryOfProviderOf(keyType, valueType)))); + } + + private static <K, V> RealMapBinder<K, V> newRealMapBinder(Binder binder, TypeLiteral<K> keyType, TypeLiteral<V> valueType, Key<Map<K, V>> mapKey, Multibinder<Entry<K, Provider<V>>> entrySetBinder) { RealMapBinder<K, V> mapBinder = @@ -342,12 +354,8 @@ public abstract class MapBinder<K, V> { multimapKey, providerMultimapKey, entrySetBinder.getSetKey())); return this; } - - /** - * This creates two bindings. One for the {@code Map.Entry<K, Provider<V>>} - * and another for {@code V}. - */ - @Override public LinkedBindingBuilder<V> addBinding(K key) { + + Key<V> getKeyForNewValue(K key) { checkNotNull(key, "key"); checkConfiguration(!isInitialized(), "MapBinder was already initialized"); @@ -355,7 +363,15 @@ public abstract class MapBinder<K, V> { new RealElement(entrySetBinder.getSetName(), MAPBINDER, keyType.toString())); entrySetBinder.addBinding().toProvider(new ProviderMapEntry<K, V>( key, binder.getProvider(valueKey), valueKey)); - return binder.bind(valueKey); + return valueKey; + } + + /** + * This creates two bindings. One for the {@code Map.Entry<K, Provider<V>>} + * and another for {@code V}. + */ + @Override public LinkedBindingBuilder<V> addBinding(K key) { + return binder.bind(getKeyForNewValue(key)); } @Override public void configure(Binder binder) { diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java new file mode 100644 index 00000000..bcb6a3b8 --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/MapKey.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Allows users define customized key type annotations for map bindings by annotating an annotation + * of a {@code Map}'s key type. The custom key annotation can be applied to methods also annotated + * with {@literal @}{@link ProvidesIntoMap}. + * + * <p>A {@link StringMapKey} and {@link ClassMapKey} are provided for convenience with maps whose + * keys are strings or classes. For maps with enums or primitive types as keys, you must provide + * your own MapKey annotation, such as this one for an enum: + * + * <pre> + * {@literal @}MapKey(unwrapValue = true) + * {@literal @}Retention(RUNTIME) + * public {@literal @}interface MyCustomEnumKey { + * MyCustomEnum value(); + * } + * </pre> + * + * You can also use the whole annotation as the key, if {@code unwrapValue=false}. + * When unwrapValue is false, the annotation type will be the key type for the injected map and + * the annotation instances will be the key values. If {@code unwrapValue=true}, the value() type + * will be the key type for injected map and the value() instances will be the keys values. + */ +@Documented +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +public @interface MapKey { + /** + * if {@code unwrapValue} is false, then the whole annotation will be the type and annotation + * instances will be the keys. If {@code unwrapValue} is true, the value() type of key type + * annotation will be the key type for injected map and the value instances will be the keys. + */ + boolean unwrapValue(); +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java index 66a4951f..56433f78 100644 --- a/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java +++ b/extensions/multibindings/src/com/google/inject/multibindings/Multibinder.java @@ -121,10 +121,7 @@ public abstract class Multibinder<T> { * itself bound with no binding annotation. */ public static <T> Multibinder<T> newSetBinder(Binder binder, TypeLiteral<T> type) { - binder = binder.skipSources(RealMultibinder.class, Multibinder.class); - RealMultibinder<T> result = new RealMultibinder<T>(binder, type, Key.get(setOf(type))); - binder.install(result); - return result; + return newRealSetBinder(binder, Key.get(type)); } /** @@ -132,7 +129,7 @@ public abstract class Multibinder<T> { * itself bound with no binding annotation. */ public static <T> Multibinder<T> newSetBinder(Binder binder, Class<T> type) { - return newSetBinder(binder, TypeLiteral.get(type)); + return newRealSetBinder(binder, Key.get(type)); } /** @@ -141,11 +138,7 @@ public abstract class Multibinder<T> { */ public static <T> Multibinder<T> newSetBinder( Binder binder, TypeLiteral<T> type, Annotation annotation) { - binder = binder.skipSources(RealMultibinder.class, Multibinder.class); - RealMultibinder<T> result = - new RealMultibinder<T>(binder, type, Key.get(setOf(type), annotation)); - binder.install(result); - return result; + return newRealSetBinder(binder, Key.get(type, annotation)); } /** @@ -154,7 +147,7 @@ public abstract class Multibinder<T> { */ public static <T> Multibinder<T> newSetBinder( Binder binder, Class<T> type, Annotation annotation) { - return newSetBinder(binder, TypeLiteral.get(type), annotation); + return newRealSetBinder(binder, Key.get(type, annotation)); } /** @@ -163,9 +156,24 @@ public abstract class Multibinder<T> { */ public static <T> Multibinder<T> newSetBinder(Binder binder, TypeLiteral<T> type, Class<? extends Annotation> annotationType) { + return newRealSetBinder(binder, Key.get(type, annotationType)); + } + + /** + * Returns a new multibinder that collects instances of the key's type in a {@link Set} that is + * itself bound with the annotation (if any) of the key. + */ + public static <T> Multibinder<T> newSetBinder(Binder binder, Key<T> key) { + return newRealSetBinder(binder, key); + } + + /** + * Implementation of newSetBinder. + */ + static <T> RealMultibinder<T> newRealSetBinder(Binder binder, Key<T> key) { binder = binder.skipSources(RealMultibinder.class, Multibinder.class); - RealMultibinder<T> result = - new RealMultibinder<T>(binder, type, Key.get(setOf(type), annotationType)); + RealMultibinder<T> result = new RealMultibinder<T>(binder, key.getTypeLiteral(), + key.ofType(setOf(key.getTypeLiteral()))); binder.install(result); return result; } @@ -176,7 +184,7 @@ public abstract class Multibinder<T> { */ public static <T> Multibinder<T> newSetBinder(Binder binder, Class<T> type, Class<? extends Annotation> annotationType) { - return newSetBinder(binder, TypeLiteral.get(type), annotationType); + return newSetBinder(binder, Key.get(type, annotationType)); } @SuppressWarnings("unchecked") // wrapping a T in a Set safely returns a Set<T> @@ -295,11 +303,14 @@ public abstract class Multibinder<T> { binder.install(new PermitDuplicatesModule(permitDuplicatesKey)); return this; } - - @Override public LinkedBindingBuilder<T> addBinding() { + + Key<T> getKeyForNewItem() { checkConfiguration(!isInitialized(), "Multibinder was already initialized"); + return Key.get(elementType, new RealElement(setName, MULTIBINDER, "")); + } - return binder.bind(Key.get(elementType, new RealElement(setName, MULTIBINDER, ""))); + @Override public LinkedBindingBuilder<T> addBinding() { + return binder.bind(getKeyForNewItem()); } /** diff --git a/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java b/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java new file mode 100644 index 00000000..02fce320 --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/MultibindingsScanner.java @@ -0,0 +1,192 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.spi.InjectionPoint; +import com.google.inject.spi.ModuleAnnotatedMethodScanner; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * Scans a module for annotations that signal multibindings, mapbindings, and optional bindings. + */ +public class MultibindingsScanner { + + private MultibindingsScanner() {} + + /** + * Returns a module that, when installed, will scan all modules for methods with the annotations + * {@literal @}{@link ProvidesIntoMap}, {@literal @}{@link ProvidesIntoSet}, and + * {@literal @}{@link ProvidesIntoOptional}. + * + * <p>This is a convenience method, equivalent to doing + * {@code binder().scanModulesForAnnotatedMethods(MultibindingsScanner.scanner())}. + */ + public static Module asModule() { + return new AbstractModule() { + @Override protected void configure() { + binder().scanModulesForAnnotatedMethods(Scanner.INSTANCE); + } + }; + } + + /** + * Returns a {@link ModuleAnnotatedMethodScanner} that, when bound, will scan all modules for + * methods with the annotations {@literal @}{@link ProvidesIntoMap}, + * {@literal @}{@link ProvidesIntoSet}, and {@literal @}{@link ProvidesIntoOptional}. + */ + public static ModuleAnnotatedMethodScanner scanner() { + return Scanner.INSTANCE; + } + + private static class Scanner extends ModuleAnnotatedMethodScanner { + private static final Scanner INSTANCE = new Scanner(); + + @Override + public Set<? extends Class<? extends Annotation>> annotationClasses() { + return ImmutableSet.of( + ProvidesIntoSet.class, ProvidesIntoMap.class, ProvidesIntoOptional.class); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) // mapKey doesn't know its key type + @Override + public <T> Key<T> prepareMethod(Binder binder, Annotation annotation, Key<T> key, + InjectionPoint injectionPoint) { + Method method = (Method) injectionPoint.getMember(); + AnnotationOrError mapKey = findMapKeyAnnotation(binder, method); + if (annotation instanceof ProvidesIntoSet) { + if (mapKey.annotation != null) { + binder.addError("Found a MapKey annotation on non map binding at %s.", method); + } + return Multibinder.newRealSetBinder(binder, key).getKeyForNewItem(); + } else if (annotation instanceof ProvidesIntoMap) { + if (mapKey.error) { + // Already failed on the MapKey, don't bother doing more work. + return key; + } + if (mapKey.annotation == null) { + // If no MapKey, make an error and abort. + binder.addError("No MapKey found for map binding at %s.", method); + return key; + } + TypeAndValue typeAndValue = typeAndValueOfMapKey(mapKey.annotation); + return MapBinder.newRealMapBinder(binder, typeAndValue.type, key) + .getKeyForNewValue(typeAndValue.value); + } else if (annotation instanceof ProvidesIntoOptional) { + if (mapKey.annotation != null) { + binder.addError("Found a MapKey annotation on non map binding at %s.", method); + } + switch (((ProvidesIntoOptional)annotation).value()) { + case DEFAULT: + return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForDefaultBinding(); + case ACTUAL: + return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForActualBinding(); + } + } + throw new IllegalStateException("Invalid annotation: " + annotation); + } + } + + private static class AnnotationOrError { + final Annotation annotation; + final boolean error; + AnnotationOrError(Annotation annotation, boolean error) { + this.annotation = annotation; + this.error = error; + } + + static AnnotationOrError forPossiblyNullAnnotation(Annotation annotation) { + return new AnnotationOrError(annotation, false); + } + + static AnnotationOrError forError() { + return new AnnotationOrError(null, true); + } + } + + private static AnnotationOrError findMapKeyAnnotation(Binder binder, Method method) { + Annotation foundAnnotation = null; + for (Annotation annotation : method.getAnnotations()) { + MapKey mapKey = annotation.annotationType().getAnnotation(MapKey.class); + if (mapKey != null) { + if (foundAnnotation != null) { + binder.addError("Found more than one MapKey annotations on %s.", method); + return AnnotationOrError.forError(); + } + if (mapKey.unwrapValue()) { + try { + // validate there's a declared method called "value" + Method valueMethod = annotation.annotationType().getDeclaredMethod("value"); + if (valueMethod.getReturnType().isArray()) { + binder.addError("Array types are not allowed in a MapKey with unwrapValue=true: %s", + annotation.annotationType()); + return AnnotationOrError.forError(); + } + } catch (NoSuchMethodException invalid) { + binder.addError("No 'value' method in MapKey with unwrapValue=true: %s", + annotation.annotationType()); + return AnnotationOrError.forError(); + } + } + foundAnnotation = annotation; + } + } + return AnnotationOrError.forPossiblyNullAnnotation(foundAnnotation); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static TypeAndValue<?> typeAndValueOfMapKey(Annotation mapKeyAnnotation) { + if (!mapKeyAnnotation.annotationType().getAnnotation(MapKey.class).unwrapValue()) { + return new TypeAndValue(TypeLiteral.get(mapKeyAnnotation.annotationType()), mapKeyAnnotation); + } else { + try { + Method valueMethod = mapKeyAnnotation.annotationType().getDeclaredMethod("value"); + valueMethod.setAccessible(true); + TypeLiteral<?> returnType = + TypeLiteral.get(mapKeyAnnotation.annotationType()).getReturnType(valueMethod); + return new TypeAndValue(returnType, valueMethod.invoke(mapKeyAnnotation)); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } catch (SecurityException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e); + } + } + } + + private static class TypeAndValue<T> { + final TypeLiteral<T> type; + final T value; + + TypeAndValue(TypeLiteral<T> type, T value) { + this.type = type; + this.value = value; + } + } +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java b/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java index 60e72d9f..d18f974d 100644 --- a/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java +++ b/extensions/multibindings/src/com/google/inject/multibindings/OptionalBinder.java @@ -129,7 +129,7 @@ import javax.inject.Qualifier; * public class FrameworkModule extends AbstractModule { * protected void configure() { * OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class)) - * .setDefault().to(DEFAULT_LOOKUP_URL); + * .setDefault().toInstance(DEFAULT_LOOKUP_URL); * } * }</code></pre> * With the above module, code can inject an {@code @LookupUrl String} and it @@ -138,7 +138,7 @@ import javax.inject.Qualifier; * public class UserLookupModule extends AbstractModule { * protected void configure() { * OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class)) - * .setBinding().to(CUSTOM_LOOKUP_URL); + * .setBinding().toInstance(CUSTOM_LOOKUP_URL); * } * }</code></pre> * ... which will override the default value. @@ -150,12 +150,12 @@ import javax.inject.Qualifier; * public class FrameworkModule extends AbstractModule { * protected void configure() { * OptionalBinder.newOptionalBinder(binder(), Key.get(String.class, LookupUrl.class)) - * .setDefault().to(DEFAULT_LOOKUP_URL); + * .setDefault().toInstance(DEFAULT_LOOKUP_URL); * } * } * public class UserLookupModule extends AbstractModule { * protected void configure() { - * bind(Key.get(String.class, LookupUrl.class)).to(CUSTOM_LOOKUP_URL); + * bind(Key.get(String.class, LookupUrl.class)).toInstance(CUSTOM_LOOKUP_URL); * } * }</code></pre> * ... would generate an error, because both the framework and the user are trying to bind @@ -191,14 +191,18 @@ public abstract class OptionalBinder<T> { private OptionalBinder() {} public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, Class<T> type) { - return newOptionalBinder(binder, Key.get(type)); + return newRealOptionalBinder(binder, Key.get(type)); } public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, TypeLiteral<T> type) { - return newOptionalBinder(binder, Key.get(type)); + return newRealOptionalBinder(binder, Key.get(type)); } public static <T> OptionalBinder<T> newOptionalBinder(Binder binder, Key<T> type) { + return newRealOptionalBinder(binder, type); + } + + static <T> RealOptionalBinder<T> newRealOptionalBinder(Binder binder, Key<T> type) { binder = binder.skipSources(OptionalBinder.class, RealOptionalBinder.class); RealOptionalBinder<T> optionalBinder = new RealOptionalBinder<T>(binder, type); binder.install(optionalBinder); @@ -349,16 +353,24 @@ public abstract class OptionalBinder<T> { binder.bind(typeKey).toProvider(new RealDirectTypeProvider()); } - @Override public LinkedBindingBuilder<T> setDefault() { + Key<T> getKeyForDefaultBinding() { checkConfiguration(!isInitialized(), "already initialized"); addDirectTypeBinding(binder); - return binder.bind(defaultKey); + return defaultKey; } - @Override public LinkedBindingBuilder<T> setBinding() { + @Override public LinkedBindingBuilder<T> setDefault() { + return binder.bind(getKeyForDefaultBinding()); + } + + Key<T> getKeyForActualBinding() { checkConfiguration(!isInitialized(), "already initialized"); addDirectTypeBinding(binder); - return binder.bind(actualKey); + return actualKey; + } + + @Override public LinkedBindingBuilder<T> setBinding() { + return binder.bind(getKeyForActualBinding()); } @Override public void configure(Binder binder) { diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java new file mode 100644 index 00000000..886a0fd1 --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoMap.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates methods of a {@link Module} to add items to a {@link MapBinder}. + * The method's return type, binding annotation and additional key annotation determines + * what Map this will contribute to. For example, + * + * <pre> + * {@literal @}ProvidesIntoMap + * {@literal @}StringMapKey("Foo") + * {@literal @}Named("plugins") + * Plugin provideFooUrl(FooManager fm) { returm fm.getPlugin(); } + * + * {@literal @}ProvidesIntoMap + * {@literal @}StringMapKey("Bar") + * {@literal @}Named("urls") + * Plugin provideBarUrl(BarManager bm) { return bm.getPlugin(); } + * </pre> + * + * will add two items to the {@code @Named("urls") Map<String, Plugin>} map. The key 'Foo' + * will map to the provideFooUrl method, and the key 'Bar' will map to the provideBarUrl method. + * The values are bound as providers and will be evaluated at injection time. + * + * <p>Because the key is specified as an annotation, only Strings, Classes, enums, primitive + * types and annotation instances are supported as keys. + * + * @author sameb@google.com (Sam Berlin) + * @since 4.0 + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoMap { +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java new file mode 100644 index 00000000..6f021700 --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoOptional.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates methods of a {@link Module} to add items to a {@link Multibinder}. + * The method's return type and binding annotation determines what Optional this will + * contribute to. For example, + * + * <pre> + * {@literal @}ProvidesIntoOptional(DEFAULT) + * {@literal @}Named("url") + * String provideFooUrl(FooManager fm) { returm fm.getUrl(); } + * + * {@literal @}ProvidesIntoOptional(ACTUAL) + * {@literal @}Named("url") + * String provideBarUrl(BarManager bm) { return bm.getUrl(); } + * </pre> + * + * will set the default value of {@code @Named("url") Optional<String>} to foo's URL, + * and then override it to bar's URL. + * + * @author sameb@google.com (Sam Berlin) + * @since 4.0 + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoOptional { + enum Type { + /** Corresponds to {@link OptionalBinder#setBinding}. */ + ACTUAL, + + /** Corresponds to {@link OptionalBinder#setDefault}. */ + DEFAULT + } + + /** Specifies if the binding is for the actual or default value. */ + Type value(); +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java new file mode 100644 index 00000000..b26df683 --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/ProvidesIntoSet.java @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.Module; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Annotates methods of a {@link Module} to add items to a {@link Multibinder}. + * The method's return type and binding annotation determines what Set this will + * contribute to. For example, + * + * <pre> + * {@literal @}ProvidesIntoSet + * {@literal @}Named("urls") + * String provideFooUrl(FooManager fm) { returm fm.getUrl(); } + * + * {@literal @}ProvidesIntoSet + * {@literal @}Named("urls") + * String provideBarUrl(BarManager bm) { return bm.getUrl(); } + * </pre> + * + * will add two items to the {@code @Named("urls") Set<String>} set. The items are bound as + * providers and will be evaluated at injection time. + * + * @author sameb@google.com (Sam Berlin) + * @since 4.0 + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ProvidesIntoSet { +} diff --git a/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java b/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java new file mode 100644 index 00000000..dd4d99bc --- /dev/null +++ b/extensions/multibindings/src/com/google/inject/multibindings/StringMapKey.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Allows {@literal @}{@link ProvidesIntoMap} to specify a string map key. + */ +@MapKey(unwrapValue = true) +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface StringMapKey { + String value(); +} diff --git a/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java b/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java index 9ea52db3..6806edde 100644 --- a/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java +++ b/extensions/multibindings/test/com/google/inject/multibindings/AllTests.java @@ -30,6 +30,7 @@ public class AllTests { suite.addTestSuite(MultibinderTest.class); suite.addTestSuite(OptionalBinderTest.class); suite.addTestSuite(RealElementTest.class); + suite.addTestSuite(ProvidesIntoTest.class); return suite; } } diff --git a/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java b/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java new file mode 100644 index 00000000..62c7a58b --- /dev/null +++ b/extensions/multibindings/test/com/google/inject/multibindings/ProvidesIntoTest.java @@ -0,0 +1,373 @@ +/** + * Copyright (C) 2015 Google Inc. + * + * 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.google.inject.multibindings; + +import static com.google.inject.Asserts.assertContains; +import static com.google.inject.name.Names.named; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +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.Module; +import com.google.inject.multibindings.ProvidesIntoOptional.Type; +import com.google.inject.name.Named; + +import junit.framework.TestCase; + +import java.lang.annotation.Retention; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +/** + * Tests the various @ProvidesInto annotations. + * + * @author sameb@google.com (Sam Berlin) + */ +public class ProvidesIntoTest extends TestCase { + + public void testAnnotation() throws Exception { + Injector injector = Guice.createInjector(MultibindingsScanner.asModule(), new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoSet + @Named("foo") + String setFoo() { return "foo"; } + + @ProvidesIntoSet + @Named("foo") + String setFoo2() { return "foo2"; } + + @ProvidesIntoSet + @Named("bar") + String setBar() { return "bar"; } + + @ProvidesIntoSet + @Named("bar") + String setBar2() { return "bar2"; } + + @ProvidesIntoSet + String setNoAnnotation() { return "na"; } + + @ProvidesIntoSet + String setNoAnnotation2() { return "na2"; } + + @ProvidesIntoMap + @StringMapKey("fooKey") + @Named("foo") + String mapFoo() { return "foo"; } + + @ProvidesIntoMap + @StringMapKey("foo2Key") + @Named("foo") + String mapFoo2() { return "foo2"; } + + @ProvidesIntoMap + @ClassMapKey(String.class) + @Named("bar") + String mapBar() { return "bar"; } + + @ProvidesIntoMap + @ClassMapKey(Number.class) + @Named("bar") + String mapBar2() { return "bar2"; } + + @ProvidesIntoMap + @TestEnumKey(TestEnum.A) + String mapNoAnnotation() { return "na"; } + + @ProvidesIntoMap + @TestEnumKey(TestEnum.B) + String mapNoAnnotation2() { return "na2"; } + + @ProvidesIntoMap + @WrappedKey(number = 1) + Number wrapped1() { return 11; } + + @ProvidesIntoMap + @WrappedKey(number = 2) + Number wrapped2() { return 22; } + + @ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT) + @Named("foo") + String optionalDefaultFoo() { return "foo"; } + + @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL) + @Named("foo") + String optionalActualFoo() { return "foo2"; } + + @ProvidesIntoOptional(ProvidesIntoOptional.Type.DEFAULT) + @Named("bar") + String optionalDefaultBar() { return "bar"; } + + @ProvidesIntoOptional(ProvidesIntoOptional.Type.ACTUAL) + String optionalActualBar() { return "na2"; } + }); + + Set<String> fooSet = injector.getInstance(new Key<Set<String>>(named("foo")) {}); + assertEquals(ImmutableSet.of("foo", "foo2"), fooSet); + + Set<String> barSet = injector.getInstance(new Key<Set<String>>(named("bar")) {}); + assertEquals(ImmutableSet.of("bar", "bar2"), barSet); + + Set<String> noAnnotationSet = injector.getInstance(new Key<Set<String>>() {}); + assertEquals(ImmutableSet.of("na", "na2"), noAnnotationSet); + + Map<String, String> fooMap = + injector.getInstance(new Key<Map<String, String>>(named("foo")) {}); + assertEquals(ImmutableMap.of("fooKey", "foo", "foo2Key", "foo2"), fooMap); + + Map<Class<?>, String> barMap = + injector.getInstance(new Key<Map<Class<?>, String>>(named("bar")) {}); + assertEquals(ImmutableMap.of(String.class, "bar", Number.class, "bar2"), barMap); + + Map<TestEnum, String> noAnnotationMap = + injector.getInstance(new Key<Map<TestEnum, String>>() {}); + assertEquals(ImmutableMap.of(TestEnum.A, "na", TestEnum.B, "na2"), noAnnotationMap); + + Map<WrappedKey, Number> wrappedMap = + injector.getInstance(new Key<Map<WrappedKey, Number>>() {}); + assertEquals(ImmutableMap.of(wrappedKeyFor(1), 11, wrappedKeyFor(2), 22), wrappedMap); + + Optional<String> fooOptional = + injector.getInstance(new Key<Optional<String>>(named("foo")) {}); + assertEquals("foo2", fooOptional.get()); + + Optional<String> barOptional = + injector.getInstance(new Key<Optional<String>>(named("bar")) {}); + assertEquals("bar", barOptional.get()); + + Optional<String> noAnnotationOptional = + injector.getInstance(new Key<Optional<String>>() {}); + assertEquals("na2", noAnnotationOptional.get()); + } + + enum TestEnum { + A, B + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface TestEnumKey { + TestEnum value(); + } + + @MapKey(unwrapValue = false) + @Retention(RUNTIME) + @interface WrappedKey { + int number(); + } + + @SuppressWarnings("unused") @WrappedKey(number=1) private static Object wrappedKey1Holder; + @SuppressWarnings("unused") @WrappedKey(number=2) private static Object wrappedKey2Holder; + WrappedKey wrappedKeyFor(int number) throws Exception { + Field field; + switch (number) { + case 1: + field = ProvidesIntoTest.class.getDeclaredField("wrappedKey1Holder"); + break; + case 2: + field = ProvidesIntoTest.class.getDeclaredField("wrappedKey2Holder"); + break; + default: + throw new IllegalArgumentException("only 1 or 2 supported"); + } + return field.getAnnotation(WrappedKey.class); + } + + public void testDoubleScannerIsIgnored() { + Injector injector = Guice.createInjector( + MultibindingsScanner.asModule(), + MultibindingsScanner.asModule(), + new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoSet String provideFoo() { return "foo"; } + } + ); + assertEquals(ImmutableSet.of("foo"), injector.getInstance(new Key<Set<String>>() {})); + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface ArrayUnwrappedKey { + int[] value(); + } + + public void testArrayKeys_unwrapValuesTrue() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap @ArrayUnwrappedKey({1, 2}) String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), + "Array types are not allowed in a MapKey with unwrapValue=true: " + + ArrayUnwrappedKey.class.getName(), + "at " + m.getClass().getName() + ".provideFoo("); + } + } + + @MapKey(unwrapValue = false) + @Retention(RUNTIME) + @interface ArrayWrappedKey { + int[] number(); + } + + @SuppressWarnings("unused") @ArrayWrappedKey(number={1, 2}) private static Object arrayWrappedKeyHolder12; + @SuppressWarnings("unused") @ArrayWrappedKey(number={3, 4}) private static Object arrayWrappedKeyHolder34; + ArrayWrappedKey arrayWrappedKeyFor(int number) throws Exception { + Field field; + switch (number) { + case 12: + field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder12"); + break; + case 34: + field = ProvidesIntoTest.class.getDeclaredField("arrayWrappedKeyHolder34"); + break; + default: + throw new IllegalArgumentException("only 1 or 2 supported"); + } + return field.getAnnotation(ArrayWrappedKey.class); + } + + public void testArrayKeys_unwrapValuesFalse() throws Exception { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap @ArrayWrappedKey(number = {1, 2}) String provideFoo() { return "foo"; } + @ProvidesIntoMap @ArrayWrappedKey(number = {3, 4}) String provideBar() { return "bar"; } + }; + Injector injector = Guice.createInjector(MultibindingsScanner.asModule(), m); + Map<ArrayWrappedKey, String> map = + injector.getInstance(new Key<Map<ArrayWrappedKey, String>>() {}); + ArrayWrappedKey key12 = arrayWrappedKeyFor(12); + ArrayWrappedKey key34 = arrayWrappedKeyFor(34); + assertEquals("foo", map.get(key12)); + assertEquals("bar", map.get(key34)); + assertEquals(2, map.size()); + } + + public void testProvidesIntoSetWithMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoSet @TestEnumKey(TestEnum.A) String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found a MapKey annotation on non map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + public void testProvidesIntoOptionalWithMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoOptional(Type.ACTUAL) + @TestEnumKey(TestEnum.A) + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found a MapKey annotation on non map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + public void testProvidesIntoMapWithoutMapKey() { + Module m = new AbstractModule() { + @Override protected void configure() {} + @ProvidesIntoMap String provideFoo() { return "foo"; } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "No MapKey found for map binding at " + + m.getClass().getName() + ".provideFoo"); + } + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface TestEnumKey2 { + TestEnum value(); + } + + public void testMoreThanOneMapKeyAnnotation() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoMap + @TestEnumKey(TestEnum.A) + @TestEnumKey2(TestEnum.B) + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "Found more than one MapKey annotations on " + + m.getClass().getName() + ".provideFoo"); + } + } + + @MapKey(unwrapValue = true) + @Retention(RUNTIME) + @interface MissingValueMethod { + } + + public void testMapKeyMissingValueMethod() { + Module m = new AbstractModule() { + @Override protected void configure() {} + + @ProvidesIntoMap + @MissingValueMethod + String provideFoo() { + return "foo"; + } + }; + try { + Guice.createInjector(MultibindingsScanner.asModule(), m); + fail(); + } catch (CreationException ce) { + assertEquals(1, ce.getErrorMessages().size()); + assertContains(ce.getMessage(), "No 'value' method in MapKey with unwrapValue=true: " + + MissingValueMethod.class.getName()); + } + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 084967cf..80824241 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -18,6 +18,8 @@ <modules> <module>assistedinject</module> + <!-- TODO(cgruber): Comment out dagger-adapter module if guice releases before dagger 2.x --> + <module>dagger-adapter</module> <module>grapher</module> <module>jmx</module> <module>jndi</module> diff --git a/extensions/spring/.gitignore b/extensions/spring/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/spring/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/extensions/struts2/.gitignore b/extensions/struts2/.gitignore new file mode 100644 index 00000000..84c048a7 --- /dev/null +++ b/extensions/struts2/.gitignore @@ -0,0 +1 @@ +/build/ @@ -467,4 +467,15 @@ See the Apache License Version 2.0 for the specific language governing permissio </profile> </profiles> + <!-- TODO(cgruber): Update the google parent pom or migrate to sonatype's --> + <!-- TODO(cgruber): Comment out dagger-adapter from extensions/pom.xml if v2 is not released. --> + <repositories> + <repository> + <id>sonatype-nexus-snapshots</id> + <name>Sonatype Nexus Snapshots</name> + <url>https://oss.sonatype.org/content/repositories/snapshots</url> + <releases><enabled>false</enabled></releases> + <snapshots><enabled>true</enabled></snapshots> + </repository> + </repositories> </project> |