/*
* 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.annotations;
import static com.mastfrog.acteur.annotations.HttpCall.GENERATED_SOURCE_SUFFIX;
import com.mastfrog.acteur.preconditions.InjectUrlParametersAs;
import com.mastfrog.acteur.preconditions.InjectRequestBodyAs;
import com.mastfrog.giulius.annotations.processors.IndexGeneratingProcessor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.annotation.Annotation;
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.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.openide.util.lookup.ServiceProvider;
/**
* Processes the @Defaults annotation, generating properties files in the
* location specified by the annotation (the default is
* com/mastfrog/defaults.properties).
* <p/>
* Keep this in a separate package so it can be detached from this JAR
*
* @author Tim Boudreau
*/
@ServiceProvider(service = Processor.class)
@SupportedAnnotationTypes({"com.mastfrog.acteur.annotations.HttpCall",
"com.mastfrog.acteur.preconditions.InjectRequestBodyAs",
"com.mastfrog.acteur.preconditions.InjectUrlParametersAs"
})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class HttpCallAnnotationProcessor extends IndexGeneratingProcessor {
public HttpCallAnnotationProcessor() {
super(true);
}
private boolean isPageSubtype(Element e) {
Types types = processingEnv.getTypeUtils();
Elements elements = processingEnv.getElementUtils();
TypeElement pageType = elements.getTypeElement("com.mastfrog.acteur.Page");
if (pageType == null) {
return false;
}
return types.isSubtype(e.asType(), pageType.asType());
}
private boolean isActeurSubtype(Element e) {
Types types = processingEnv.getTypeUtils();
Elements elements = processingEnv.getElementUtils();
TypeElement pageType = elements.getTypeElement("com.mastfrog.acteur.Acteur");
if (pageType == null) {
return false;
}
return types.isSubtype(e.asType(), pageType.asType());
}
private AnnotationMirror findMirror(Element el, Class<? extends Annotation> annoType) {
for (AnnotationMirror mir : el.getAnnotationMirrors()) {
TypeMirror type = mir.getAnnotationType().asElement().asType();
if (annoType.getName().equals(type.toString())) {
return mir;
}
}
return null;
}
private String canonicalize(TypeMirror tm, Types types) {
TypeElement e = (TypeElement) types.asElement(tm);
StringBuilder nm = new StringBuilder(e.getQualifiedName().toString());
Element enc = e.getEnclosingElement();
while (enc != null && enc.getKind() != ElementKind.PACKAGE) {
int ix = nm.lastIndexOf(".");
if (ix > 0) {
nm.setCharAt(ix, '$');
}
enc = enc.getEnclosingElement();
}
return nm.toString();
}
private static String types(Object o) { //debug stuff
List<String> s = new ArrayList<>();
Class<?> x = o.getClass();
while (x != Object.class) {
s.add(x.getName());
for (Class<?> c : x.getInterfaces()) {
s.add(c.getName());
}
x = x.getSuperclass();
}
StringBuilder sb = new StringBuilder();
for (String ss : s) {
sb.append(ss);
sb.append(", ");
}
return sb.toString();
}
private List<String> typeList(AnnotationMirror mirror, String param) {
List<String> result = new ArrayList<>();
if (mirror != null) {
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> x : mirror.getElementValues().entrySet()) {
String annoParam = x.getKey().getSimpleName().toString();
if (param.equals(annoParam)) {
if (x.getValue().getValue() instanceof List) {
List<?> list = (List<?>) x.getValue().getValue();
for (Object o : list) {
if (o instanceof AnnotationValue) {
AnnotationValue av = (AnnotationValue) o;
if (av.getValue() instanceof DeclaredType) {
DeclaredType dt = (DeclaredType) av.getValue();
// Convert e.g. mypackage.Foo.Bar.Baz to mypackage.Foo$Bar$Baz
String canonical = canonicalize(dt.asElement().asType(), processingEnv.getTypeUtils());
result.add(canonical);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"Not a declared type: " + av);
}
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"Annotation value for scopeTypes is not an AnnotationValue " + types(o));
}
}
} else if (x.getValue().getValue() instanceof DeclaredType) {
DeclaredType dt = (DeclaredType) x.getValue().getValue();
// Convert e.g. mypackage.Foo.Bar.Baz to mypackage.Foo$Bar$Baz
String canonical = canonicalize(dt.asElement().asType(), processingEnv.getTypeUtils());
result.add(canonical);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
"Annotation value for scopeTypes is not a list on " + mirror + " - " + types(x.getValue().getValue()));
}
}
}
}
return result;
}
private List<String> bindingTypes(Element el) {
AnnotationMirror mirror = findMirror(el, HttpCall.class);
List<String> result = typeList(mirror, "scopeTypes");
mirror = findMirror(el, InjectUrlParametersAs.class);
result.addAll(typeList(mirror, "value"));
mirror = findMirror(el, InjectRequestBodyAs.class);
result.addAll(typeList(mirror, "value"));
return result;
}
Set<Element> elements = new HashSet<>();
int ix;
private List<String> deferred = new LinkedList<String>();
@Override
public boolean handleProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<Element> all = new HashSet<>(roundEnv.getElementsAnnotatedWith(HttpCall.class));
all.addAll(roundEnv.getElementsAnnotatedWith(InjectUrlParametersAs.class));
all.addAll(roundEnv.getElementsAnnotatedWith(InjectRequestBodyAs.class));
List<String> 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) {
Annotation anno = e.getAnnotation(HttpCall.class);
int order = 0;
if (anno instanceof HttpCall) {
order = ((HttpCall) anno).order();
}
if (anno == null) {
continue;
}
boolean acteur = isActeurSubtype(e);
if (!isPageSubtype(e) && !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);
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();
lines.append(canonicalize(e.asType(), processingEnv.getTypeUtils())).append(":").append(order);
List<String> bindingTypes = bindingTypes(e);
if (!bindingTypes.isEmpty()) {
lines.append('{');
for (Iterator<String> it = bindingTypes.iterator(); it.hasNext();) {
lines.append(it.next());
if (it.hasNext()) {
lines.append(',');
} else {
lines.append('}');
}
}
}
addLine(HttpCall.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();
}
@Override
public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
return Collections.<Completion>emptySet();
}
private PackageElement findPackage(Element el) {
while (el != null && !(el instanceof PackageElement)) {
el = el.getEnclosingElement();
}
return (PackageElement) el;
}
@SuppressWarnings("unchecked")
private String generatePageSource(TypeElement typeElement, AtomicBoolean error) throws IOException {
PackageElement pkg = findPackage(typeElement);
String className = typeElement.getSimpleName() + GENERATED_SOURCE_SUFFIX;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (PrintStream ps = new PrintStream(out)) {
ps.println("package " + pkg.getQualifiedName() + ";");
TypeElement outer = typeElement;
while (!outer.getEnclosingElement().equals(pkg)) {
outer = (TypeElement) outer.getEnclosingElement();
}
ps.println("\nimport com.mastfrog.acteur.Page;");
for (AnnotationMirror am : typeElement.getAnnotationMirrors()) {
ps.println("import " + am.getAnnotationType() + ";");
}
ps.println();
List<String> precursorClassNames = new ArrayList<>();
List<String> denoumentClassNames = new ArrayList<>();
ams:
for (AnnotationMirror am : typeElement.getAnnotationMirrors()) {
if (am.getAnnotationType().toString().equals(Precursors.class.getName())) {
if (!am.getElementValues().entrySet().isEmpty()) {
for (String s : typeList(am, "value")) {
precursorClassNames.add(s.replace('$', '.'));
}
continue;
}
}
if (am.getAnnotationType().toString().equals(Concluders.class.getName())) {
if (!am.getElementValues().entrySet().isEmpty()) {
for (String s : typeList(am, "value")) {
denoumentClassNames.add(s.replace('$', '.'));
}
continue;
}
}
ps.print("@" + am.getAnnotationType());
boolean first = true;
Iterator<?> it = am.getElementValues().entrySet().iterator();
if (it.hasNext()) {
while (it.hasNext()) {
Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> el = (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue>) it.next();
if (first) {
ps.print('(');
first = false;
} else {
ps.print(',');
}
String key = "" + el.getKey();
ps.print(key.substring(0, key.length() - 2));
ps.print('=');
if ("<error>".equals(el.getValue().getValue())) {
error.set(true);
break ams;
} else {
ps.print(el.getValue());
}
if (!it.hasNext()) {
ps.print(")\n");
}
}
} else {
ps.print('\n');
}
}
ps.println("\npublic final class " + className + " extends Page {\n");
ps.println(" " + className + "(){");
for (String p : precursorClassNames) {
ps.println(" add(" + p + ".class); //precursor");
}
ps.println(" add(" + typeElement.getQualifiedName() + ".class);");
for (String p : denoumentClassNames) {
ps.println(" add(" + p + ".class); //denoument");
}
ps.println(" }");
ps.println("}");
}
if (!error.get()) {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(pkg.getQualifiedName() + "." + className, typeElement);
try (OutputStream stream = jfo.openOutputStream()) {
stream.write(out.toByteArray());
}
}
return pkg.getQualifiedName() + "." + className;
}
}