1 /* 2 * Copyright (C) 2015 The Dagger Authors. 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 dagger.internal.codegen.binding; 18 19 import static androidx.room.compiler.processing.XTypeKt.isArray; 20 import static com.google.common.base.Preconditions.checkArgument; 21 import static com.google.common.collect.Iterables.getOnlyElement; 22 import static com.squareup.javapoet.MethodSpec.methodBuilder; 23 import static dagger.internal.codegen.base.MapKeyAccessibility.isMapKeyPubliclyAccessible; 24 import static dagger.internal.codegen.binding.SourceFiles.elementBasedClassName; 25 import static dagger.internal.codegen.extension.DaggerCollectors.toOptional; 26 import static dagger.internal.codegen.xprocessing.XElements.asExecutable; 27 import static dagger.internal.codegen.xprocessing.XElements.getSimpleName; 28 import static dagger.internal.codegen.xprocessing.XTypes.isDeclared; 29 import static dagger.internal.codegen.xprocessing.XTypes.isTypeOf; 30 import static dagger.internal.codegen.xprocessing.XTypes.rewrapType; 31 import static javax.lang.model.element.Modifier.PUBLIC; 32 import static javax.lang.model.element.Modifier.STATIC; 33 34 import androidx.room.compiler.processing.XAnnotation; 35 import androidx.room.compiler.processing.XAnnotationValue; 36 import androidx.room.compiler.processing.XElement; 37 import androidx.room.compiler.processing.XMethodElement; 38 import androidx.room.compiler.processing.XProcessingEnv; 39 import androidx.room.compiler.processing.XType; 40 import androidx.room.compiler.processing.XTypeElement; 41 import com.google.common.collect.ImmutableSet; 42 import com.squareup.javapoet.ClassName; 43 import com.squareup.javapoet.CodeBlock; 44 import com.squareup.javapoet.MethodSpec; 45 import dagger.MapKey; 46 import dagger.internal.codegen.base.DaggerSuperficialValidation; 47 import dagger.internal.codegen.base.MapKeyAccessibility; 48 import dagger.internal.codegen.javapoet.TypeNames; 49 import dagger.internal.codegen.model.DaggerAnnotation; 50 import dagger.internal.codegen.xprocessing.XElements; 51 import java.util.NoSuchElementException; 52 import java.util.Optional; 53 54 /** Methods for extracting {@link MapKey} annotations and key code blocks from binding elements. */ 55 public final class MapKeys { 56 57 /** 58 * If {@code bindingElement} is annotated with a {@link MapKey} annotation, returns it. 59 * 60 * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey} 61 * annotation 62 */ getMapKey(XElement bindingElement)63 static Optional<XAnnotation> getMapKey(XElement bindingElement) { 64 return getMapKeys(bindingElement).stream().collect(toOptional()); 65 } 66 67 /** Returns all of the {@link MapKey} annotations that annotate {@code bindingElement}. */ getMapKeys(XElement bindingElement)68 public static ImmutableSet<XAnnotation> getMapKeys(XElement bindingElement) { 69 return XElements.getAnnotatedAnnotations(bindingElement, TypeNames.MAP_KEY); 70 } 71 72 /** 73 * Returns the annotation value if {@code mapKey}'s type is annotated with {@link 74 * MapKey @MapKey(unwrapValue = true)}. 75 * 76 * @throws IllegalArgumentException if {@code mapKey}'s type is not annotated with {@link 77 * MapKey @MapKey} at all. 78 */ unwrapValue(XAnnotation mapKey)79 private static Optional<XAnnotationValue> unwrapValue(XAnnotation mapKey) { 80 XTypeElement mapKeyType = mapKey.getType().getTypeElement(); 81 XAnnotation mapKeyAnnotation = mapKeyType.getAnnotation(TypeNames.MAP_KEY); 82 checkArgument(mapKeyAnnotation != null, "%s is not annotated with @MapKey", mapKeyType); 83 return mapKeyAnnotation.getAsBoolean("unwrapValue") 84 ? Optional.of(getOnlyElement(mapKey.getAnnotationValues())) 85 : Optional.empty(); 86 } 87 mapKeyType(XAnnotation mapKey)88 static XType mapKeyType(XAnnotation mapKey) { 89 return unwrapValue(mapKey).isPresent() 90 ? getUnwrappedMapKeyType(mapKey.getType()) 91 : mapKey.getType(); 92 } 93 94 /** 95 * Returns the map key type for an unwrapped {@link MapKey} annotation type. If the single member 96 * type is primitive, returns the boxed type. 97 * 98 * @throws IllegalArgumentException if {@code mapKeyAnnotationType} is not an annotation type or 99 * has more than one member, or if its single member is an array 100 * @throws NoSuchElementException if the annotation has no members 101 */ getUnwrappedMapKeyType(XType mapKeyAnnotationType)102 public static XType getUnwrappedMapKeyType(XType mapKeyAnnotationType) { 103 checkArgument( 104 isDeclared(mapKeyAnnotationType) 105 && mapKeyAnnotationType.getTypeElement().isAnnotationClass(), 106 "%s is not an annotation type", 107 mapKeyAnnotationType); 108 109 XMethodElement annotationValueMethod = 110 getOnlyElement(mapKeyAnnotationType.getTypeElement().getDeclaredMethods()); 111 XType annotationValueType = annotationValueMethod.getReturnType(); 112 if (isArray(annotationValueType)) { 113 throw new IllegalArgumentException( 114 mapKeyAnnotationType 115 + "." 116 + getSimpleName(annotationValueMethod) 117 + " cannot be an array"); 118 } 119 // If the source kind is Kotlin, the annotation value type is seen as KClass rather than Class, 120 // but either way we want the multibinding key to be Class so we rewrap it here. 121 return isTypeOf(annotationValueType, TypeNames.KCLASS) 122 ? rewrapType(annotationValueType, TypeNames.CLASS) 123 : annotationValueType.boxed(); 124 } 125 126 /** 127 * Returns a code block for {@code binding}'s {@link ContributionBinding#mapKeyAnnotation() map 128 * key}. If for whatever reason the map key is not accessible from within {@code requestingClass} 129 * (i.e. it has a package-private {@code enum} from a different package), this will return an 130 * invocation of a proxy-method giving it access. 131 * 132 * @throws IllegalStateException if {@code binding} is not a {@link dagger.multibindings.IntoMap 133 * map} contribution. 134 */ getMapKeyExpression( ContributionBinding binding, ClassName requestingClass, XProcessingEnv processingEnv)135 public static CodeBlock getMapKeyExpression( 136 ContributionBinding binding, ClassName requestingClass, XProcessingEnv processingEnv) { 137 XAnnotation mapKeyAnnotation = binding.mapKey().get().xprocessing(); 138 return MapKeyAccessibility.isMapKeyAccessibleFrom( 139 mapKeyAnnotation, requestingClass.packageName()) 140 ? directMapKeyExpression(mapKeyAnnotation, processingEnv) 141 : CodeBlock.of("$T.create()", mapKeyProxyClassName(binding)); 142 } 143 144 /** 145 * Returns a code block for the map key annotation {@code mapKey}. 146 * 147 * <p>This method assumes the map key will be accessible in the context that the returned {@link 148 * CodeBlock} is used. Use {@link #getMapKeyExpression(ContributionBinding, ClassName, 149 * XProcessingEnv)} when that assumption is not guaranteed. 150 * 151 * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey} 152 * annotation 153 * @throws IllegalStateException if {@code bindingElement} is not annotated with a {@code MapKey} 154 * annotation 155 */ directMapKeyExpression( XAnnotation mapKey, XProcessingEnv processingEnv)156 private static CodeBlock directMapKeyExpression( 157 XAnnotation mapKey, XProcessingEnv processingEnv) { 158 Optional<XAnnotationValue> unwrappedValue = unwrapValue(mapKey); 159 if (mapKey.getQualifiedName().contentEquals("dagger.android.AndroidInjectionKey")) { 160 XTypeElement unwrappedType = 161 DaggerSuperficialValidation.requireTypeElement( 162 processingEnv, unwrappedValue.get().asString()); 163 return CodeBlock.of( 164 "$T.of($S)", 165 ClassName.get("dagger.android.internal", "AndroidInjectionKeys"), 166 unwrappedType.getClassName().reflectionName()); 167 } 168 AnnotationExpression annotationExpression = new AnnotationExpression(mapKey); 169 return unwrappedValue.isPresent() 170 ? annotationExpression.getValueExpression(unwrappedValue.get()) 171 : annotationExpression.getAnnotationInstanceExpression(); 172 } 173 174 /** 175 * Returns the {@link ClassName} in which {@link #mapKeyFactoryMethod(ContributionBinding, 176 * XProcessingEnv)} is generated. 177 */ mapKeyProxyClassName(ContributionBinding binding)178 public static ClassName mapKeyProxyClassName(ContributionBinding binding) { 179 return elementBasedClassName(asExecutable(binding.bindingElement().get()), "MapKey"); 180 } 181 182 /** 183 * A {@code static create()} method to be added to {@link 184 * #mapKeyProxyClassName(ContributionBinding)} when the {@code @MapKey} annotation is not publicly 185 * accessible. 186 */ mapKeyFactoryMethod( ContributionBinding binding, XProcessingEnv processingEnv)187 public static Optional<MethodSpec> mapKeyFactoryMethod( 188 ContributionBinding binding, XProcessingEnv processingEnv) { 189 return binding 190 .mapKey() 191 .map(DaggerAnnotation::xprocessing) 192 .filter(mapKey -> !isMapKeyPubliclyAccessible(mapKey)) 193 .map( 194 mapKey -> 195 methodBuilder("create") 196 .addModifiers(PUBLIC, STATIC) 197 .returns(mapKeyType(mapKey).getTypeName()) 198 .addStatement("return $L", directMapKeyExpression(mapKey, processingEnv)) 199 .build()); 200 } 201 202 /** 203 * Returns if this binding is a map binding and uses @LazyClassKey for contributing class keys. 204 * 205 * <p>@LazyClassKey won't co-exist with @ClassKey in the graph, since the same binding type cannot 206 * use more than one @MapKey annotation type and Dagger validation will fail. 207 */ useLazyClassKey(Binding binding, BindingGraph graph)208 public static boolean useLazyClassKey(Binding binding, BindingGraph graph) { 209 if (!binding.dependencies().isEmpty()) { 210 ContributionBinding contributionBinding = 211 graph.contributionBinding(binding.dependencies().iterator().next().key()); 212 return contributionBinding.mapKey().isPresent() 213 && contributionBinding 214 .mapKey() 215 .get() 216 .xprocessing() 217 .getClassName() 218 .equals(TypeNames.LAZY_CLASS_KEY); 219 } 220 return false; 221 } 222 MapKeys()223 private MapKeys() {} 224 } 225