Reputation: 423
I've written an annotation processor that simply dumps the list of annotated classes to a text file. To accommodate incremental builds in Eclipse, it also attempts to re-read the file and checks if the classes still have the annotation, and if not, it removes the class from the list.
The problem is Eclipse doesn't trigger the annotation processor when you remove the annotation, so the text file will not be updated until you add the annotation to another class.
Is there a way to configure Eclipse to run the annotation processor when an annotation is removed? The project is using Gradle/Buildship.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
try {
generateAnnotationFiles(annotations, roundEnv);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
return false;
}
private void generateAnnotationFiles(
Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws IOException {
for (var i : annotations) {
var elements = roundEnv.getElementsAnnotatedWith(i);
var fileName = "META-INF/annotations/" + i.getQualifiedName();
var classBuilder = new HashSet<>(convertElementsToList(elements));
classBuilder.addAll(getPreviousListedClasses(fileName, i));
var classes = new ArrayList<>(classBuilder);
Collections.sort(classes);
try (var out =
new PrintWriter(
processingEnv
.getFiler()
.createResource(StandardLocation.SOURCE_OUTPUT, "", fileName)
.openWriter())) {
for (var clazz : classes) {
out.append(clazz);
out.append('\n');
}
}
}
}
private List<String> convertElementsToList(Set<? extends Element> elements) {
var ret = new ArrayList<String>();
for (var elem : elements) {
var packag = processingEnv.getElementUtils().getPackageOf(elem).getQualifiedName().toString();
var fullyQualifiedName = packag + "." + elem.getSimpleName();
ret.add(fullyQualifiedName);
}
return ret;
}
private List<String> getPreviousListedClasses(String fileName, TypeElement annotation)
throws IOException {
try {
var previousFile =
processingEnv.getFiler().getResource(StandardLocation.SOURCE_OUTPUT, "", fileName);
var classSplit = openPreviousFile(previousFile);
var ret = new ArrayList<String>();
for (var clazz : classSplit) {
if (classIsStillAnnotated(clazz, annotation)) {
ret.add(clazz);
}
}
return ret;
} catch (FilerException e) {
throw e;
} catch (IOException e) {
return Collections.emptyList();
}
}
private String[] openPreviousFile(FileObject previousFile) throws IOException {
try (var in = previousFile.openInputStream()) {
var fileContents = ByteStreams.toByteArray(in);
var fileString = new String(fileContents, StandardCharsets.UTF_8);
var classSplit = fileString.split("\n");
return classSplit;
}
}
private boolean classIsStillAnnotated(String clazz, TypeElement annotation) {
var prevTypeElement = processingEnv.getElementUtils().getTypeElement(clazz);
if (prevTypeElement == null) {
return false;
}
var annot = prevTypeElement.getAnnotationMirrors();
for (var i : annot) {
if (i.getAnnotationType().asElement().equals(annotation)) {
return true;
}
}
return false;
}
Upvotes: 1
Views: 176
Reputation: 423
The issue was getSupportedAnnotationTypes() needed to return "*" because it needed to be able to operate on the empty set of annotations. It also needed to always run the writeAnnotationFile
, not just when roundEnv.getElementsAnnotatedWith
returned a result.
Now it properly updates the file when an annotation is removed. This means that it'll consume all classes when you do a full rebuild, but a slow processor is better than one that doesn't work at all. It also has a problem where it creates empty text files instead of removing it, but it's not that big of a deal.
The previous posted solution worked within Eclipse, but did not when built with Gradle. This solution works in both Eclipse and Gradle.
@SupportedAnnotationTypes({"*"})
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService(Processor.class)
public class AnnotationTextDump extends AbstractProcessor {
private List<AnnotationTextDumpWriter> writers = null;
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of("*");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (writers == null) {
writers =
Arrays.asList(
new AnnotationTextDumpWriter("your.Annotation1"),
new AnnotationTextDumpWriter("your.Annotation2"));
}
if (!roundEnv.processingOver()) {
for (var i : writers) {
try {
i.appendElements(annotations, roundEnv, processingEnv);
} catch (IOException e) {
// You should properly log the exception here, but I'm not sure how to get it to appear
// in Eclipse's Problem's view.
e.printStackTrace();
throw new IllegalStateException(e);
}
}
} else {
for (var i : writers) {
i.close();
}
}
return false;
}
}
class AnnotationTextDumpWriter{
private String annotationName;
private boolean initialized;
private Set<String> classes;
private PrintWriter fileOutput;
XmImmutableListWriter(String annotationName) {
this.annotationName = annotationName;
initialized = false;
classes = null;
fileOutput = null;
}
void appendElements(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv,
ProcessingEnvironment processingEnv)
throws IOException {
initialize(processingEnv);
for (var i : annotations) {
if (i.getQualifiedName().toString().equals(annotationName)) {
var elements = roundEnv.getElementsAnnotatedWith(i);
convertElementsToList(elements, processingEnv);
}
}
}
private void initialize(ProcessingEnvironment processingEnv) throws IOException {
if (initialized) {
return;
}
var fileName = "META-INF/annotations/" + annotationName;
initialized = true;
classes = new HashSet<>();
classes.addAll(getPreviousListedClasses(fileName, annotationName, processingEnv));
fileOutput =
new PrintWriter(
processingEnv
.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, "", fileName)
.openWriter());
}
private void convertElementsToList(
Set<? extends Element> elements, ProcessingEnvironment processingEnv) {
for (var elem : elements) {
var packag = processingEnv.getElementUtils().getPackageOf(elem).getQualifiedName().toString();
var fullyQualifiedName = packag + "." + elem.getSimpleName();
classes.add(fullyQualifiedName);
}
}
void close() {
if (classes != null) {
var sortedClasses = new ArrayList<>(classes);
Collections.sort(sortedClasses);
for (var i : sortedClasses) {
fileOutput.append(i);
fileOutput.append('\n');
}
}
fileOutput.close();
}
private static List<String> getPreviousListedClasses(
String fileName, String annotationName, ProcessingEnvironment processingEnv)
throws IOException {
try {
var previousFile =
processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", fileName);
var classSplit = openPreviousFile(previousFile);
var ret = new ArrayList<String>();
for (var clazz : classSplit) {
if (classIsStillAnnotated(clazz, annotationName, processingEnv)) {
ret.add(clazz);
}
}
return ret;
} catch (FilerException e) {
throw e;
} catch (IOException e) {
return Collections.emptyList();
}
}
private static List<String> openPreviousFile(FileObject previousFile) throws IOException {
try (var in = previousFile.openInputStream()) {
var fileContents = ByteStreams.toByteArray(in);
var fileString = new String(fileContents, StandardCharsets.UTF_8);
if (fileString.isBlank()) {
return Collections.emptyList();
}
var classSplit = fileString.split("\n");
return Arrays.asList(classSplit);
}
}
private static boolean classIsStillAnnotated(
String clazz, String annotationName, ProcessingEnvironment processingEnv) {
var prevTypeElement = processingEnv.getElementUtils().getTypeElement(clazz);
if (prevTypeElement == null) {
return false;
}
var annot = prevTypeElement.getAnnotationMirrors();
for (var i : annot) {
var annotElem = i.getAnnotationType().asElement();
var packag =
processingEnv.getElementUtils().getPackageOf(annotElem).getQualifiedName().toString();
var qualifiedAnnotName = packag + "." + annotElem.getSimpleName();
if (qualifiedAnnotName.equals(annotationName)) {
return true;
}
}
return false;
}
}
@SupportedAnnotationTypes({"*"})
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService(Processor.class)
public class AnnotationTextDump extends AbstractProcessor {
public static final String YOUR_ANNOTATION1 = "your.Annotation1";
public static final String YOUR_ANNOTATION2 = "your.Annotation2";
@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of("*");
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
try {
generateAnnotationFiles(annotations, roundEnv);
} catch (Exception e) {
// You should properly log the exception here, but I'm not sure how to get it to appear
// in Eclipse's Problem's view.
throw new IllegalStateException(e);
}
}
return false;
}
private void generateAnnotationFiles(
Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws IOException {
Set<? extends Element> elements1 = Collections.<Element>emptySet();
Set<? extends Element> elements2 = Collections.<Element>emptySet();
for (var i : annotations) {
if (i.getQualifiedName().toString().equals(YOUR_ANNOTATION1)) {
elements1 = roundEnv.getElementsAnnotatedWith(i);
}
if (i.getQualifiedName().toString().equals(YOUR_ANNOTATION2)) {
elements2 = roundEnv.getElementsAnnotatedWith(i);
}
}
writeAnnotationFile(YOUR_ANNOTATION1, elements1);
writeAnnotationFile(YOUR_ANNOTATION2, elements2);
}
private void writeAnnotationFile(String annotationName, Set<? extends Element> elements)
throws IOException {
var fileName = "META-INF/annotations/" + annotationName;
var classBuilder = new HashSet<>(convertElementsToList(elements));
classBuilder.addAll(getPreviousListedClasses(fileName, annotationName));
var classes = new ArrayList<>(classBuilder);
Collections.sort(classes);
var fileContents = new StringBuilder();
for (var i : classes) {
fileContents.append(i);
fileContents.append('\n');
}
try (var out =
new PrintWriter(
processingEnv
.getFiler()
.createResource(StandardLocation.SOURCE_OUTPUT, "", fileName)
.openWriter())) {
out.append(fileContents.toString());
}
}
private List<String> getPreviousListedClasses(String fileName, String annotationName)
throws IOException {
try {
var previousFile =
processingEnv.getFiler().getResource(StandardLocation.SOURCE_OUTPUT, "", fileName);
var classSplit = openPreviousFile(previousFile);
var ret = new ArrayList<String>();
for (var clazz : classSplit) {
if (classIsStillAnnotated(clazz, annotationName)) {
ret.add(clazz);
}
}
return ret;
} catch (FilerException e) {
throw e;
} catch (IOException e) {
return Collections.emptyList();
}
}
private List<String> openPreviousFile(FileObject previousFile) throws IOException {
try (var in = previousFile.openInputStream()) {
var fileContents = ByteStreams.toByteArray(in);
var fileString = new String(fileContents, StandardCharsets.UTF_8);
if (fileString.isBlank()) {
return Collections.emptyList();
}
var classSplit = fileString.split("\n");
return Arrays.asList(classSplit);
}
}
private boolean classIsStillAnnotated(String clazz, String annotationName) {
var prevTypeElement = processingEnv.getElementUtils().getTypeElement(clazz);
if (prevTypeElement == null) {
return false;
}
var annot = prevTypeElement.getAnnotationMirrors();
for (var i : annot) {
var annotElem = i.getAnnotationType().asElement();
var packag =
processingEnv.getElementUtils().getPackageOf(annotElem).getQualifiedName().toString();
var qualifiedAnnotName = packag + "." + annotElem.getSimpleName();
if (qualifiedAnnotName.equals(annotationName)) {
return true;
}
}
return false;
}
private List<String> convertElementsToList(Set<? extends Element> elements) {
var ret = new ArrayList<String>();
for (var elem : elements) {
var packag = processingEnv.getElementUtils().getPackageOf(elem).getQualifiedName().toString();
var fullyQualifiedName = packag + "." + elem.getSimpleName();
ret.add(fullyQualifiedName);
}
return ret;
}
}
Upvotes: 1