/* * Copyright (C) 2015 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.callbuilder; import static javax.lang.model.element.Modifier.STATIC; import com.google.callbuilder.util.Preconditions; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.tools.JavaFileObject; public class CallBuilderProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new HashSet<>(); types.add(CallBuilder.class.getName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { boolean claimed = (annotations.size() == 1 && annotations.iterator().next().getQualifiedName().toString().equals( CallBuilder.class.getName())); if (claimed) { process(roundEnv); return true; } else { return false; } } static String capitalizeFirst(String s) { return s.substring(0, 1).toUpperCase() + s.substring(1); } List<String> simpleNames(Iterable<? extends Element> elements) { List<String> names = new ArrayList<>(); for (Element el : elements) { names.add(el.getSimpleName().toString()); } return names; } private static String lines(String... separated) { StringBuilder joined = new StringBuilder(); for (String line : separated) { joined.append(line); joined.append('\n'); } return joined.toString(); } private static void writef(Writer writer, String format, Object... args) throws IOException { writer.write(String.format(format, (Object[]) args)); } private List<ExecutableElement> callbuilderElements(RoundEnvironment roundEnv) { Collection<? extends Element> unfiltered = roundEnv.getElementsAnnotatedWith(CallBuilder.class); List<ExecutableElement> elements = new ArrayList<>(); elements.addAll(ElementFilter.constructorsIn(unfiltered)); elements.addAll(ElementFilter.methodsIn(unfiltered)); return elements; } /** * Information about the context of the builder. This only applies to non-static, non-constructor * methods for which a builder is being generated. If an @{@link CallBuilder}-annotated method is * not static, then the generated builder must choose on which object to call the method. This is * passed as a constructor argument to the builder. */ private static final class Context { private final TypeMirror type; private final String builderFieldName; Context(TypeMirror type, String builderFieldName) { this.type = Preconditions.checkNotNull(type); this.builderFieldName = Preconditions.checkNotNull(builderFieldName); } public TypeMirror getType() { return type; } public String getBuilderFieldName() { return builderFieldName; } } private static StringBuilder joinOn( StringBuilder builder, String delimiter, Iterable<?> elements) { int added = 0; for (Object element : elements) { if (added++ > 0) { builder.append(delimiter); } builder.append(element.toString()); } return builder; } private static final class TypeParameters { private final List<TypeParameterElement> classParameters; private final List<TypeParameterElement> methodParameters; private final @Nullable Context context; private final boolean isConstructor; TypeParameters(List<? extends TypeParameterElement> classParameters, List<? extends TypeParameterElement> methodParameters, @Nullable Context context, boolean isConstructor) { this.classParameters = Collections.unmodifiableList(new ArrayList<>(classParameters)); this.methodParameters = Collections.unmodifiableList(new ArrayList<>(methodParameters)); this.context = context; this.isConstructor = isConstructor; } private List<TypeParameterElement> allParameters() { List<TypeParameterElement> allParameters = new ArrayList<>(); if ((context != null) || isConstructor) { allParameters.addAll(classParameters); } allParameters.addAll(methodParameters); return allParameters; } /** * The type parameters to place on the builder, without the "extends ..." bounds. */ String alligator() { List<TypeParameterElement> allParameters = allParameters(); if (allParameters.isEmpty()) { return ""; } StringBuilder alligator = new StringBuilder("<"); joinOn(alligator, ", ", allParameters); return alligator.append(">").toString(); } /** * The type parameters to place on the builder, with the "extends ..." bounds. */ String alligatorWithBounds() { List<TypeParameterElement> allParameters = allParameters(); if (allParameters.isEmpty()) { return ""; } StringBuilder alligator = new StringBuilder("<"); String separator = ""; for (TypeParameterElement param : allParameters) { alligator.append(separator); separator = ", "; alligator.append(param.toString()); for (TypeMirror bound : param.getBounds()) { alligator.append(" extends ").append(bound); } } return alligator.append(">").toString(); } } private void process(RoundEnvironment roundEnv) { Elements elementUtils = processingEnv.getElementUtils(); for (ExecutableElement el : callbuilderElements(roundEnv)) { boolean isConstructor = el.getSimpleName().toString().equals("<init>"); TypeElement enclosingType = (TypeElement) el.getEnclosingElement(); UniqueSymbols uniqueSymbols = new UniqueSymbols.Builder() .addAllUserDefined(simpleNames(el.getParameters())) .build(); Context context = null; if (!isConstructor && !el.getModifiers().contains(STATIC)) { context = new Context(enclosingType.asType(), uniqueSymbols.get("")); } TypeParameters typeParameters = new TypeParameters( enclosingType.getTypeParameters(), el.getTypeParameters(), context, isConstructor); CallBuilder ann = el.getAnnotation(CallBuilder.class); String className; if (!ann.className().isEmpty()) { className = ann.className(); } else if (isConstructor) { className = enclosingType.getSimpleName() + "Builder"; } else { className = capitalizeFirst(el.getSimpleName().toString()) + "Builder"; } String packageName = packageNameOf(el); String generatedCanonicalName = packageName.isEmpty() ? className : (packageName + "." + className); try { JavaFileObject file = processingEnv.getFiler().createSourceFile(generatedCanonicalName, el); try (Writer wrt = file.openWriter()) { if (!packageName.isEmpty()) { writef(wrt, lines( "package %s;", ""), packageName); } writef(wrt, lines( "public final class %s%s {"), className, typeParameters.alligatorWithBounds()); if (context != null) { String constructorParameterName = ann.contextName(); writef(wrt, lines( " private final %s %s;", " public %s(%s %s) {", " %s = %s;", " }"), context.getType(), context.getBuilderFieldName(), className, context.getType(), constructorParameterName, context.getBuilderFieldName(), constructorParameterName); } List<FieldInfo> fields = FieldInfo.fromAll(elementUtils, el.getParameters()); for (FieldInfo field : fields) { if (field.style() != null) { FieldStyle fieldStyle = field.style(); TypeInference inference = TypeInference.forField(fieldStyle, field.parameter()); if (inference != null) { writef(wrt, lines(" private %s %s = %s.start();"), inference.builderFieldType(), field.name(), qualifiedName(fieldStyle.styleClass())); for (ExecutableElement modifier : fieldStyle.modifiers()) { List<? extends VariableElement> parameters = modifier.getParameters(); List<String> nonFieldParameterNames = simpleNames(parameters.subList(1, parameters.size())); List<String> nonFieldParameterTypes = inference.modifierParameterTypes(modifier); if (nonFieldParameterTypes != null) { writef(wrt, lines( " public %s%s %s%s(%s) {", " this.%s = %s.%s(this.%s, %s);", " return this;", " }"), className, typeParameters.alligator(), modifier.getSimpleName(), capitalizeFirst(field.name()), parameterList(nonFieldParameterTypes, nonFieldParameterNames), field.name(), qualifiedName(fieldStyle.styleClass()), modifier.getSimpleName(), field.name(), joinOn(new StringBuilder(), ", ", nonFieldParameterNames)); } // TODO: report warning if could not inference parameter types for some modifier. // TODO: support generic type parameters on the *generated* modifier } } // TODO: report error if TypeInference could not be obtained. } else { writef(wrt, lines( " private %s %s;", " public %s%s set%s(%s %s) {", " this.%s = %s;", " return this;", " }"), field.finishType(), field.name(), className, typeParameters.alligator(), capitalizeFirst(field.name()), field.finishType(), field.name(), field.name(), field.name()); } } TypeMirror generatedMethodReturn; if (isConstructor) { generatedMethodReturn = enclosingType.asType(); } else { generatedMethodReturn = el.getReturnType(); } // invocation: the return expression of the generated method, minus the argument list. String invocation; if (isConstructor) { invocation = String.format("new %s%s", enclosingType.getQualifiedName(), typeParameters.alligator()); } else { invocation = String.format("%s.%s", (context != null) ? context.getBuilderFieldName() : enclosingType.getQualifiedName(), el.getSimpleName()); } writef(wrt, lines( " %s %s() {", " %s%s(%s);", " }", "}"), generatedMethodReturn, ann.methodName(), (generatedMethodReturn.getKind() == TypeKind.VOID) ? "" : "return ", invocation, finishInvocations(fields)); } } catch (IOException e) { throw new RuntimeException(e); } } } /** * Returns the name of the package that the given element is in. If the element is in the default * (unnamed) package then the name is the empty string. Taken from AutoValue source code. */ static String packageNameOf(Element el) { while (true) { Element enclosing = el.getEnclosingElement(); if (enclosing instanceof PackageElement) { return ((PackageElement) enclosing).getQualifiedName().toString(); } el = (Element) enclosing; } } private static String parameterList(List<String> parameterTypes, List<String> parameterNames) { StringBuilder list = new StringBuilder(); for (int i = 0; i < parameterTypes.size(); i++) { if (i != 0) { list.append(", "); } list.append(parameterTypes.get(i)).append(" ").append(parameterNames.get(i)); } return list.toString(); } static String qualifiedName(DeclaredType type) { return ((TypeElement) type.asElement()).getQualifiedName().toString(); } private static String finishInvocations(Iterable<FieldInfo> fields) { StringBuilder invocations = new StringBuilder(); for (FieldInfo field : fields) { if (invocations.length() != 0) { invocations.append(", "); } if (field.style() != null) { invocations.append(String.format("%s.finish(%s)", qualifiedName(field.style().styleClass()), field.name())); } else { invocations.append(field.name()); } } return invocations.toString(); } }