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;
}
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 whenroundEnv.getElementsAnnotatedWithreturned 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.
Edit
The previous posted solution worked within Eclipse, but did not when built with Gradle. This solution works in both Eclipse and Gradle.
Old solution that works in Eclipse, but not in Gradle