001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.tools; 020 021import javax.tools.DiagnosticCollector; 022import javax.tools.DocumentationTool; 023import javax.tools.JavaFileObject; 024import javax.tools.StandardJavaFileManager; 025import javax.tools.StandardLocation; 026import javax.tools.ToolProvider; 027 028import java.io.File; 029import java.io.IOException; 030import java.io.PrintWriter; 031import java.io.Reader; 032import java.io.Writer; 033import java.net.URISyntaxException; 034import java.net.URL; 035import java.net.URLClassLoader; 036import java.nio.charset.StandardCharsets; 037import java.nio.file.Files; 038import java.nio.file.Path; 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.Comparator; 042import java.util.LinkedHashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.Properties; 046import java.util.concurrent.Callable; 047import java.util.stream.Collectors; 048import java.util.stream.Stream; 049 050import org.apache.velocity.VelocityContext; 051import org.apache.velocity.app.VelocityEngine; 052import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; 053import org.codehaus.plexus.util.io.CachingWriter; 054import picocli.CommandLine; 055 056/** 057 * This tool is used both from <a href="https://github.com/apache/maven-resolver/blob/79d102b66235f33ad1e6134e18451ac3ee91b44a/maven-resolver-tools/pom.xml#L185">Resolver</a> 058 * as well as from <a href="https://github.com/apache/maven/blob/7aa3c8a37b091a5a86d3dae3a7d99ce910fd6caa/pom.xml#L1043">Maven</a> 059 * to generate documentation for configuration keys. It scans the source files under a given root directory, collects the configuration keys declared in them and renders them into Velocity templates. 060 * It relies on javadoc with a custom doclet to extract the configuration keys from the source files. 061 * The doclet writes the discovered keys into an intermediate properties file, which is then read back and used to render the Velocity templates. 062 */ 063@CommandLine.Command(name = "docgen", description = "Configuration Documentation Generator") 064public class CollectConfiguration implements Callable<Integer> { 065 public static void main(String[] args) { 066 new CommandLine(new CollectConfiguration()).execute(args); 067 } 068 069 protected static final String KEY = "key"; 070 071 /** 072 * The metadata fields collected per configuration key and written to / read from the intermediate properties file. 073 */ 074 protected static final List<String> FIELDS = List.of( 075 KEY, 076 "defaultValue", 077 "fqName", 078 "description", 079 "since", 080 "configurationSource", 081 "configurationType", 082 "supportRepoIdSuffix", 083 "deprecated"); 084 085 /** 086 * Javadoc block tag marking a constant field as a configuration key. 087 */ 088 protected static final String CONFIGURATION_MARKER = "@configurationSource"; 089 090 /** 091 * Text marker used to pre-select the source files to feed the doclet when scanning Maven sources. Maven declares 092 * configuration keys via the {@code org.apache.maven.api.annotations.Config} annotation. 093 */ 094 protected static final String MAVEN_CONFIGURATION_MARKER = "@Config"; 095 096 /** 097 * The mode of the generator, i.e. what kind of sources are being scanned. 098 */ 099 public enum Mode { 100 maven, 101 resolver 102 } 103 104 @CommandLine.Option( 105 names = {"-m", "--mode"}, 106 arity = "1", 107 paramLabel = "mode", 108 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'") 109 protected Mode mode = Mode.resolver; 110 111 @CommandLine.Option( 112 names = {"-t", "--templates"}, 113 arity = "1", 114 split = ",", 115 paramLabel = "template", 116 description = "The template names to write content out without '.vm' extension") 117 protected List<String> templates; 118 119 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from") 120 protected Path rootDirectory; 121 122 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to") 123 protected Path outputDirectory; 124 125 @Override 126 public Integer call() { 127 try { 128 rootDirectory = rootDirectory.toAbsolutePath().normalize(); 129 outputDirectory = outputDirectory.toAbsolutePath().normalize(); 130 131 System.out.println("Processing sources from " + rootDirectory); 132 Path intermediateFile = Files.createTempFile("configuration-keys", ".properties"); 133 try { 134 runDoclet(intermediateFile); 135 List<Map<String, String>> discoveredKeys = readDiscoveredKeys(intermediateFile); 136 discoveredKeys.sort(Comparator.comparing(e -> e.get(KEY))); 137 render(discoveredKeys); 138 } finally { 139 Files.deleteIfExists(intermediateFile); 140 } 141 return 0; 142 } catch (Exception e) { 143 e.printStackTrace(System.err); 144 return 1; 145 } 146 } 147 148 /** 149 * Collects the source files under {@link #rootDirectory} and runs {@link ConfigurationCollectorDoclet} against them, 150 * having it write the discovered configuration keys into the given intermediate properties file. 151 */ 152 protected void runDoclet(Path intermediateFile) throws Exception { 153 // Only feed javadoc the files that actually declare configuration keys. This keeps the set of types that 154 // javadoc must resolve small, avoiding failures caused by unrelated sources referencing dependencies that 155 // are not on this module's classpath (e.g. gson, jetty). 156 String marker = mode == Mode.maven ? MAVEN_CONFIGURATION_MARKER : CONFIGURATION_MARKER; 157 List<File> sourceFiles; 158 try (Stream<Path> stream = Files.walk(rootDirectory)) { 159 sourceFiles = stream.map(Path::toAbsolutePath) 160 .filter(p -> p.getFileName().toString().endsWith(".java")) 161 .filter(p -> p.toString().contains("/src/main/java/")) 162 .filter(p -> !p.toString().endsWith("/module-info.java")) 163 .filter(p -> !p.toString().contains("/maven-resolver-tools/")) 164 .filter(p -> fileContains(p, marker)) 165 .map(Path::toFile) 166 .collect(Collectors.toList()); 167 } 168 if (sourceFiles.isEmpty()) { 169 throw new IllegalStateException( 170 "No Java sources declaring configuration keys found under " + rootDirectory); 171 } 172 173 DocumentationTool documentationTool = ToolProvider.getSystemDocumentationTool(); 174 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); 175 try (StandardJavaFileManager fileManager = 176 documentationTool.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) { 177 // Configure the classpath on the file manager (the -classpath option is not honored when a file manager 178 // is supplied to getTask()). Note that under exec:java the project dependencies are on the context 179 // classloader, not on the JVM's java.class.path. 180 fileManager.setLocation(StandardLocation.CLASS_PATH, resolveClasspath()); 181 182 Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFiles); 183 184 List<String> options = new ArrayList<>(Arrays.asList( 185 "--output", intermediateFile.toString(), "--mode", mode.name(), "-encoding", "UTF-8")); 186 187 Writer out = new PrintWriter(System.err); 188 DocumentationTool.DocumentationTask task = documentationTool.getTask( 189 out, fileManager, diagnostics, ConfigurationCollectorDoclet.class, options, compilationUnits); 190 boolean ok = task.call(); 191 out.flush(); 192 if (!ok) { 193 diagnostics.getDiagnostics().forEach(d -> System.err.println(d)); 194 throw new IllegalStateException("Javadoc doclet execution failed"); 195 } 196 } 197 } 198 199 private static boolean fileContains(Path path, String marker) { 200 try { 201 return Files.readString(path, StandardCharsets.UTF_8).contains(marker); 202 } catch (IOException e) { 203 return false; 204 } 205 } 206 207 /** 208 * Resolves the classpath to use for symbol resolution during the javadoc run. Under {@code exec:java} the project 209 * dependencies live on the context classloader (a {@link URLClassLoader}), not on the JVM's 210 * {@code java.class.path}, so both sources are combined. 211 */ 212 private static List<File> resolveClasspath() { 213 List<File> classpath = new ArrayList<>(); 214 for (ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl != null; cl = cl.getParent()) { 215 if (cl instanceof URLClassLoader) { 216 for (URL url : ((URLClassLoader) cl).getURLs()) { 217 if ("file".equals(url.getProtocol())) { 218 try { 219 classpath.add(new File(url.toURI())); 220 } catch (URISyntaxException e) { 221 classpath.add(new File(url.getPath())); 222 } 223 } 224 } 225 } 226 } 227 for (String element : System.getProperty("java.class.path").split(File.pathSeparator)) { 228 classpath.add(new File(element)); 229 } 230 return classpath; 231 } 232 233 /** 234 * Reads back the intermediate properties file produced by {@link ConfigurationCollectorDoclet} into the list of 235 * maps consumed by the Velocity templates. 236 */ 237 static List<Map<String, String>> readDiscoveredKeys(Path intermediateFile) throws Exception { 238 Properties properties = new Properties(); 239 try (Reader reader = Files.newBufferedReader(intermediateFile, StandardCharsets.UTF_8)) { 240 properties.load(reader); 241 } 242 int count = Integer.parseInt(properties.getProperty("keys.count", "0")); 243 List<Map<String, String>> discoveredKeys = new ArrayList<>(count); 244 for (int i = 0; i < count; i++) { 245 Map<String, String> entry = new LinkedHashMap<>(); 246 for (String field : FIELDS) { 247 entry.put(field, properties.getProperty("keys." + i + "." + field, "")); 248 } 249 discoveredKeys.add(entry); 250 } 251 return discoveredKeys; 252 } 253 254 protected void render(List<Map<String, String>> discoveredKeys) throws Exception { 255 Properties properties = new Properties(); 256 properties.setProperty("resource.loaders", "classpath"); 257 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName()); 258 VelocityEngine velocityEngine = new VelocityEngine(); 259 velocityEngine.init(properties); 260 261 VelocityContext context = new VelocityContext(); 262 context.put("keys", discoveredKeys); 263 264 for (String template : templates) { 265 Path output = outputDirectory.resolve(template); 266 Files.createDirectories(output.getParent()); 267 System.out.println("Writing out to " + output); 268 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) { 269 velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter); 270 } 271 } 272 } 273}