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 java.io.BufferedWriter;
22  import java.io.IOException;
23  import java.io.PrintWriter;
24  import java.io.StringWriter;
25  import java.io.UncheckedIOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Properties;
33  import java.util.TreeMap;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  import java.util.spi.ToolProvider;
37  
38  import org.apache.velocity.VelocityContext;
39  import org.apache.velocity.app.VelocityEngine;
40  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
41  import org.jboss.forge.roaster.Roaster;
42  import org.jboss.forge.roaster.model.JavaDocCapable;
43  import org.jboss.forge.roaster.model.JavaDocTag;
44  import org.jboss.forge.roaster.model.JavaType;
45  import org.jboss.forge.roaster.model.source.FieldSource;
46  import org.jboss.forge.roaster.model.source.JavaClassSource;
47  
48  public class CollectConfiguration {
49      public static void main(String[] args) throws Exception {
50          Path start = Paths.get(args.length > 0 ? args[0] : ".");
51          Path output = Paths.get(args.length > 1 ? args[1] : "output");
52  
53          TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>();
54          Files.walk(start)
55                  .map(Path::toAbsolutePath)
56                  .filter(p -> p.getFileName().toString().endsWith(".java"))
57                  .filter(p -> p.toString().contains("/src/main/java/"))
58                  .filter(p -> !p.toString().endsWith("/module-info.java"))
59                  .forEach(p -> {
60                      JavaType<?> type = parse(p);
61                      if (type instanceof JavaClassSource javaClassSource) {
62                          javaClassSource.getFields().stream()
63                                  .filter(CollectConfiguration::hasConfigurationSource)
64                                  .forEach(f -> {
65                                      Map<String, String> constants = extractConstants(Paths.get(p.toString()
66                                              .replace("/src/main/java/", "/target/classes/")
67                                              .replace(".java", ".class")));
68  
69                                      String name = f.getName();
70                                      String key = constants.get(name);
71                                      String fqName = f.getOrigin().getCanonicalName() + "." + name;
72                                      String configurationType = getConfigurationType(f);
73                                      String defValue = getTag(f, "@configurationDefaultValue");
74                                      if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
75                                          // constant "lookup"
76                                          String lookupValue =
77                                                  constants.get(defValue.substring(8, defValue.length() - 1));
78                                          if (lookupValue == null) {
79                                              // currently we hard fail if javadoc cannot be looked up
80                                              // workaround: at cost of redundancy, but declare constants in situ for now
81                                              // (in same class)
82                                              throw new IllegalArgumentException(
83                                                      "Could not look up " + defValue + " for configuration " + fqName);
84                                          }
85                                          defValue = lookupValue;
86                                      }
87                                      if ("java.lang.Long".equals(configurationType)
88                                              && (defValue.endsWith("l") || defValue.endsWith("L"))) {
89                                          defValue = defValue.substring(0, defValue.length() - 1);
90                                      }
91                                      discoveredKeys.put(
92                                              key,
93                                              new ConfigurationKey(
94                                                      key,
95                                                      defValue,
96                                                      fqName,
97                                                      f.getJavaDoc().getText(),
98                                                      nvl(getSince(f), ""),
99                                                      getConfigurationSource(f),
100                                                     configurationType,
101                                                     toBoolean(getTag(f, "@configurationRepoIdSuffix"))));
102                                 });
103                     }
104                 });
105 
106         VelocityEngine velocityEngine = new VelocityEngine();
107         Properties properties = new Properties();
108         properties.setProperty("resource.loaders", "classpath");
109         properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
110         velocityEngine.init(properties);
111 
112         VelocityContext context = new VelocityContext();
113         context.put("keys", discoveredKeys.values());
114 
115         try (BufferedWriter fileWriter = Files.newBufferedWriter(output)) {
116             velocityEngine.getTemplate("page.vm").merge(context, fileWriter);
117         }
118     }
119 
120     private static JavaType<?> parse(Path path) {
121         try {
122             return Roaster.parse(path.toFile());
123         } catch (IOException e) {
124             throw new UncheckedIOException(e);
125         }
126     }
127 
128     private static boolean toBoolean(String value) {
129         return ("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value));
130     }
131 
132     /**
133      * Would be record, but... Velocity have no idea what it is nor how to handle it.
134      */
135     public static class ConfigurationKey {
136         private final String key;
137         private final String defaultValue;
138         private final String fqName;
139         private final String description;
140         private final String since;
141         private final String configurationSource;
142         private final String configurationType;
143         private final boolean supportRepoIdSuffix;
144 
145         @SuppressWarnings("checkstyle:parameternumber")
146         public ConfigurationKey(
147                 String key,
148                 String defaultValue,
149                 String fqName,
150                 String description,
151                 String since,
152                 String configurationSource,
153                 String configurationType,
154                 boolean supportRepoIdSuffix) {
155             this.key = key;
156             this.defaultValue = defaultValue;
157             this.fqName = fqName;
158             this.description = description;
159             this.since = since;
160             this.configurationSource = configurationSource;
161             this.configurationType = configurationType;
162             this.supportRepoIdSuffix = supportRepoIdSuffix;
163         }
164 
165         public String getKey() {
166             return key;
167         }
168 
169         public String getDefaultValue() {
170             return defaultValue;
171         }
172 
173         public String getFqName() {
174             return fqName;
175         }
176 
177         public String getDescription() {
178             return description;
179         }
180 
181         public String getSince() {
182             return since;
183         }
184 
185         public String getConfigurationSource() {
186             return configurationSource;
187         }
188 
189         public String getConfigurationType() {
190             return configurationType;
191         }
192 
193         public boolean isSupportRepoIdSuffix() {
194             return supportRepoIdSuffix;
195         }
196     }
197 
198     private static String nvl(String string, String def) {
199         return string == null ? def : string;
200     }
201 
202     private static boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
203         return getTag(javaDocCapable, "@configurationSource") != null;
204     }
205 
206     private static String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
207         String type = getTag(javaDocCapable, "@configurationType");
208         if (type != null) {
209             String linkPrefix = "{@link ";
210             String linkSuffix = "}";
211             if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
212                 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
213             }
214             String javaLangPackage = "java.lang.";
215             if (type.startsWith(javaLangPackage)) {
216                 type = type.substring(javaLangPackage.length());
217             }
218         }
219         return nvl(type, "n/a");
220     }
221 
222     private static String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
223         String source = getTag(javaDocCapable, "@configurationSource");
224         if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
225             return "Session Configuration";
226         } else if ("{@link System#getProperty(String,String)}".equals(source)) {
227             return "Java System Properties";
228         } else {
229             return source;
230         }
231     }
232 
233     private static String getSince(JavaDocCapable<?> javaDocCapable) {
234         List<JavaDocTag> tags;
235         if (javaDocCapable != null) {
236             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
237                 tags = fieldSource.getJavaDoc().getTags("@since");
238                 if (tags.isEmpty()) {
239                     return getSince(fieldSource.getOrigin());
240                 } else {
241                     return tags.get(0).getValue();
242                 }
243             } else if (javaDocCapable instanceof JavaClassSource classSource) {
244                 tags = classSource.getJavaDoc().getTags("@since");
245                 if (!tags.isEmpty()) {
246                     return tags.get(0).getValue();
247                 }
248             }
249         }
250         return null;
251     }
252 
253     private static String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
254         List<JavaDocTag> tags;
255         if (javaDocCapable != null) {
256             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
257                 tags = fieldSource.getJavaDoc().getTags(tagName);
258                 if (tags.isEmpty()) {
259                     return getTag(fieldSource.getOrigin(), tagName);
260                 } else {
261                     return tags.get(0).getValue();
262                 }
263             }
264         }
265         return null;
266     }
267 
268     private static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
269 
270     private static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
271 
272     /**
273      * Builds "constant table" for one single class.
274      *
275      * Limitations:
276      * - works only for single class (no inherited constants)
277      * - does not work for fields that are Enum.name()
278      * - more to come
279      */
280     private static Map<String, String> extractConstants(Path file) {
281         StringWriter out = new StringWriter();
282         JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
283         Map<String, String> result = new HashMap<>();
284         out.getBuffer().toString().lines().forEach(l -> {
285             Matcher matcher = CONSTANT_PATTERN.matcher(l);
286             if (matcher.matches()) {
287                 result.put(matcher.group(1), matcher.group(2));
288             }
289         });
290         return result;
291     }
292 }