com.mastfrog.acteur.annotation.processors.HttpCallAnnotationProcessor Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.mastfrog.acteur.annotation.processors;
import static com.mastfrog.acteur.annotation.processors.HttpCallAnnotationProcessor.EARLY_ANNOTATION;
import static com.mastfrog.acteur.annotation.processors.HttpCallAnnotationProcessor.HTTP_CALL_ANNOTATION;
import static com.mastfrog.acteur.annotation.processors.HttpCallAnnotationProcessor.INJECT_URL_PARAMS_AS_ANNOTATION;
import com.mastfrog.annotation.registries.AnnotationIndexFactory;
import com.mastfrog.annotation.registries.IndexGeneratingProcessor;
import com.mastfrog.annotation.registries.Line;
import com.mastfrog.util.service.ServiceProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.Completion;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
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.tools.Diagnostic;
import javax.tools.JavaFileObject;
import static com.mastfrog.acteur.annotation.processors.HttpCallAnnotationProcessor.INJECT_BODY_AS_ANNOTATION;
import com.mastfrog.annotation.AnnotationUtils;
import static com.mastfrog.annotation.AnnotationUtils.simpleName;
import static com.mastfrog.annotation.AnnotationUtils.types;
import com.mastfrog.function.TriConsumer;
import com.mastfrog.java.vogon.ClassBuilder;
import com.mastfrog.java.vogon.LinesBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.function.Consumer;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/**
* Processes the @Defaults annotation, generating properties files in the
* location specified by the annotation (the default is
* com/mastfrog/defaults.properties).
*
* Keep this in a separate package so it can be detached from this JAR
*
* @author Tim Boudreau
*/
@ServiceProvider(Processor.class)
@SupportedAnnotationTypes({HTTP_CALL_ANNOTATION,
EARLY_ANNOTATION,
INJECT_BODY_AS_ANNOTATION,
INJECT_URL_PARAMS_AS_ANNOTATION
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class HttpCallAnnotationProcessor extends IndexGeneratingProcessor {
public static final String GENERATED_SOURCE_SUFFIX = "__GenPage";
public static final String META_INF_PATH = "META-INF/http/pages.list";
public static final String HTTP_CALL_ANNOTATION = "com.mastfrog.acteur.annotations.HttpCall";
public static final String EARLY_ANNOTATION = "com.mastfrog.acteur.annotations.Early";
public static final String INJECT_BODY_AS_ANNOTATION = "com.mastfrog.acteur.preconditions.InjectRequestBodyAs";
public static final String INJECT_URL_PARAMS_AS_ANNOTATION = "com.mastfrog.acteur.preconditions.InjectUrlParametersAs";
private static final String PRECURSORS_ANNOTATION = "com.mastfrog.acteur.annotations.Precursors";
private static final String CONCLUDERS_ANNOTATION = "com.mastfrog.acteur.annotations.Concluders";
private static final String INSTALL_CHUNK_HANDLER_ACTEUR = "com.mastfrog.acteur.annotations.InstallChunkHandler";
private static final String GENERATED_FROM_ANNOTATION = "com.mastfrog.acteur.annotations.GeneratedFrom";
private static final String ACTEUR_FQN = "com.mastfrog.acteur.Acteur";
private static final String PAGE_FQN = "com.mastfrog.acteur.Page";
public HttpCallAnnotationProcessor() {
super(true, AnnotationIndexFactory.lines());
}
private boolean isPageSubtype(Element e, AnnotationUtils utils) {
return utils.isSubtypeOf(e, PAGE_FQN).isSubtype();
}
private boolean isActeurSubtype(Element e, AnnotationUtils utils) {
return utils.isSubtypeOf(e, ACTEUR_FQN).isSubtype();
}
private List bindingTypes(Element el, AnnotationUtils utils) {
AnnotationMirror mirror = utils.findMirror(el, HTTP_CALL_ANNOTATION);
List result = new ArrayList(15);
if (mirror != null) {
result.addAll(utils.typeList(mirror, "scopeTypes"));
}
mirror = utils.findMirror(el, INJECT_URL_PARAMS_AS_ANNOTATION);
if (mirror != null) {
result.addAll(utils.typeList(mirror, "value"));
}
mirror = utils.findMirror(el, INJECT_BODY_AS_ANNOTATION);
if (mirror != null) {
result.addAll(utils.typeList(mirror, "value"));
}
return result;
}
Set elements = new HashSet<>();
int ix;
private List deferred = new LinkedList<>();
private void sanityCheckNonHttpCallElement(Element el, AnnotationUtils utils) {
AnnotationMirror inj = utils.findAnnotationMirror(el, INJECT_BODY_AS_ANNOTATION);
if (inj != null) {
utils.warn("@InjectRequestBodyAs annotation not applicable to classes not annotated with @HttpCall", el, inj);
}
AnnotationMirror inj2 = utils.findAnnotationMirror(el, INJECT_URL_PARAMS_AS_ANNOTATION);
if (inj2 != null) {
utils.warn("@InjectUrlParametersAs annotation not applicable to classes not annotated with @HttpCall", el, inj2);
}
AnnotationMirror early = utils.findAnnotationMirror(el, EARLY_ANNOTATION);
if (early != null) {
utils.log("@Early annotation not applicable to classes not annotated with @HttpCall", el, early);
}
AnnotationMirror pre = utils.findAnnotationMirror(el, PRECURSORS_ANNOTATION);
if (pre != null) {
utils.warn("@Precursors annotation not applicable to classes not annotated with @HttpCall", el, pre);
}
AnnotationMirror conc = utils.findAnnotationMirror(el, CONCLUDERS_ANNOTATION);
if (conc != null) {
utils.warn("@Precursors annotation not applicable to classes not annotated with @HttpCall", el, conc);
}
}
@Override
public boolean handleProcess(Set extends TypeElement> annotations, RoundEnvironment roundEnv, AnnotationUtils utils) {
// Set all = new HashSet<>(roundEnv.getElementsAnnotatedWith(HttpCall.class));
// all.addAll(roundEnv.getElementsAnnotatedWith(InjectUrlParametersAs.class));
// all.addAll(roundEnv.getElementsAnnotatedWith(InjectRequestBodyAs.class));
// all.addAll(roundEnv.getElementsAnnotatedWith(Early.class));
Set all = utils.findAnnotatedElements(roundEnv);
List failed = new LinkedList<>();
// Add in any types that could not be generated on a previous round because
// they relied on a generated time (i.e. @InjectRequestBodyAs can't be copied
// correctly into a generated page subclass if the type of its value will be
// generated by Numble
for (String type : deferred) {
TypeElement retry = processingEnv.getElementUtils().getTypeElement(type);
all.add(retry);
}
deferred.clear();
try {
for (Element e : all) {
AnnotationMirror am = utils.findAnnotationMirror(e, HTTP_CALL_ANNOTATION);
if (am == null) {
sanityCheckNonHttpCallElement(e, utils);
continue;
}
Integer order = utils.annotationValue(am, "order", Integer.class, 0);
boolean acteur = isActeurSubtype(e, utils);
if (!isPageSubtype(e, utils) && !acteur) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Not a subclass of Page or Acteur: " + e.asType(), e);
continue;
}
elements.add(e);
if (acteur) {
TypeElement te = (TypeElement) e;
// Generating a page source may fail if InjectRequetsBodyAs or
// similar may inject a class that has not been generated yet.
// So, if it failed, make note of it, don't write the file out
// and we'll solve it on a subsequent round
AtomicBoolean err = new AtomicBoolean();
String className = generatePageSource(te, err, utils);
if (!err.get()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Generated " + className + " for " + e.asType(), e);
} else {
failed.add(te.getQualifiedName().toString());
}
} else {
StringBuilder lines = new StringBuilder();
String canonicalName = utils.canonicalize(e.asType());
lines.append(canonicalName).append(":").append(order);
List bindingTypes = bindingTypes(e, utils);
if (!bindingTypes.isEmpty()) {
lines.append('{');
for (Iterator it = bindingTypes.iterator(); it.hasNext();) {
lines.append(it.next());
if (it.hasNext()) {
lines.append(',');
} else {
lines.append('}');
}
}
}
addLine(META_INF_PATH, lines.toString(), e);
}
}
} catch (IOException ex) {
Logger.getLogger(HttpCallAnnotationProcessor.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
deferred.addAll(failed);
return failed.isEmpty();
}
private int lineCount;
protected boolean addLine(String path, String line, Element... el) {
Line l = new Line(lineCount++, el, line);
return addIndexElement(path, l, el);
}
@Override
public Iterable extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
return Collections.emptySet();
}
private PackageElement findPackage(Element el) {
while (el != null && !(el instanceof PackageElement)) {
el = el.getEnclosingElement();
}
return (PackageElement) el;
}
private String generatePageSource(TypeElement typeElement, AtomicBoolean error, AnnotationUtils utils) throws IOException {
boolean hasNumble = utils.type("com.mastfrog.parameters.Types") != null;
PackageElement pkg = findPackage(typeElement);
ClassBuilder cb = ClassBuilder.forPackage(pkg.getQualifiedName())
.named(typeElement.getSimpleName() + GENERATED_SOURCE_SUFFIX)
.extending(simpleName(PAGE_FQN))
.importing(PAGE_FQN, ACTEUR_FQN,
"static com.mastfrog.acteur.headers.Method.*"
)
.conditionally(hasNumble, b -> {
b.importing("static com.mastfrog.parameters.Types.*",
"static org.netbeans.validation.api.builtin.stringvalidation.StringValidators.*");
})
.docComment("Generated from annotations on ", typeElement.getSimpleName(),
" using Java Vogon", "http://github.com/timboudreau/annotation-utils")
.annotatedWith(GENERATED_FROM_ANNOTATION, ab -> {
ab.addClassArgument("value", typeElement.asType().toString());
});
List precursorClassNames = new ArrayList<>();
List denoumentClassNames = new ArrayList<>();
List argDebugComments = new ArrayList<>();
argDebugComments.add("JDK 15 Annotation Argument Debug Info");
typeElement.getAnnotationMirrors().forEach(am -> {
cb.importing(am.getAnnotationType().toString());
if (am.getAnnotationType().toString().equals(PRECURSORS_ANNOTATION)) {
if (!am.getElementValues().entrySet().isEmpty()) {
for (String s : utils.typeList(am, "value")) {
String valueClass = s.replace('$', '.');
precursorClassNames.add(valueClass);
}
}
return;
}
if (am.getAnnotationType().toString().equals(EARLY_ANNOTATION)) {
denoumentClassNames.add(0, INSTALL_CHUNK_HANDLER_ACTEUR);
return;
}
if (am.getAnnotationType().toString().equals(CONCLUDERS_ANNOTATION)) {
if (!am.getElementValues().entrySet().isEmpty()) {
for (String s : utils.typeList(am, "value")) {
denoumentClassNames.add(s.replace('$', '.'));
}
return;
}
}
cb.annotatedWith(am.getAnnotationType().toString(), ab -> {
AnnotationArgumentsInfo args = arguments(am, error, utils);
argDebugComments.add(am.getAnnotationType().toString() + ": ");
copyAnnotation(args, argDebugComments, ab, utils, error);
});
});
cb.constructor().setModifier(Modifier.PUBLIC).body(bb -> {
// For now,
argDebugComments.forEach(bb::lineComment);
if (!precursorClassNames.isEmpty()) {
bb.lineComment("precursors");
for (String p : precursorClassNames) {
bb.invoke("add").withClassArgument(p).inScope();
}
}
bb.lineComment("generator");
bb.invoke("add").withClassArgument(typeElement.getQualifiedName().toString()).inScope();
if (!denoumentClassNames.isEmpty()) {
bb.lineComment("concluders");
for (String p : denoumentClassNames) {
bb.invoke("add").withClassArgument(p).inScope();
}
}
});
if (!error.get()) {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(cb.fqn(), typeElement);
try (OutputStream stream = jfo.openOutputStream()) {
stream.write(cb.build().getBytes(UTF_8));
}
}
return cb.fqn();
}
private void copyAnnotation(AnnotationArgumentsInfo args, List argDebugComments, ClassBuilder.AnnotationBuilder> ab, AnnotationUtils utils, AtomicBoolean error) {
args.forEach((name, element, val) -> {
copyOneArgument(argDebugComments, name, val, ab, element, error, utils);
});
}
private void copyOneArgument(List argDebugComments, String name, Object val, ClassBuilder.AnnotationBuilder> ab, ExecutableElement element, AtomicBoolean error, AnnotationUtils utils) {
argDebugComments.add(" * " + name + " = " + val + " is " + (val == null ? "null" : val.getClass().getName()));
if (val instanceof List>) {
ab.addArrayArgument(name, avb -> {
List> l = (List>) val;
for (Object o : l) {
addAnnotationArgument(argDebugComments, o, element, avb::expression);
}
});
} else if (val instanceof AnnotationMirror) {
AnnotationMirror sub = (AnnotationMirror) val;
AnnotationArgumentsInfo subArgs = arguments(sub, error, utils);
ab.addAnnotationArgument(name, sub.getAnnotationType().toString(), abSub -> {
copyAnnotation(subArgs, argDebugComments, abSub, utils, error);
});
} else if (val instanceof TypeMirror) {
ab.addClassArgument(name, val.toString());
} else if (val instanceof String) {
ab.addArgument(name, (String) val);
} else if (val instanceof Character) {
char c = (Character) val;
switch (c) {
case '\\':
ab.addExpressionArgument(name, "'\\\\'");
break;
case '\'':
ab.addExpressionArgument(name, "'\\''");
break;
default:
ab.addExpressionArgument(name, "'" + c + "'s");
}
} else {
addAnnotationArgument(argDebugComments, val, element, v -> {
ab.addExpressionArgument(name, v);
});
}
}
private void addAnnotationArgument(List argDebugComments, Object o, ExecutableElement element, Consumer avb) {
argDebugComments.add(" ** " + o.getClass().getName());
argDebugComments.add(" **** " + types(o));
argDebugComments.add(" --------- " + o + " ---------");
if (o instanceof AnnotationValue) {
AnnotationValue av = (AnnotationValue) o;
TypeMirror retType = element.getReturnType();
argDebugComments.add("RET TYPE: " + retType);
argDebugComments.add(" RET TYPE KIND " + retType.getKind());
// argDebugComments.add(" RTT " + processingEnv.getElementUtils().getAllTypeElements(retType.toString()));
argDebugComments.add(" RET TYPE types " + types(retType));
boolean isClass = false;
String enumType = null;
if (retType instanceof ArrayType) {
ArrayType at = (ArrayType) retType;
at.getComponentType();
argDebugComments.add(" COMPONENT TYPE " + at.getComponentType());
argDebugComments.add(" COMP TYPE kind " + at.getComponentType().getKind() + " types " + types(at.getComponentType()));
DeclaredType dt = (DeclaredType) at.getComponentType();
Element el = dt.asElement();
argDebugComments.add(" ELE KIND " + el.getKind() + " types " + types(el));
switch (el.getKind()) {
case ENUM:
enumType = dt.toString();
break;
}
} else if (retType instanceof DeclaredType) {
DeclaredType dt = (DeclaredType) retType;
Element el = dt.asElement();
switch (el.getKind()) {
case ENUM:
enumType = dt.toString();
break;
}
}
if (enumType != null) {
// In JDK 15, enum elements do not contain the type as a prefix;
// in earlier versions they do, and getting this wrong will result
// in uncompilable code
if (av.toString().indexOf('.') < 0) {
avb.accept(enumType + "." + av.toString());
} else {
avb.accept(av.toString());
}
} else {
if (isClass) {
avb.accept(av.toString() + ".class");
} else {
avb.accept(av.toString());
}
}
} else {
argDebugComments.add(" * OTYPES " + types(o));
if (o instanceof TypeMirror) {
avb.accept(o + ".class");
} else if (o instanceof String) {
avb.accept(escapeAndQuoteString((String) o));
} else if (o instanceof Character) {
char c = (Character) o;
switch (c) {
case '\\':
avb.accept("'\\\\'");
break;
case '\'':
avb.accept("'\\''");
break;
default:
avb.accept("'" + c + "'s");
}
} else {
avb.accept(Objects.toString(o));
}
}
}
private String escapeAndQuoteString(String s) {
return LinesBuilder.escape(s);
}
private AnnotationArgumentsInfo arguments(AnnotationMirror am, AtomicBoolean error, AnnotationUtils utils) {
AnnotationArgumentsInfo result = new AnnotationArgumentsInfo();
am.getElementValues().forEach((ExecutableElement ee, AnnotationValue av) -> {
if ("".equals(av.getValue() + "")) {
error.set(true);
utils.warn("Erroneous type " + av.getValue() + " for "
+ ee.getSimpleName() + " in " + am
+ ". If this is to be generated in a subsequent round of annotation processing, this may be a non-problem.");
return;
}
Object val = av.getValue();
result.add(ee.getSimpleName().toString(), ee, val);
});
return result;
}
private static final class AnnotationArgumentsInfo {
private final Map elements = new HashMap<>(8);
private final Map arguments = new LinkedHashMap<>(8);
void add(String name, ExecutableElement ex, Object arg) {
elements.put(name, ex);
arguments.put(name, arg);
}
void forEach(TriConsumer argConsumer) {
arguments.forEach((name, arg) -> {
ExecutableElement el = elements.get(name);
assert el != null;
argConsumer.accept(name, el, arg);
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy