You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by kw...@apache.org on 2026/07/16 08:17:25 UTC
(maven-resolver) branch feature/doclet-for-config-documentation created (now cb0080c5d)
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a change to branch feature/doclet-for-config-documentation
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
at cb0080c5d Use custom doclet to extract configuration metadata with the help of javadoc
This branch includes the following new commits:
new cb0080c5d Use custom doclet to extract configuration metadata with the help of javadoc
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven-resolver) 01/01: Use custom doclet to extract configuration metadata with the help of javadoc
Posted by kw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch feature/doclet-for-config-documentation
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
commit cb0080c5dad9722a860489e04c8dc9dae53f5cc9
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Thu Jul 16 10:17:12 2026 +0200
Use custom doclet to extract configuration metadata with the help of
javadoc
Remove unused "maven" mode
---
maven-resolver-tools/pom.xml | 37 +-
.../eclipse/aether/tools/CollectConfiguration.java | 477 ++++++---------------
.../aether/tools/ConfigurationCollectorDoclet.java | 392 +++++++++++++++++
3 files changed, 552 insertions(+), 354 deletions(-)
diff --git a/maven-resolver-tools/pom.xml b/maven-resolver-tools/pom.xml
index 677b73a59..ba1da627d 100644
--- a/maven-resolver-tools/pom.xml
+++ b/maven-resolver-tools/pom.xml
@@ -33,7 +33,6 @@
<properties>
<javaVersion>17</javaVersion>
- <roasterVersion>2.31.1.Final</roasterVersion>
</properties>
<dependencies>
@@ -63,6 +62,12 @@
<artifactId>maven-resolver-named-locks</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-named-locks-ipc</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-named-locks-hazelcast</artifactId>
@@ -103,6 +108,12 @@
<artifactId>maven-resolver-transport-jetty</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.maven.resolver</groupId>
+ <artifactId>maven-resolver-transport-minio</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-wagon</artifactId>
@@ -119,26 +130,18 @@
<scope>provided</scope>
</dependency>
+ <!-- Needed so the javadoc doclet can resolve javax.inject.* referenced by the scanned resolver sources. -->
<dependency>
- <groupId>org.jboss.forge.roaster</groupId>
- <artifactId>roaster-api</artifactId>
- <version>${roasterVersion}</version>
- </dependency>
- <dependency>
- <groupId>org.jboss.forge.roaster</groupId>
- <artifactId>roaster-jdt</artifactId>
- <version>${roasterVersion}</version>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <scope>provided</scope>
</dependency>
+
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>${plexusUtils4Version}</version>
</dependency>
- <dependency>
- <groupId>org.ow2.asm</groupId>
- <artifactId>asm</artifactId>
- <version>9.10.1</version>
- </dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
@@ -183,8 +186,10 @@
<phase>package</phase>
<configuration>
<mainClass>org.eclipse.aether.tools.CollectConfiguration</mainClass>
+ <!-- 'test' scope classpath includes the provided-scope resolver artifacts, which the javadoc doclet
+ needs to resolve the types referenced by the scanned configuration sources. -->
+ <classpathScope>test</classpathScope>
<arguments>
- <argument>--mode=resolver</argument>
<argument>--templates=configuration.md</argument>
<argument>${basedir}/..</argument>
<argument>${basedir}/../target/generated-site/markdown/</argument>
@@ -199,8 +204,8 @@
<phase>package</phase>
<configuration>
<mainClass>org.eclipse.aether.tools.CollectConfiguration</mainClass>
+ <classpathScope>test</classpathScope>
<arguments>
- <argument>--mode=resolver</argument>
<argument>--templates=configuration.properties,configuration.yaml</argument>
<argument>${basedir}/..</argument>
<argument>${basedir}/target/</argument>
diff --git a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/CollectConfiguration.java b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/CollectConfiguration.java
index 74e1dabfc..dc5f53678 100644
--- a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/CollectConfiguration.java
+++ b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/CollectConfiguration.java
@@ -18,48 +18,38 @@
*/
package org.eclipse.aether.tools;
-import java.io.IOException;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DocumentationTool;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import java.io.File;
import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.io.UncheckedIOException;
+import java.io.Reader;
import java.io.Writer;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Comparator;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.spi.ToolProvider;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.codehaus.plexus.util.io.CachingWriter;
-import org.jboss.forge.roaster.Roaster;
-import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
-import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
-import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
-import org.jboss.forge.roaster.model.JavaDoc;
-import org.jboss.forge.roaster.model.JavaDocCapable;
-import org.jboss.forge.roaster.model.JavaDocTag;
-import org.jboss.forge.roaster.model.JavaType;
-import org.jboss.forge.roaster.model.impl.JavaDocImpl;
-import org.jboss.forge.roaster.model.source.FieldSource;
-import org.jboss.forge.roaster.model.source.JavaClassSource;
-import org.jboss.forge.roaster.model.source.JavaDocSource;
-import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.ClassReader;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.FieldVisitor;
-import org.objectweb.asm.Opcodes;
import picocli.CommandLine;
@CommandLine.Command(name = "docgen", description = "Maven Documentation Generator")
@@ -70,17 +60,23 @@ public class CollectConfiguration implements Callable<Integer> {
protected static final String KEY = "key";
- public enum Mode {
- maven,
- resolver
- }
+ /**
+ * The metadata fields collected per configuration key and written to / read from the intermediate properties file.
+ */
+ protected static final List<String> FIELDS = List.of(
+ KEY,
+ "defaultValue",
+ "fqName",
+ "description",
+ "since",
+ "configurationSource",
+ "configurationType",
+ "supportRepoIdSuffix");
- @CommandLine.Option(
- names = {"-m", "--mode"},
- arity = "1",
- paramLabel = "mode",
- description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'")
- protected Mode mode;
+ /**
+ * Javadoc block tag marking a constant field as a configuration key.
+ */
+ protected static final String CONFIGURATION_MARKER = "@configurationSource";
@CommandLine.Option(
names = {"-t", "--templates"},
@@ -102,44 +98,15 @@ public class CollectConfiguration implements Callable<Integer> {
rootDirectory = rootDirectory.toAbsolutePath().normalize();
outputDirectory = outputDirectory.toAbsolutePath().normalize();
- ArrayList<Map<String, String>> discoveredKeys = new ArrayList<>();
- try (Stream<Path> stream = Files.walk(rootDirectory)) {
- if (mode == Mode.maven) {
- System.out.println("Processing Maven sources from " + rootDirectory);
- stream.map(Path::toAbsolutePath)
- .filter(p -> p.getFileName().toString().endsWith(".class"))
- .filter(p -> p.toString().contains("/target/classes/"))
- .forEach(p -> processMavenClass(p, discoveredKeys));
- } else if (mode == Mode.resolver) {
- System.out.println("Processing Resolver sources from " + rootDirectory);
- stream.map(Path::toAbsolutePath)
- .filter(p -> p.getFileName().toString().endsWith(".java"))
- .filter(p -> p.toString().contains("/src/main/java/"))
- .filter(p -> !p.toString().endsWith("/module-info.java"))
- .forEach(p -> processResolverClass(p, discoveredKeys));
- } else {
- throw new IllegalStateException("Unsupported mode " + mode);
- }
- }
-
- discoveredKeys.sort(Comparator.comparing(e -> e.get(KEY)));
-
- Properties properties = new Properties();
- properties.setProperty("resource.loaders", "classpath");
- properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
- VelocityEngine velocityEngine = new VelocityEngine();
- velocityEngine.init(properties);
-
- VelocityContext context = new VelocityContext();
- context.put("keys", discoveredKeys);
-
- for (String template : templates) {
- Path output = outputDirectory.resolve(template);
- Files.createDirectories(output.getParent());
- System.out.println("Writing out to " + output);
- try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
- velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
- }
+ System.out.println("Processing sources from " + rootDirectory);
+ Path intermediateFile = Files.createTempFile("configuration-keys", ".properties");
+ try {
+ runDoclet(intermediateFile);
+ List<Map<String, String>> discoveredKeys = readDiscoveredKeys(intermediateFile);
+ discoveredKeys.sort(Comparator.comparing(e -> e.get(KEY)));
+ render(discoveredKeys);
+ } finally {
+ Files.deleteIfExists(intermediateFile);
}
return 0;
} catch (Exception e) {
@@ -148,294 +115,128 @@ public class CollectConfiguration implements Callable<Integer> {
}
}
- protected void processMavenClass(Path path, List<Map<String, String>> discoveredKeys) {
- try {
- ClassReader classReader = new ClassReader(Files.newInputStream(path));
- classReader.accept(
- new ClassVisitor(Opcodes.ASM9) {
- @Override
- public FieldVisitor visitField(
- int fieldAccess,
- String fieldName,
- String fieldDescriptor,
- String fieldSignature,
- Object fieldValue) {
- return new FieldVisitor(Opcodes.ASM9) {
- @Override
- public AnnotationVisitor visitAnnotation(
- String annotationDescriptor, boolean annotationVisible) {
- if (annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
- return new AnnotationVisitor(Opcodes.ASM9) {
- final Map<String, Object> values = new HashMap<>();
-
- @Override
- public void visit(String name, Object value) {
- values.put(name, value);
- }
-
- @Override
- public void visitEnum(String name, String descriptor, String value) {
- values.put(name, value);
- }
-
- @Override
- public void visitEnd() {
- JavaType<?> jtype = parse(Paths.get(path.toString()
- .replace("/target/classes/", "/src/main/java/")
- .replace(".class", ".java")));
- FieldSource<JavaClassSource> f =
- ((JavaClassSource) jtype).getField(fieldName);
-
- String fqName = null;
- String desc = cloneJavadoc(f.getJavaDoc())
- .removeAllTags()
- .getFullText()
- .replace("*", "\\*");
- String since = getSince(f);
- String source = (values.get("source") != null
- ? (String) values.get("source")
- : "USER_PROPERTIES") // TODO: enum
- .toLowerCase();
- source = switch (source) {
- case "model" -> "Model properties";
- case "user_properties" -> "User properties";
- default -> source;
- };
- String type = (values.get("type") != null
- ? (String) values.get("type")
- : "java.lang.String");
- if (type.startsWith("java.lang.")) {
- type = type.substring("java.lang.".length());
- } else if (type.startsWith("java.util.")) {
- type = type.substring("java.util.".length());
- }
- discoveredKeys.add(Map.of(
- KEY,
- fieldValue.toString(),
- "defaultValue",
- values.get("defaultValue") != null
- ? values.get("defaultValue")
- .toString()
- : "",
- "fqName",
- nvl(fqName, ""),
- "description",
- desc,
- "since",
- nvl(since, ""),
- "configurationSource",
- source,
- "configurationType",
- type));
- }
- };
- }
- return null;
- }
- };
- }
- },
- 0);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ /**
+ * Collects the source files under {@link #rootDirectory} and runs {@link ConfigurationCollectorDoclet} against them,
+ * having it write the discovered configuration keys into the given intermediate properties file.
+ */
+ protected void runDoclet(Path intermediateFile) throws Exception {
+ // Only feed javadoc the files that actually declare configuration keys. This keeps the set of types that
+ // javadoc must resolve small, avoiding failures caused by unrelated sources referencing dependencies that
+ // are not on this module's classpath (e.g. gson, jetty).
+ List<File> sourceFiles;
+ try (Stream<Path> stream = Files.walk(rootDirectory)) {
+ sourceFiles = stream.map(Path::toAbsolutePath)
+ .filter(p -> p.getFileName().toString().endsWith(".java"))
+ .filter(p -> p.toString().contains("/src/main/java/"))
+ .filter(p -> !p.toString().endsWith("/module-info.java"))
+ .filter(p -> !p.toString().contains("/maven-resolver-tools/"))
+ .filter(p -> fileContains(p, CONFIGURATION_MARKER))
+ .map(Path::toFile)
+ .collect(Collectors.toList());
}
- }
-
- protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
- JavaType<?> type = parse(path);
- if (type instanceof JavaClassSource javaClassSource) {
- javaClassSource.getFields().stream()
- .filter(this::hasConfigurationSource)
- .forEach(f -> {
- Map<String, String> constants = extractConstants(Paths.get(path.toString()
- .replace("/src/main/java/", "/target/classes/")
- .replace(".java", ".class")));
-
- String name = f.getName();
- String key = constants.get(name);
- String fqName = f.getOrigin().getCanonicalName() + "." + name;
- String configurationType = getConfigurationType(f);
- String defValue = getTag(f, "@configurationDefaultValue");
- if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
- // constant "lookup"
- String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
- if (lookupValue == null) {
- // currently we hard fail if javadoc cannot be looked up
- // workaround: at cost of redundancy, but declare constants in situ for now
- // (in same class)
- throw new IllegalArgumentException(
- "Could not look up " + defValue + " for configuration " + fqName);
- }
- defValue = lookupValue;
- if ("java.lang.Long".equals(configurationType)
- && (defValue.endsWith("l") || defValue.endsWith("L"))) {
- defValue = defValue.substring(0, defValue.length() - 1);
- }
- }
- discoveredKeys.add(Map.of(
- KEY,
- key,
- "defaultValue",
- nvl(defValue, ""),
- "fqName",
- fqName,
- "description",
- cleanseJavadoc(f),
- "since",
- nvl(getSince(f), ""),
- "configurationSource",
- getConfigurationSource(f),
- "configurationType",
- configurationType,
- "supportRepoIdSuffix",
- toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
- });
+ if (sourceFiles.isEmpty()) {
+ throw new IllegalStateException(
+ "No Java sources declaring configuration keys found under " + rootDirectory);
}
- }
-
- protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
- Javadoc jd = (Javadoc) javaDoc.getInternal();
- return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
- ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
- }
- protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
- JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
- String[] text = javaDoc.getFullText().split("\n");
- StringBuilder result = new StringBuilder();
- for (String line : text) {
- if (!line.startsWith("@") && !line.trim().isEmpty()) {
- result.append(line);
+ DocumentationTool documentationTool = ToolProvider.getSystemDocumentationTool();
+ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+ try (StandardJavaFileManager fileManager =
+ documentationTool.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
+ // Configure the classpath on the file manager (the -classpath option is not honored when a file manager
+ // is supplied to getTask()). Note that under exec:java the project dependencies are on the context
+ // classloader, not on the JVM's java.class.path.
+ fileManager.setLocation(StandardLocation.CLASS_PATH, resolveClasspath());
+
+ Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFiles);
+
+ List<String> options =
+ new ArrayList<>(Arrays.asList("--output", intermediateFile.toString(), "-encoding", "UTF-8"));
+
+ Writer out = new PrintWriter(System.err);
+ DocumentationTool.DocumentationTask task = documentationTool.getTask(
+ out, fileManager, diagnostics, ConfigurationCollectorDoclet.class, options, compilationUnits);
+ boolean ok = task.call();
+ out.flush();
+ if (!ok) {
+ diagnostics.getDiagnostics().forEach(d -> System.err.println(d));
+ throw new IllegalStateException("Javadoc doclet execution failed");
}
}
- return cleanseTags(result.toString());
}
- protected String cleanseTags(String text) {
- // {@code XXX} -> <pre>XXX</pre>
- // {@link XXX} -> ??? pre for now
- Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
- Matcher matcher = pattern.matcher(text);
- if (!matcher.find()) {
- return text;
- }
- int prevEnd = 0;
- StringBuilder result = new StringBuilder();
- do {
- result.append(text, prevEnd, matcher.start(1));
- result.append("<code>");
- result.append(matcher.group(2));
- result.append("</code>");
- prevEnd = matcher.end(1);
- } while (matcher.find());
- result.append(text, prevEnd, text.length());
- return result.toString();
- }
-
- protected JavaType<?> parse(Path path) {
+ private static boolean fileContains(Path path, String marker) {
try {
- return Roaster.parse(path.toFile());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ return Files.readString(path, StandardCharsets.UTF_8).contains(marker);
+ } catch (Exception e) {
+ return false;
}
}
- protected String toYesNo(String value) {
- return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
- }
-
- protected String nvl(String string, String def) {
- return string == null ? def : string;
- }
-
- protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
- return getTag(javaDocCapable, "@configurationSource") != null;
- }
-
- protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
- String type = getTag(javaDocCapable, "@configurationType");
- if (type != null) {
- String linkPrefix = "{@link ";
- String linkSuffix = "}";
- if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
- type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
- }
- String javaLangPackage = "java.lang.";
- if (type.startsWith(javaLangPackage)) {
- type = type.substring(javaLangPackage.length());
+ /**
+ * Resolves the classpath to use for symbol resolution during the javadoc run. Under {@code exec:java} the project
+ * dependencies live on the context classloader (a {@link URLClassLoader}), not on the JVM's
+ * {@code java.class.path}, so both sources are combined.
+ */
+ private static List<File> resolveClasspath() {
+ List<File> classpath = new ArrayList<>();
+ for (ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl != null; cl = cl.getParent()) {
+ if (cl instanceof URLClassLoader) {
+ for (URL url : ((URLClassLoader) cl).getURLs()) {
+ if ("file".equals(url.getProtocol())) {
+ try {
+ classpath.add(new File(url.toURI()));
+ } catch (URISyntaxException e) {
+ classpath.add(new File(url.getPath()));
+ }
+ }
+ }
}
}
- return nvl(type, "n/a");
- }
-
- protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
- String source = getTag(javaDocCapable, "@configurationSource");
- if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
- return "Session Configuration";
- } else if ("{@link System#getProperty(String,String)}".equals(source)) {
- return "Java System Properties";
- } else {
- return source;
+ for (String element : System.getProperty("java.class.path").split(File.pathSeparator)) {
+ classpath.add(new File(element));
}
+ return classpath;
}
- protected String getSince(JavaDocCapable<?> javaDocCapable) {
- List<JavaDocTag> tags;
- if (javaDocCapable != null) {
- if (javaDocCapable instanceof FieldSource<?> fieldSource) {
- tags = fieldSource.getJavaDoc().getTags("@since");
- if (tags.isEmpty()) {
- return getSince(fieldSource.getOrigin());
- } else {
- return tags.get(0).getValue();
- }
- } else if (javaDocCapable instanceof JavaClassSource classSource) {
- tags = classSource.getJavaDoc().getTags("@since");
- if (!tags.isEmpty()) {
- return tags.get(0).getValue();
- }
- }
+ /**
+ * Reads back the intermediate properties file produced by {@link ConfigurationCollectorDoclet} into the list of
+ * maps consumed by the Velocity templates.
+ */
+ protected List<Map<String, String>> readDiscoveredKeys(Path intermediateFile) throws Exception {
+ Properties properties = new Properties();
+ try (Reader reader = Files.newBufferedReader(intermediateFile, StandardCharsets.UTF_8)) {
+ properties.load(reader);
}
- return null;
- }
-
- protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
- List<JavaDocTag> tags;
- if (javaDocCapable != null) {
- if (javaDocCapable instanceof FieldSource<?> fieldSource) {
- tags = fieldSource.getJavaDoc().getTags(tagName);
- if (tags.isEmpty()) {
- return getTag(fieldSource.getOrigin(), tagName);
- } else {
- return tags.get(0).getValue();
- }
+ int count = Integer.parseInt(properties.getProperty("keys.count", "0"));
+ List<Map<String, String>> discoveredKeys = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ Map<String, String> entry = new LinkedHashMap<>();
+ for (String field : FIELDS) {
+ entry.put(field, properties.getProperty("keys." + i + "." + field, ""));
}
+ discoveredKeys.add(entry);
}
- return null;
+ return discoveredKeys;
}
- protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z0-9_]+) = (.*);");
+ protected void render(List<Map<String, String>> discoveredKeys) throws Exception {
+ Properties properties = new Properties();
+ properties.setProperty("resource.loaders", "classpath");
+ properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
+ VelocityEngine velocityEngine = new VelocityEngine();
+ velocityEngine.init(properties);
- protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
+ VelocityContext context = new VelocityContext();
+ context.put("keys", discoveredKeys);
- /**
- * Builds "constant table" for one single class.
- * <p>
- * Limitations:
- * - works only for single class (no inherited constants)
- * - does not work for fields that are Enum.name()
- * - more to come
- */
- protected static Map<String, String> extractConstants(Path file) {
- StringWriter out = new StringWriter();
- JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
- Map<String, String> result = new HashMap<>();
- out.getBuffer().toString().lines().forEach(l -> {
- Matcher matcher = CONSTANT_PATTERN.matcher(l);
- if (matcher.matches()) {
- result.put(matcher.group(1), matcher.group(2));
+ for (String template : templates) {
+ Path output = outputDirectory.resolve(template);
+ Files.createDirectories(output.getParent());
+ System.out.println("Writing out to " + output);
+ try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
+ velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
}
- });
- return result;
+ }
}
}
diff --git a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
new file mode 100644
index 000000000..02f606fe8
--- /dev/null
+++ b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
@@ -0,0 +1,392 @@
+/*
+ * 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.eclipse.aether.tools;
+
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.ElementFilter;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.LiteralTree;
+import com.sun.source.doctree.SinceTree;
+import com.sun.source.doctree.TextTree;
+import com.sun.source.doctree.UnknownBlockTagTree;
+import com.sun.source.util.DocTrees;
+import jdk.javadoc.doclet.Doclet;
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * A custom Javadoc {@link Doclet} that scans constant fields for configuration metadata declared via custom Javadoc
+ * block tags (e.g. {@code @configurationSource}) and writes the discovered keys into an intermediate
+ * {@link Properties} file. That file is subsequently consumed by {@link CollectConfiguration} to render the
+ * documentation via Velocity templates.
+ * <p>
+ * The intermediate file uses an indexed layout:
+ * <pre>
+ * keys.count=N
+ * keys.0.key=...
+ * keys.0.description=...
+ * ...
+ * </pre>
+ */
+public class ConfigurationCollectorDoclet implements Doclet {
+
+ private Path output;
+ private DocTrees docTrees;
+
+ @Override
+ public void init(Locale locale, Reporter reporter) {
+ // no state to initialize
+ }
+
+ @Override
+ public String getName() {
+ return "ConfigurationCollector";
+ }
+
+ @Override
+ public Set<? extends Option> getSupportedOptions() {
+ return Set.of(new SimpleOption(
+ List.of("--output", "-o"),
+ 1,
+ "The intermediate properties file to write discovered keys to",
+ "<file>",
+ args -> output = Paths.get(args.get(0))));
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public boolean run(DocletEnvironment environment) {
+ try {
+ return doRun(environment);
+ } catch (RuntimeException e) {
+ e.printStackTrace(System.err);
+ return false;
+ }
+ }
+
+ private boolean doRun(DocletEnvironment environment) {
+ if (output == null) {
+ throw new IllegalStateException("Missing required --output option");
+ }
+ docTrees = environment.getDocTrees();
+ List<Map<String, String>> discoveredKeys = new ArrayList<>();
+
+ Set<TypeElement> types = ElementFilter.typesIn(environment.getIncludedElements());
+ for (TypeElement type : types) {
+ for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) {
+ if (field.getConstantValue() == null) {
+ continue;
+ }
+ DocCommentTree docComment = docTrees.getDocCommentTree(field);
+ processField(type, field, docComment, discoveredKeys);
+ }
+ }
+
+ writeProperties(discoveredKeys);
+ return true;
+ }
+
+ private void processField(
+ TypeElement type, VariableElement field, DocCommentTree docComment, List<Map<String, String>> discovered) {
+ if (docComment == null) {
+ return;
+ }
+ Map<String, List<? extends DocTree>> blockTags = collectBlockTags(docComment);
+ if (!blockTags.containsKey("configurationSource")) {
+ return;
+ }
+
+ String configurationType = getConfigurationType(renderContent(blockTags.get("configurationType")));
+ String defValue = resolveDefaultValue(type, blockTags.get("configurationDefaultValue"));
+
+ Map<String, String> entry = new LinkedHashMap<>();
+ entry.put("key", String.valueOf(field.getConstantValue()));
+ entry.put("defaultValue", nvl(defValue, ""));
+ entry.put("fqName", type.getQualifiedName() + "." + field.getSimpleName());
+ entry.put("description", cleanseJavadoc(renderContent(docComment.getFullBody())));
+ entry.put("since", nvl(getSince(type, docComment), ""));
+ entry.put("configurationSource", getConfigurationSource(renderContent(blockTags.get("configurationSource"))));
+ entry.put("configurationType", configurationType);
+ entry.put("supportRepoIdSuffix", toYesNo(renderContent(blockTags.get("configurationRepoIdSuffix"))));
+ discovered.add(entry);
+ }
+
+ private void writeProperties(List<Map<String, String>> discoveredKeys) {
+ Properties properties = new Properties();
+ properties.setProperty("keys.count", String.valueOf(discoveredKeys.size()));
+ for (int i = 0; i < discoveredKeys.size(); i++) {
+ Map<String, String> entry = discoveredKeys.get(i);
+ for (Map.Entry<String, String> field : entry.entrySet()) {
+ properties.setProperty("keys." + i + "." + field.getKey(), field.getValue());
+ }
+ }
+ try {
+ if (output.getParent() != null) {
+ Files.createDirectories(output.getParent());
+ }
+ try (Writer writer = new BufferedWriter(Files.newBufferedWriter(output, StandardCharsets.UTF_8))) {
+ properties.store(writer, "Generated by ConfigurationCollectorDoclet - DO NOT EDIT");
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ // --- Javadoc extraction helpers -------------------------------------------------------------------------------
+
+ private Map<String, List<? extends DocTree>> collectBlockTags(DocCommentTree docComment) {
+ Map<String, List<? extends DocTree>> result = new LinkedHashMap<>();
+ for (DocTree tag : docComment.getBlockTags()) {
+ if (tag instanceof UnknownBlockTagTree) {
+ UnknownBlockTagTree unknown = (UnknownBlockTagTree) tag;
+ result.put(unknown.getTagName(), unknown.getContent());
+ }
+ }
+ return result;
+ }
+
+ private String resolveDefaultValue(TypeElement type, List<? extends DocTree> content) {
+ if (content == null || content.isEmpty()) {
+ return null;
+ }
+ for (DocTree tree : content) {
+ if (tree instanceof LinkTree) {
+ LinkTree link = (LinkTree) tree;
+ if (link.getReference() != null) {
+ String signature = link.getReference().getSignature();
+ String constantName = signature.substring(signature.indexOf('#') + 1);
+ String value = lookupConstant(type, constantName);
+ if (value == null) {
+ // hard fail as in the original implementation: default value constants must be resolvable
+ throw new IllegalArgumentException("Could not look up {@link #" + constantName
+ + "} for configuration " + type.getQualifiedName());
+ }
+ return value;
+ }
+ }
+ }
+ return renderContent(content);
+ }
+
+ private String lookupConstant(TypeElement type, String constantName) {
+ for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) {
+ if (field.getSimpleName().contentEquals(constantName) && field.getConstantValue() != null) {
+ return String.valueOf(field.getConstantValue());
+ }
+ }
+ return null;
+ }
+
+ private String renderContent(List<? extends DocTree> content) {
+ if (content == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (DocTree tree : content) {
+ sb.append(renderTree(tree));
+ }
+ return sb.toString().trim();
+ }
+
+ private String renderTree(DocTree tree) {
+ switch (tree.getKind()) {
+ case TEXT:
+ return ((TextTree) tree).getBody();
+ case LINK:
+ case LINK_PLAIN:
+ LinkTree link = (LinkTree) tree;
+ String ref = link.getReference() != null ? link.getReference().getSignature() : "";
+ return "{@link " + ref + "}";
+ case CODE:
+ return "{@code " + ((LiteralTree) tree).getBody().getBody() + "}";
+ case LITERAL:
+ return "{@literal " + ((LiteralTree) tree).getBody().getBody() + "}";
+ default:
+ return tree.toString();
+ }
+ }
+
+ private String getSince(TypeElement type, DocCommentTree docComment) {
+ String since = getSinceTag(docComment);
+ if (since == null && type != null) {
+ // fall back to the enclosing type's @since
+ since = getSinceTag(docTrees.getDocCommentTree(type));
+ }
+ return since;
+ }
+
+ private String getSinceTag(DocCommentTree docComment) {
+ if (docComment == null) {
+ return null;
+ }
+ for (DocTree tag : docComment.getBlockTags()) {
+ if (tag instanceof SinceTree) {
+ return renderContent(((SinceTree) tag).getBody());
+ }
+ }
+ return null;
+ }
+
+ private String getConfigurationType(String type) {
+ if (type != null) {
+ String linkPrefix = "{@link ";
+ String linkSuffix = "}";
+ if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
+ type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
+ }
+ String javaLangPackage = "java.lang.";
+ if (type.startsWith(javaLangPackage)) {
+ type = type.substring(javaLangPackage.length());
+ }
+ }
+ return nvl(type, "n/a");
+ }
+
+ private String getConfigurationSource(String source) {
+ if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
+ return "Session Configuration";
+ } else if ("{@link System#getProperty(String,String)}".equals(source)) {
+ return "Java System Properties";
+ } else {
+ return source;
+ }
+ }
+
+ private String cleanseJavadoc(String fullText) {
+ String[] lines = fullText.split("\n");
+ StringBuilder result = new StringBuilder();
+ for (String line : lines) {
+ if (!line.startsWith("@") && !line.trim().isEmpty()) {
+ result.append(line);
+ }
+ }
+ return cleanseTags(result.toString());
+ }
+
+ private String cleanseTags(String text) {
+ // {@code XXX} -> <code>XXX</code>
+ // {@link XXX} -> <code>XXX</code>
+ Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
+ Matcher matcher = pattern.matcher(text);
+ if (!matcher.find()) {
+ return text;
+ }
+ int prevEnd = 0;
+ StringBuilder result = new StringBuilder();
+ do {
+ result.append(text, prevEnd, matcher.start(1));
+ result.append("<code>");
+ result.append(matcher.group(2));
+ result.append("</code>");
+ prevEnd = matcher.end(1);
+ } while (matcher.find());
+ result.append(text, prevEnd, text.length());
+ return result.toString();
+ }
+
+ private String toYesNo(String value) {
+ return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
+ }
+
+ private String nvl(String string, String def) {
+ return string == null ? def : string;
+ }
+
+ /**
+ * Minimal {@link Option} implementation.
+ */
+ private static final class SimpleOption implements Option {
+ private final List<String> names;
+ private final int argumentCount;
+ private final String description;
+ private final String parameters;
+ private final java.util.function.Consumer<List<String>> processor;
+
+ SimpleOption(
+ List<String> names,
+ int argumentCount,
+ String description,
+ String parameters,
+ java.util.function.Consumer<List<String>> processor) {
+ this.names = names;
+ this.argumentCount = argumentCount;
+ this.description = description;
+ this.parameters = parameters;
+ this.processor = processor;
+ }
+
+ @Override
+ public int getArgumentCount() {
+ return argumentCount;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public Kind getKind() {
+ return Kind.STANDARD;
+ }
+
+ @Override
+ public List<String> getNames() {
+ return names;
+ }
+
+ @Override
+ public String getParameters() {
+ return parameters;
+ }
+
+ @Override
+ public boolean process(String option, List<String> arguments) {
+ processor.accept(arguments);
+ return true;
+ }
+ }
+}