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