DiIndexProcessor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.di.tool;
import javax.annotation.processing.AbstractProcessor;
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.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.apache.maven.api.di.Named;
/**
* Annotation processor that generates an index file for classes annotated with {@link Named}.
* This processor scans for classes with the {@code @Named} annotation and creates a file
* at {@code META-INF/maven/org.apache.maven.api.di.Inject} containing the fully qualified
* names of these classes.
*
* @since 4.0.0
*/
@SupportedAnnotationTypes("org.apache.maven.api.di.Named")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class DiIndexProcessor extends AbstractProcessor {
/**
* Set of fully qualified class names that have been processed and contain the {@link Named} annotation.
*/
private final Set<String> processedClasses = new HashSet<>();
/**
* Processes classes with the {@link Named} annotation and generates an index file.
*
* @param annotations the annotation types requested to be processed
* @param roundEnv environment for information about the current and prior round
* @return whether or not the set of annotations are claimed by this processor
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
logMessage(
Diagnostic.Kind.NOTE, "Processing " + roundEnv.getRootElements().size() + " classes");
for (Element element : roundEnv.getElementsAnnotatedWith(Named.class)) {
if (element instanceof TypeElement typeElement) {
String className = getFullClassName(typeElement);
processedClasses.add(className);
}
}
if (roundEnv.processingOver()) {
try {
updateFileIfChanged();
} catch (Exception e) {
logError("Error updating file", e);
}
}
return true;
}
/**
* Gets the fully qualified class name for a type element, including handling inner classes.
*
* @param typeElement the type element to get the class name for
* @return the fully qualified class name
*/
private String getFullClassName(TypeElement typeElement) {
StringBuilder className = new StringBuilder(typeElement.getSimpleName());
Element enclosingElement = typeElement.getEnclosingElement();
while (enclosingElement instanceof TypeElement enclosingTypeElement) {
className.insert(0, "$").insert(0, enclosingTypeElement.getSimpleName());
enclosingElement = enclosingElement.getEnclosingElement();
}
if (enclosingElement instanceof PackageElement packageElement) {
className.insert(0, ".").insert(0, packageElement.getQualifiedName());
}
return className.toString();
}
/**
* Updates the index file if its content has changed.
* The file is created at {@code META-INF/maven/org.apache.maven.api.di.Inject} and contains
* the fully qualified names of classes with the {@link Named} annotation.
*
* @throws IOException if there is an error reading or writing the file
*/
private void updateFileIfChanged() throws IOException {
String path = "META-INF/maven/org.apache.maven.api.di.Inject";
Set<String> existingClasses = new TreeSet<>(); // Using TreeSet for natural ordering
String existingContent = "";
// Try to read existing content
try {
FileObject inputFile = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", path);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputFile.openInputStream()))) {
String line;
StringBuilder contentBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
if (!line.trim().startsWith("#")) {
existingClasses.add(line.trim());
}
contentBuilder.append(line).append("\n");
}
existingContent = contentBuilder.toString();
}
} catch (IOException e) {
logMessage(Diagnostic.Kind.NOTE, "Unable to read existing file. Proceeding with empty content.");
}
Set<String> allClasses = new TreeSet<>(existingClasses); // Using TreeSet for natural ordering
allClasses.addAll(processedClasses);
StringBuilder newContentBuilder = new StringBuilder();
for (String className : allClasses) {
newContentBuilder.append(className).append("\n");
}
String newContent = newContentBuilder.toString();
if (!newContent.equals(existingContent)) {
logMessage(Diagnostic.Kind.NOTE, "Content has changed. Updating file.");
try {
FileObject outputFile =
processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", path);
try (Writer writer = outputFile.openWriter()) {
writer.write(newContent);
}
} catch (IOException e) {
logError("Failed to write to file", e);
throw e; // Re-throw to ensure the compilation fails
}
} else {
logMessage(Diagnostic.Kind.NOTE, "Content unchanged. Skipping file update.");
}
}
/**
* Logs a message to the annotation processing environment.
*
* @param kind the kind of diagnostic message
* @param message the message to log
*/
private void logMessage(Diagnostic.Kind kind, String message) {
processingEnv.getMessager().printMessage(kind, message);
}
/**
* Logs an error message with exception details to the annotation processing environment.
*
* @param message the error message
* @param e the exception that occurred
*/
private void logError(String message, Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String stackTrace = sw.toString();
String fullMessage = message + "\n" + "Exception: "
+ e.getClass().getName() + "\n" + "Message: "
+ e.getMessage() + "\n" + "Stack trace:\n"
+ stackTrace;
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, fullMessage);
}
}