• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2015 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.inject.multibindings;
18 
19 import com.google.common.collect.ImmutableSet;
20 import com.google.inject.AbstractModule;
21 import com.google.inject.Binder;
22 import com.google.inject.Key;
23 import com.google.inject.Module;
24 import com.google.inject.TypeLiteral;
25 import com.google.inject.spi.InjectionPoint;
26 import com.google.inject.spi.ModuleAnnotatedMethodScanner;
27 
28 import java.lang.annotation.Annotation;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.util.Set;
32 
33 /**
34  * Scans a module for annotations that signal multibindings, mapbindings, and optional bindings.
35  *
36  * @since 4.0
37  */
38 public class MultibindingsScanner {
39 
MultibindingsScanner()40   private MultibindingsScanner() {}
41 
42   /**
43    * Returns a module that, when installed, will scan all modules for methods with the annotations
44    * {@literal @}{@link ProvidesIntoMap}, {@literal @}{@link ProvidesIntoSet}, and
45    * {@literal @}{@link ProvidesIntoOptional}.
46    *
47    * <p>This is a convenience method, equivalent to doing
48    * {@code binder().scanModulesForAnnotatedMethods(MultibindingsScanner.scanner())}.
49    */
asModule()50   public static Module asModule() {
51     return new AbstractModule() {
52       @Override protected void configure() {
53         binder().scanModulesForAnnotatedMethods(Scanner.INSTANCE);
54       }
55     };
56   }
57 
58   /**
59    * Returns a {@link ModuleAnnotatedMethodScanner} that, when bound, will scan all modules for
60    * methods with the annotations {@literal @}{@link ProvidesIntoMap},
61    * {@literal @}{@link ProvidesIntoSet}, and {@literal @}{@link ProvidesIntoOptional}.
62    */
63   public static ModuleAnnotatedMethodScanner scanner() {
64     return Scanner.INSTANCE;
65   }
66 
67   private static class Scanner extends ModuleAnnotatedMethodScanner {
68     private static final Scanner INSTANCE = new Scanner();
69 
70     @Override
71     public Set<? extends Class<? extends Annotation>> annotationClasses() {
72       return ImmutableSet.of(
73           ProvidesIntoSet.class, ProvidesIntoMap.class, ProvidesIntoOptional.class);
74     }
75 
76     @SuppressWarnings({"unchecked", "rawtypes"}) // mapKey doesn't know its key type
77     @Override
78     public <T> Key<T> prepareMethod(Binder binder, Annotation annotation, Key<T> key,
79         InjectionPoint injectionPoint) {
80       Method method = (Method) injectionPoint.getMember();
81       AnnotationOrError mapKey = findMapKeyAnnotation(binder, method);
82       if (annotation instanceof ProvidesIntoSet) {
83         if (mapKey.annotation != null) {
84           binder.addError("Found a MapKey annotation on non map binding at %s.", method);
85         }
86         return Multibinder.newRealSetBinder(binder, key).getKeyForNewItem();
87       } else if (annotation instanceof ProvidesIntoMap) {
88         if (mapKey.error) {
89           // Already failed on the MapKey, don't bother doing more work.
90           return key;
91         }
92         if (mapKey.annotation == null) {
93           // If no MapKey, make an error and abort.
94           binder.addError("No MapKey found for map binding at %s.", method);
95           return key;
96         }
97         TypeAndValue typeAndValue = typeAndValueOfMapKey(mapKey.annotation);
98         return MapBinder.newRealMapBinder(binder, typeAndValue.type, key)
99             .getKeyForNewValue(typeAndValue.value);
100       } else if (annotation instanceof ProvidesIntoOptional) {
101         if (mapKey.annotation != null) {
102           binder.addError("Found a MapKey annotation on non map binding at %s.", method);
103         }
104         switch (((ProvidesIntoOptional)annotation).value()) {
105           case DEFAULT:
106             return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForDefaultBinding();
107           case ACTUAL:
108             return OptionalBinder.newRealOptionalBinder(binder, key).getKeyForActualBinding();
109         }
110       }
111       throw new IllegalStateException("Invalid annotation: " + annotation);
112     }
113   }
114 
115   private static class AnnotationOrError {
116     final Annotation annotation;
117     final boolean error;
118     AnnotationOrError(Annotation annotation, boolean error) {
119       this.annotation = annotation;
120       this.error = error;
121     }
122 
123     static AnnotationOrError forPossiblyNullAnnotation(Annotation annotation) {
124       return new AnnotationOrError(annotation, false);
125     }
126 
127     static AnnotationOrError forError() {
128       return new AnnotationOrError(null, true);
129     }
130   }
131 
132   private static AnnotationOrError findMapKeyAnnotation(Binder binder, Method method) {
133     Annotation foundAnnotation = null;
134     for (Annotation annotation : method.getAnnotations()) {
135       MapKey mapKey = annotation.annotationType().getAnnotation(MapKey.class);
136       if (mapKey != null) {
137         if (foundAnnotation != null) {
138           binder.addError("Found more than one MapKey annotations on %s.", method);
139           return AnnotationOrError.forError();
140         }
141         if (mapKey.unwrapValue()) {
142           try {
143             // validate there's a declared method called "value"
144             Method valueMethod = annotation.annotationType().getDeclaredMethod("value");
145             if (valueMethod.getReturnType().isArray()) {
146               binder.addError("Array types are not allowed in a MapKey with unwrapValue=true: %s",
147                   annotation.annotationType());
148               return AnnotationOrError.forError();
149             }
150           } catch (NoSuchMethodException invalid) {
151             binder.addError("No 'value' method in MapKey with unwrapValue=true: %s",
152                 annotation.annotationType());
153             return AnnotationOrError.forError();
154           }
155         }
156         foundAnnotation = annotation;
157       }
158     }
159     return AnnotationOrError.forPossiblyNullAnnotation(foundAnnotation);
160   }
161 
162   @SuppressWarnings({"unchecked", "rawtypes"})
163   static TypeAndValue<?> typeAndValueOfMapKey(Annotation mapKeyAnnotation) {
164     if (!mapKeyAnnotation.annotationType().getAnnotation(MapKey.class).unwrapValue()) {
165       return new TypeAndValue(TypeLiteral.get(mapKeyAnnotation.annotationType()), mapKeyAnnotation);
166     } else {
167       try {
168         Method valueMethod = mapKeyAnnotation.annotationType().getDeclaredMethod("value");
169         valueMethod.setAccessible(true);
170         TypeLiteral<?> returnType =
171             TypeLiteral.get(mapKeyAnnotation.annotationType()).getReturnType(valueMethod);
172         return new TypeAndValue(returnType, valueMethod.invoke(mapKeyAnnotation));
173       } catch (NoSuchMethodException e) {
174         throw new IllegalStateException(e);
175       } catch (SecurityException e) {
176         throw new IllegalStateException(e);
177       } catch (IllegalAccessException e) {
178         throw new IllegalStateException(e);
179       } catch (InvocationTargetException e) {
180         throw new IllegalStateException(e);
181       }
182     }
183   }
184 
185   private static class TypeAndValue<T> {
186     final TypeLiteral<T> type;
187     final T value;
188 
189     TypeAndValue(TypeLiteral<T> type, T value) {
190       this.type = type;
191       this.value = value;
192     }
193   }
194 }
195