View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.tools;
20  
21  import javax.tools.DiagnosticCollector;
22  import javax.tools.DocumentationTool;
23  import javax.tools.JavaFileObject;
24  import javax.tools.StandardJavaFileManager;
25  import javax.tools.StandardLocation;
26  import javax.tools.ToolProvider;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.PrintWriter;
31  import java.io.Reader;
32  import java.io.Writer;
33  import java.net.URISyntaxException;
34  import java.net.URL;
35  import java.net.URLClassLoader;
36  import java.nio.charset.StandardCharsets;
37  import java.nio.file.Files;
38  import java.nio.file.Path;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Comparator;
42  import java.util.LinkedHashMap;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Properties;
46  import java.util.concurrent.Callable;
47  import java.util.stream.Collectors;
48  import java.util.stream.Stream;
49  
50  import org.apache.velocity.VelocityContext;
51  import org.apache.velocity.app.VelocityEngine;
52  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
53  import org.codehaus.plexus.util.io.CachingWriter;
54  import picocli.CommandLine;
55  
56  /**
57   * This tool is used both from <a href="https://github.com/apache/maven-resolver/blob/79d102b66235f33ad1e6134e18451ac3ee91b44a/maven-resolver-tools/pom.xml#L185">Resolver</a>
58   * as well as from <a href="https://github.com/apache/maven/blob/7aa3c8a37b091a5a86d3dae3a7d99ce910fd6caa/pom.xml#L1043">Maven</a>
59   * 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.
60   * It relies on javadoc with a custom doclet to extract the configuration keys from the source files.
61   * The doclet writes the discovered keys into an intermediate properties file, which is then read back and used to render the Velocity templates.
62   */
63  @CommandLine.Command(name = "docgen", description = "Configuration Documentation Generator")
64  public class CollectConfiguration implements Callable<Integer> {
65      public static void main(String[] args) {
66          new CommandLine(new CollectConfiguration()).execute(args);
67      }
68  
69      protected static final String KEY = "key";
70  
71      /**
72       * The metadata fields collected per configuration key and written to / read from the intermediate properties file.
73       */
74      protected static final List<String> FIELDS = List.of(
75              KEY,
76              "defaultValue",
77              "fqName",
78              "description",
79              "since",
80              "configurationSource",
81              "configurationType",
82              "supportRepoIdSuffix",
83              "deprecated");
84  
85      /**
86       * Javadoc block tag marking a constant field as a configuration key.
87       */
88      protected static final String CONFIGURATION_MARKER = "@configurationSource";
89  
90      /**
91       * Text marker used to pre-select the source files to feed the doclet when scanning Maven sources. Maven declares
92       * configuration keys via the {@code org.apache.maven.api.annotations.Config} annotation.
93       */
94      protected static final String MAVEN_CONFIGURATION_MARKER = "@Config";
95  
96      /**
97       * The mode of the generator, i.e. what kind of sources are being scanned.
98       */
99      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 }