1 /* 2 * Copyright (C) 2015 Square, 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 package com.squareup.javapoet; 17 18 import java.io.IOException; 19 import java.lang.annotation.Annotation; 20 import java.lang.reflect.Array; 21 import java.lang.reflect.Method; 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.Comparator; 25 import java.util.Iterator; 26 import java.util.LinkedHashMap; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Objects; 30 import javax.lang.model.SourceVersion; 31 import javax.lang.model.element.AnnotationMirror; 32 import javax.lang.model.element.AnnotationValue; 33 import javax.lang.model.element.ExecutableElement; 34 import javax.lang.model.element.TypeElement; 35 import javax.lang.model.element.VariableElement; 36 import javax.lang.model.type.TypeMirror; 37 import javax.lang.model.util.SimpleAnnotationValueVisitor8; 38 39 import static com.squareup.javapoet.Util.characterLiteralWithoutSingleQuotes; 40 import static com.squareup.javapoet.Util.checkArgument; 41 import static com.squareup.javapoet.Util.checkNotNull; 42 43 /** A generated annotation on a declaration. */ 44 public final class AnnotationSpec { 45 public final TypeName type; 46 public final Map<String, List<CodeBlock>> members; 47 AnnotationSpec(Builder builder)48 private AnnotationSpec(Builder builder) { 49 this.type = builder.type; 50 this.members = Util.immutableMultimap(builder.members); 51 } 52 emit(CodeWriter codeWriter, boolean inline)53 void emit(CodeWriter codeWriter, boolean inline) throws IOException { 54 String whitespace = inline ? "" : "\n"; 55 String memberSeparator = inline ? ", " : ",\n"; 56 if (members.isEmpty()) { 57 // @Singleton 58 codeWriter.emit("@$T", type); 59 } else if (members.size() == 1 && members.containsKey("value")) { 60 // @Named("foo") 61 codeWriter.emit("@$T(", type); 62 emitAnnotationValues(codeWriter, whitespace, memberSeparator, members.get("value")); 63 codeWriter.emit(")"); 64 } else { 65 // Inline: 66 // @Column(name = "updated_at", nullable = false) 67 // 68 // Not inline: 69 // @Column( 70 // name = "updated_at", 71 // nullable = false 72 // ) 73 codeWriter.emit("@$T(" + whitespace, type); 74 codeWriter.indent(2); 75 for (Iterator<Map.Entry<String, List<CodeBlock>>> i 76 = members.entrySet().iterator(); i.hasNext(); ) { 77 Map.Entry<String, List<CodeBlock>> entry = i.next(); 78 codeWriter.emit("$L = ", entry.getKey()); 79 emitAnnotationValues(codeWriter, whitespace, memberSeparator, entry.getValue()); 80 if (i.hasNext()) codeWriter.emit(memberSeparator); 81 } 82 codeWriter.unindent(2); 83 codeWriter.emit(whitespace + ")"); 84 } 85 } 86 emitAnnotationValues(CodeWriter codeWriter, String whitespace, String memberSeparator, List<CodeBlock> values)87 private void emitAnnotationValues(CodeWriter codeWriter, String whitespace, 88 String memberSeparator, List<CodeBlock> values) throws IOException { 89 if (values.size() == 1) { 90 codeWriter.indent(2); 91 codeWriter.emit(values.get(0)); 92 codeWriter.unindent(2); 93 return; 94 } 95 96 codeWriter.emit("{" + whitespace); 97 codeWriter.indent(2); 98 boolean first = true; 99 for (CodeBlock codeBlock : values) { 100 if (!first) codeWriter.emit(memberSeparator); 101 codeWriter.emit(codeBlock); 102 first = false; 103 } 104 codeWriter.unindent(2); 105 codeWriter.emit(whitespace + "}"); 106 } 107 get(Annotation annotation)108 public static AnnotationSpec get(Annotation annotation) { 109 return get(annotation, false); 110 } 111 get(Annotation annotation, boolean includeDefaultValues)112 public static AnnotationSpec get(Annotation annotation, boolean includeDefaultValues) { 113 Builder builder = builder(annotation.annotationType()); 114 try { 115 Method[] methods = annotation.annotationType().getDeclaredMethods(); 116 Arrays.sort(methods, Comparator.comparing(Method::getName)); 117 for (Method method : methods) { 118 Object value = method.invoke(annotation); 119 if (!includeDefaultValues) { 120 if (Objects.deepEquals(value, method.getDefaultValue())) { 121 continue; 122 } 123 } 124 if (value.getClass().isArray()) { 125 for (int i = 0; i < Array.getLength(value); i++) { 126 builder.addMemberForValue(method.getName(), Array.get(value, i)); 127 } 128 continue; 129 } 130 if (value instanceof Annotation) { 131 builder.addMember(method.getName(), "$L", get((Annotation) value)); 132 continue; 133 } 134 builder.addMemberForValue(method.getName(), value); 135 } 136 } catch (Exception e) { 137 throw new RuntimeException("Reflecting " + annotation + " failed!", e); 138 } 139 return builder.build(); 140 } 141 get(AnnotationMirror annotation)142 public static AnnotationSpec get(AnnotationMirror annotation) { 143 TypeElement element = (TypeElement) annotation.getAnnotationType().asElement(); 144 AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get(element)); 145 Visitor visitor = new Visitor(builder); 146 for (ExecutableElement executableElement : annotation.getElementValues().keySet()) { 147 String name = executableElement.getSimpleName().toString(); 148 AnnotationValue value = annotation.getElementValues().get(executableElement); 149 value.accept(visitor, name); 150 } 151 return builder.build(); 152 } 153 builder(ClassName type)154 public static Builder builder(ClassName type) { 155 checkNotNull(type, "type == null"); 156 return new Builder(type); 157 } 158 builder(Class<?> type)159 public static Builder builder(Class<?> type) { 160 return builder(ClassName.get(type)); 161 } 162 toBuilder()163 public Builder toBuilder() { 164 Builder builder = new Builder(type); 165 for (Map.Entry<String, List<CodeBlock>> entry : members.entrySet()) { 166 builder.members.put(entry.getKey(), new ArrayList<>(entry.getValue())); 167 } 168 return builder; 169 } 170 equals(Object o)171 @Override public boolean equals(Object o) { 172 if (this == o) return true; 173 if (o == null) return false; 174 if (getClass() != o.getClass()) return false; 175 return toString().equals(o.toString()); 176 } 177 hashCode()178 @Override public int hashCode() { 179 return toString().hashCode(); 180 } 181 toString()182 @Override public String toString() { 183 StringBuilder out = new StringBuilder(); 184 try { 185 CodeWriter codeWriter = new CodeWriter(out); 186 codeWriter.emit("$L", this); 187 return out.toString(); 188 } catch (IOException e) { 189 throw new AssertionError(); 190 } 191 } 192 193 public static final class Builder { 194 private final TypeName type; 195 196 public final Map<String, List<CodeBlock>> members = new LinkedHashMap<>(); 197 Builder(TypeName type)198 private Builder(TypeName type) { 199 this.type = type; 200 } 201 addMember(String name, String format, Object... args)202 public Builder addMember(String name, String format, Object... args) { 203 return addMember(name, CodeBlock.of(format, args)); 204 } 205 addMember(String name, CodeBlock codeBlock)206 public Builder addMember(String name, CodeBlock codeBlock) { 207 List<CodeBlock> values = members.computeIfAbsent(name, k -> new ArrayList<>()); 208 values.add(codeBlock); 209 return this; 210 } 211 212 /** 213 * Delegates to {@link #addMember(String, String, Object...)}, with parameter {@code format} 214 * depending on the given {@code value} object. Falls back to {@code "$L"} literal format if 215 * the class of the given {@code value} object is not supported. 216 */ addMemberForValue(String memberName, Object value)217 Builder addMemberForValue(String memberName, Object value) { 218 checkNotNull(memberName, "memberName == null"); 219 checkNotNull(value, "value == null, constant non-null value expected for %s", memberName); 220 checkArgument(SourceVersion.isName(memberName), "not a valid name: %s", memberName); 221 if (value instanceof Class<?>) { 222 return addMember(memberName, "$T.class", value); 223 } 224 if (value instanceof Enum) { 225 return addMember(memberName, "$T.$L", value.getClass(), ((Enum<?>) value).name()); 226 } 227 if (value instanceof String) { 228 return addMember(memberName, "$S", value); 229 } 230 if (value instanceof Float) { 231 return addMember(memberName, "$Lf", value); 232 } 233 if (value instanceof Character) { 234 return addMember(memberName, "'$L'", characterLiteralWithoutSingleQuotes((char) value)); 235 } 236 return addMember(memberName, "$L", value); 237 } 238 build()239 public AnnotationSpec build() { 240 for (String name : members.keySet()) { 241 checkNotNull(name, "name == null"); 242 checkArgument(SourceVersion.isName(name), "not a valid name: %s", name); 243 } 244 return new AnnotationSpec(this); 245 } 246 } 247 248 /** 249 * Annotation value visitor adding members to the given builder instance. 250 */ 251 private static class Visitor extends SimpleAnnotationValueVisitor8<Builder, String> { 252 final Builder builder; 253 Visitor(Builder builder)254 Visitor(Builder builder) { 255 super(builder); 256 this.builder = builder; 257 } 258 defaultAction(Object o, String name)259 @Override protected Builder defaultAction(Object o, String name) { 260 return builder.addMemberForValue(name, o); 261 } 262 visitAnnotation(AnnotationMirror a, String name)263 @Override public Builder visitAnnotation(AnnotationMirror a, String name) { 264 return builder.addMember(name, "$L", get(a)); 265 } 266 visitEnumConstant(VariableElement c, String name)267 @Override public Builder visitEnumConstant(VariableElement c, String name) { 268 return builder.addMember(name, "$T.$L", c.asType(), c.getSimpleName()); 269 } 270 visitType(TypeMirror t, String name)271 @Override public Builder visitType(TypeMirror t, String name) { 272 return builder.addMember(name, "$T.class", t); 273 } 274 visitArray(List<? extends AnnotationValue> values, String name)275 @Override public Builder visitArray(List<? extends AnnotationValue> values, String name) { 276 for (AnnotationValue value : values) { 277 value.accept(this, name); 278 } 279 return builder; 280 } 281 } 282 } 283