FluencyProcessor.java
/*-
* #%L
* io.earcam.instrumental.fluency
* %%
* Copyright (C) 2018 earcam
* %%
* SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
*
* You <b>must</b> choose to accept, in full - any individual or combination of
* the following licenses:
* <ul>
* <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
* <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
* <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
* <li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
* </ul>
* #L%
*/
package io.earcam.instrumental.fluency;
import static java.util.Locale.ROOT;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;
import io.earcam.instrumental.fluent.Fluent;
/**
* <p>
* FluencyProcessor; processes {@link Fluent}ly annotated class members.
* </p>
*
*/
@SupportedOptions({ FluencyProcessor.OPTION_NAME })
public class FluencyProcessor extends AbstractProcessor {
static final String OPTION_NAME = "name";
/*
* TODO
* type parameters,
* package-private/protected scope into same package if not signed,
* support constructors as wrapped as fluid methods
*/
private class FluentMethod {
final ExecutableElement methodElement;
final String comment;
FluentMethod(ExecutableElement methodElement)
{
this.methodElement = methodElement;
this.comment = nabComment();
}
Name methodName()
{
return methodElement.getSimpleName();
}
Name returnTypeName()
{
return typeName(returnType());
}
TypeMirror returnType()
{
return methodElement.getReturnType();
}
private Name typeName(TypeMirror type)
{
return type.getKind().isPrimitive() ? processingEnv.getElementUtils().getName(type.getKind().name().toLowerCase(ROOT))
: ((TypeElement) processingEnv.getTypeUtils().asElement(type)).getQualifiedName();
}
Name ownerTypeSimpleName()
{
return ownerType().getSimpleName();
}
TypeElement ownerType()
{
return (TypeElement) methodElement.getEnclosingElement();
}
PackageElement pkg()
{
return (PackageElement) ownerType().getEnclosingElement();
}
Name packageName()
{
return pkg().getQualifiedName();
}
List<Map.Entry<Name, Name>> parameterNames()
{
return methodElement.getParameters().stream()
.map(p -> new AbstractMap.SimpleEntry<>(p.getSimpleName(), typeName(p.asType())))
.collect(toList());
}
String javadoc()
{
return comment;
}
private String nabComment()
{
String javadoc = processingEnv.getElementUtils().getDocComment(methodElement);
return (javadoc == null) ? "" : makeComment(javadoc);
}
private String makeComment(String javadoc)
{
return "\n\t/**" + javadoc.replace("\n", "\n\t *") + "\n\t*/\n";
}
public Appendable appendTo(Appendable text) throws IOException
{
text.append(javadoc())
.append('\t')
.append("public static final ")
.append(returnTypeName())
.append(' ')
.append(methodName())
.append(parametersToString())
.append("\n\t{\n\t\t");
if(!returnTypeName().contentEquals("void")) {
text.append("return ");
}
return text.append(packageName())
.append('.')
.append(ownerTypeSimpleName())
.append('.')
.append(methodName())
.append(argumentsToString())
.append(";\n\t}\n");
}
private String parametersToString()
{
return parameterNames().stream().map(e -> e.getValue() + " " + e.getKey()).collect(joining(", ", "(", ")"));
}
private String argumentsToString()
{
return parameterNames().stream().map(Map.Entry::getKey).collect(joining(", ", "(", ")"));
}
}
private final List<FluentMethod> methods = new ArrayList<>();
private String name;
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
name = processingEnv.getOptions().getOrDefault(OPTION_NAME, "FluentApi");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
processingEnv.getMessager().printMessage(Kind.NOTE, "starting processing round ...");
// needs to be a one-per package where a package-private member is annotated ...
for(Element element : roundEnv.getElementsAnnotatedWith(Fluent.class)) {
ExecutableElement method = (ExecutableElement) element;
Set<Modifier> modifiers = method.getModifiers();
if(!modifiers.contains(STATIC) || modifiers.contains(PRIVATE)) {
processingEnv.getMessager().printMessage(Kind.WARNING, "Skipping as private or not static, modifiers:" + modifiers, element);
continue;
}
FluentMethod fluentMethod = new FluentMethod(method);
methods.add(fluentMethod);
}
if(roundEnv.processingOver()) {
generateSource();
}
return true;
}
private void generateSource()
{
String paquet = "com.acme";
String fqn = paquet + '.' + name;
JavaFileObject jfo;
try {
jfo = processingEnv.getFiler().createSourceFile(fqn);
try(BufferedWriter bw = new BufferedWriter(jfo.openWriter())) {
bw.append("package ").append(paquet).append(";\n\n");
// TODO date must be optional (as otherwise "different" source each time)
// JAVA9 FAILS due to split package issues with `javax.annotation`
// bw.append('@').append("javax.annotation.Generated")
// .append("(value=\"").append(FluencyProcessor.class.getCanonicalName()).append('"')
// .append(")\n")
bw.append("public final class ").append(name).append(" {\n\n");
for(FluentMethod method : methods) {
method.appendTo(bw);
}
bw.append("\n}\n");
}
} catch(IOException e) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
processingEnv.getMessager().printMessage(Kind.ERROR, writer.toString());
}
}
@Override
public Set<String> getSupportedAnnotationTypes()
{
return Collections.singleton(Fluent.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion()
{
return SourceVersion.latestSupported();
}
}