Home | History | Annotate | Download | only in multibindings
      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 
     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 }
    195