Add initial implementation of Quarkus Reflection Import Plugin
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
package de.rsitservice.quarkus.reflection;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.apache.maven.plugin.AbstractMojo;
|
||||
import org.apache.maven.plugin.MojoExecutionException;
|
||||
import org.apache.maven.plugin.MojoFailureException;
|
||||
import org.apache.maven.plugins.annotations.LifecyclePhase;
|
||||
import org.apache.maven.plugins.annotations.Mojo;
|
||||
import org.apache.maven.plugins.annotations.Parameter;
|
||||
import org.apache.maven.project.MavenProject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Mojo(name = "generate-reflection-config", defaultPhase = LifecyclePhase.PROCESS_CLASSES, threadSafe = true)
|
||||
public class ReflectionImportMojo extends AbstractMojo {
|
||||
|
||||
private static final Pattern IMPORT_PATTERN =
|
||||
Pattern.compile("^\\s*import\\s+(static\\s+)?([a-zA-Z_][\\w$]*(?:\\.[a-zA-Z_][\\w$]*)+)\\s*;\\s*$");
|
||||
|
||||
private static final TypeReference<List<ReflectionConfigEntry>> ENTRY_LIST_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
/** Current Maven project context. */
|
||||
@Parameter(defaultValue = "${project}", readonly = true, required = true)
|
||||
private MavenProject project;
|
||||
|
||||
/** Source directories that will be scanned for Java imports. Defaults to project compile roots. */
|
||||
@Parameter(defaultValue = "${project.compileSourceRoots}")
|
||||
private List<String> sourceDirectories;
|
||||
|
||||
/** Package prefixes to match against discovered imports (required). */
|
||||
@Parameter(property = "importPackages", required = true)
|
||||
private List<String> importPackages;
|
||||
|
||||
/** Build output directory from the Maven project. */
|
||||
@Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true, required = true)
|
||||
private File outputDirectory;
|
||||
|
||||
/** Optional output file path for the generated reflection config. */
|
||||
@Parameter
|
||||
private File reflectionConfigFile;
|
||||
|
||||
/** Optional extra reflection config files that should be merged before writing output. */
|
||||
@Parameter
|
||||
private List<File> additionalReflectionConfigInputs;
|
||||
|
||||
/** Value used for new entries: allDeclaredConstructors. */
|
||||
@Parameter(defaultValue = "true")
|
||||
private boolean allDeclaredConstructors;
|
||||
|
||||
/** Value used for new entries: allDeclaredMethods. */
|
||||
@Parameter(defaultValue = "true")
|
||||
private boolean allDeclaredMethods;
|
||||
|
||||
/** Value used for new entries: allDeclaredFields. */
|
||||
@Parameter(defaultValue = "true")
|
||||
private boolean allDeclaredFields;
|
||||
|
||||
/** Skip execution when true. */
|
||||
@Parameter(defaultValue = "false")
|
||||
private boolean skip;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT)
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
|
||||
@Override
|
||||
public void execute() throws MojoExecutionException, MojoFailureException {
|
||||
if (skip) {
|
||||
getLog().info("Skipping reflection import scan (skip=true).");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> normalizedPrefixes = normalizeImportPrefixes(importPackages);
|
||||
if (normalizedPrefixes.isEmpty()) {
|
||||
throw new MojoFailureException("Parameter 'importPackages' is required and must not be empty.");
|
||||
}
|
||||
|
||||
File targetFile = resolveTargetFile();
|
||||
Set<String> importsFromCode = scanImports(normalizedPrefixes);
|
||||
|
||||
Map<String, ReflectionConfigEntry> mergedEntries = loadExistingEntries(targetFile);
|
||||
int addedCount = 0;
|
||||
|
||||
for (String fqcn : importsFromCode) {
|
||||
ReflectionConfigEntry existing = mergedEntries.get(fqcn);
|
||||
if (existing != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mergedEntries.put(fqcn, ReflectionConfigEntry.forClass(
|
||||
fqcn,
|
||||
allDeclaredConstructors,
|
||||
allDeclaredMethods,
|
||||
allDeclaredFields));
|
||||
addedCount++;
|
||||
}
|
||||
|
||||
writeReflectionConfig(targetFile, mergedEntries.values());
|
||||
|
||||
getLog().info(String.format(Locale.ROOT,
|
||||
"Reflection config written to %s (imports found: %d, new entries: %d, total entries: %d)",
|
||||
targetFile.getAbsolutePath(), importsFromCode.size(), addedCount, mergedEntries.size()));
|
||||
}
|
||||
|
||||
private List<String> normalizeImportPrefixes(List<String> configuredPrefixes) {
|
||||
if (configuredPrefixes == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return configuredPrefixes.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private File resolveTargetFile() {
|
||||
if (reflectionConfigFile != null) {
|
||||
return reflectionConfigFile;
|
||||
}
|
||||
|
||||
return new File(outputDirectory,
|
||||
"META-INF/native-image/"
|
||||
+ project.getGroupId() + "/"
|
||||
+ project.getArtifactId() + "/"
|
||||
+ "reflection-config.json");
|
||||
}
|
||||
|
||||
private Set<String> scanImports(List<String> normalizedPrefixes) throws MojoExecutionException {
|
||||
List<Path> roots = resolveSourceRoots();
|
||||
Set<String> matchedImports = new TreeSet<>();
|
||||
|
||||
for (Path root : roots) {
|
||||
if (!Files.isDirectory(root)) {
|
||||
getLog().debug("Skipping non-existing source directory: " + root);
|
||||
continue;
|
||||
}
|
||||
|
||||
try (Stream<Path> files = Files.walk(root)) {
|
||||
files.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".java"))
|
||||
.forEach(path -> collectImportsFromFile(path, normalizedPrefixes, matchedImports));
|
||||
} catch (IOException e) {
|
||||
throw new MojoExecutionException("Error while scanning source directory: " + root, e);
|
||||
}
|
||||
}
|
||||
|
||||
return matchedImports;
|
||||
}
|
||||
|
||||
private List<Path> resolveSourceRoots() {
|
||||
List<String> configured = sourceDirectories == null ? List.of() : sourceDirectories;
|
||||
List<Path> roots = new ArrayList<>();
|
||||
|
||||
for (String rawRoot : configured) {
|
||||
if (rawRoot == null || rawRoot.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
roots.add(Paths.get(rawRoot));
|
||||
}
|
||||
|
||||
if (roots.isEmpty()) {
|
||||
roots.add(Paths.get(project.getBasedir().getAbsolutePath(), "src", "main", "java"));
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
private void collectImportsFromFile(Path javaFile,
|
||||
List<String> normalizedPrefixes,
|
||||
Set<String> matchedImports) {
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(javaFile);
|
||||
} catch (IOException e) {
|
||||
getLog().warn("Cannot read file: " + javaFile + " - " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
for (String line : lines) {
|
||||
Matcher matcher = IMPORT_PATTERN.matcher(line);
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isStaticImport = matcher.group(1) != null;
|
||||
String importedType = matcher.group(2);
|
||||
|
||||
if (isStaticImport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchesAnyPrefix(importedType, normalizedPrefixes)) {
|
||||
matchedImports.add(importedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesAnyPrefix(String importedType, List<String> normalizedPrefixes) {
|
||||
for (String prefix : normalizedPrefixes) {
|
||||
if (importedType.equals(prefix) || importedType.startsWith(prefix + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, ReflectionConfigEntry> loadExistingEntries(File targetFile) throws MojoExecutionException {
|
||||
Map<String, ReflectionConfigEntry> merged = new LinkedHashMap<>();
|
||||
Set<File> inputs = new LinkedHashSet<>();
|
||||
|
||||
addIfExists(inputs, targetFile);
|
||||
|
||||
Path conventionalConfigPath = Paths.get(
|
||||
project.getBasedir().getAbsolutePath(),
|
||||
"src", "main", "resources",
|
||||
"META-INF", "native-image",
|
||||
project.getGroupId(),
|
||||
project.getArtifactId(),
|
||||
"reflection-config.json");
|
||||
addIfExists(inputs, conventionalConfigPath.toFile());
|
||||
|
||||
Path fallbackConfigPath = Paths.get(
|
||||
project.getBasedir().getAbsolutePath(),
|
||||
"src", "main", "resources",
|
||||
"META-INF", "native-image",
|
||||
"reflection-config.json");
|
||||
addIfExists(inputs, fallbackConfigPath.toFile());
|
||||
|
||||
if (additionalReflectionConfigInputs != null) {
|
||||
for (File file : additionalReflectionConfigInputs) {
|
||||
addIfExists(inputs, file);
|
||||
}
|
||||
}
|
||||
|
||||
for (File input : inputs) {
|
||||
List<ReflectionConfigEntry> entries = readEntries(input);
|
||||
for (ReflectionConfigEntry entry : entries) {
|
||||
if (entry.getName() == null || entry.getName().trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
merged.putIfAbsent(entry.getName(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private void addIfExists(Set<File> files, File file) {
|
||||
if (file != null && file.isFile()) {
|
||||
files.add(file.getAbsoluteFile());
|
||||
}
|
||||
}
|
||||
|
||||
private List<ReflectionConfigEntry> readEntries(File file) throws MojoExecutionException {
|
||||
try {
|
||||
return objectMapper.readValue(file, ENTRY_LIST_TYPE);
|
||||
} catch (IOException e) {
|
||||
throw new MojoExecutionException("Cannot read reflection config: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeReflectionConfig(File targetFile,
|
||||
Collection<ReflectionConfigEntry> entries) throws MojoExecutionException {
|
||||
File parent = targetFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new MojoExecutionException("Cannot create output directory: " + parent.getAbsolutePath());
|
||||
}
|
||||
|
||||
List<ReflectionConfigEntry> sortedEntries = entries.stream()
|
||||
.sorted((left, right) -> left.getName().compareToIgnoreCase(right.getName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
objectMapper.writeValue(targetFile, sortedEntries);
|
||||
} catch (IOException e) {
|
||||
throw new MojoExecutionException("Cannot write reflection config: " + targetFile.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ReflectionConfigEntry {
|
||||
|
||||
private String name;
|
||||
private Boolean allDeclaredConstructors;
|
||||
private Boolean allDeclaredMethods;
|
||||
private Boolean allDeclaredFields;
|
||||
private final Map<String, Object> additional = new LinkedHashMap<>();
|
||||
|
||||
public static ReflectionConfigEntry forClass(String fqcn,
|
||||
boolean constructors,
|
||||
boolean methods,
|
||||
boolean fields) {
|
||||
ReflectionConfigEntry entry = new ReflectionConfigEntry();
|
||||
entry.setName(fqcn);
|
||||
entry.setAllDeclaredConstructors(constructors);
|
||||
entry.setAllDeclaredMethods(methods);
|
||||
entry.setAllDeclaredFields(fields);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Boolean getAllDeclaredConstructors() {
|
||||
return allDeclaredConstructors;
|
||||
}
|
||||
|
||||
public void setAllDeclaredConstructors(Boolean allDeclaredConstructors) {
|
||||
this.allDeclaredConstructors = allDeclaredConstructors;
|
||||
}
|
||||
|
||||
public Boolean getAllDeclaredMethods() {
|
||||
return allDeclaredMethods;
|
||||
}
|
||||
|
||||
public void setAllDeclaredMethods(Boolean allDeclaredMethods) {
|
||||
this.allDeclaredMethods = allDeclaredMethods;
|
||||
}
|
||||
|
||||
public Boolean getAllDeclaredFields() {
|
||||
return allDeclaredFields;
|
||||
}
|
||||
|
||||
public void setAllDeclaredFields(Boolean allDeclaredFields) {
|
||||
this.allDeclaredFields = allDeclaredFields;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void putAdditionalProperty(String key, Object value) {
|
||||
additional.put(key, value);
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getAdditional() {
|
||||
return additional;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user