blob: 95b4d5fa1444c86331ecdc1fff47e1859014af8d [file] [log] [blame]
sameb0b334612015-02-20 10:44:10 -08001/**
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
17package com.google.inject.multibindings;
18
19import com.google.common.collect.ImmutableSet;
20import com.google.inject.AbstractModule;
21import com.google.inject.Binder;
22import com.google.inject.Key;
23import com.google.inject.Module;
24import com.google.inject.TypeLiteral;
25import com.google.inject.spi.InjectionPoint;
26import com.google.inject.spi.ModuleAnnotatedMethodScanner;
27
28import java.lang.annotation.Annotation;
29import java.lang.reflect.InvocationTargetException;
30import java.lang.reflect.Method;
31import java.util.Set;
32
33/**
34 * Scans a module for annotations that signal multibindings, mapbindings, and optional bindings.
Ben McCannbac730f2015-04-21 16:21:01 -070035 *
36 * @since 4.0
sameb0b334612015-02-20 10:44:10 -080037 */
38public class MultibindingsScanner {
39
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 */
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}