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 java.io.BufferedWriter; 022import java.io.IOException; 023import java.io.PrintWriter; 024import java.io.StringWriter; 025import java.io.UncheckedIOException; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.Paths; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Properties; 033import java.util.TreeMap; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036import java.util.spi.ToolProvider; 037 038import org.apache.velocity.VelocityContext; 039import org.apache.velocity.app.VelocityEngine; 040import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; 041import org.jboss.forge.roaster.Roaster; 042import org.jboss.forge.roaster.model.JavaDoc; 043import org.jboss.forge.roaster.model.JavaDocCapable; 044import org.jboss.forge.roaster.model.JavaDocTag; 045import org.jboss.forge.roaster.model.JavaType; 046import org.jboss.forge.roaster.model.source.FieldSource; 047import org.jboss.forge.roaster.model.source.JavaClassSource; 048 049public class CollectConfiguration { 050 public static void main(String[] args) throws Exception { 051 Path start = Paths.get(args.length > 0 ? args[0] : "."); 052 Path output = Paths.get(args.length > 1 ? args[1] : "output"); 053 Path props = Paths.get(args.length > 2 ? args[2] : "props"); 054 Path yaml = Paths.get(args.length > 3 ? args[3] : "yaml"); 055 056 TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>(); 057 Files.walk(start) 058 .map(Path::toAbsolutePath) 059 .filter(p -> p.getFileName().toString().endsWith(".java")) 060 .filter(p -> p.toString().contains("/src/main/java/")) 061 .filter(p -> !p.toString().endsWith("/module-info.java")) 062 .forEach(p -> { 063 JavaType<?> type = parse(p); 064 if (type instanceof JavaClassSource javaClassSource) { 065 javaClassSource.getFields().stream() 066 .filter(CollectConfiguration::hasConfigurationSource) 067 .forEach(f -> { 068 Map<String, String> constants = extractConstants(Paths.get(p.toString() 069 .replace("/src/main/java/", "/target/classes/") 070 .replace(".java", ".class"))); 071 072 String name = f.getName(); 073 String key = constants.get(name); 074 String fqName = f.getOrigin().getCanonicalName() + "." + name; 075 String configurationType = getConfigurationType(f); 076 String defValue = getTag(f, "@configurationDefaultValue"); 077 if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) { 078 // constant "lookup" 079 String lookupValue = 080 constants.get(defValue.substring(8, defValue.length() - 1)); 081 if (lookupValue == null) { 082 // currently we hard fail if javadoc cannot be looked up 083 // workaround: at cost of redundancy, but declare constants in situ for now 084 // (in same class) 085 throw new IllegalArgumentException( 086 "Could not look up " + defValue + " for configuration " + fqName); 087 } 088 defValue = lookupValue; 089 } 090 if ("java.lang.Long".equals(configurationType) 091 && (defValue.endsWith("l") || defValue.endsWith("L"))) { 092 defValue = defValue.substring(0, defValue.length() - 1); 093 } 094 discoveredKeys.put( 095 key, 096 new ConfigurationKey( 097 key, 098 defValue, 099 fqName, 100 cleanseJavadoc(f), 101 nvl(getSince(f), ""), 102 getConfigurationSource(f), 103 configurationType, 104 toBoolean(getTag(f, "@configurationRepoIdSuffix")))); 105 }); 106 } 107 }); 108 109 VelocityEngine velocityEngine = new VelocityEngine(); 110 Properties properties = new Properties(); 111 properties.setProperty("resource.loaders", "classpath"); 112 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName()); 113 velocityEngine.init(properties); 114 115 VelocityContext context = new VelocityContext(); 116 context.put("keys", discoveredKeys.values()); 117 118 try (BufferedWriter fileWriter = Files.newBufferedWriter(output)) { 119 velocityEngine.getTemplate("page.vm").merge(context, fileWriter); 120 } 121 try (BufferedWriter fileWriter = Files.newBufferedWriter(props)) { 122 velocityEngine.getTemplate("props.vm").merge(context, fileWriter); 123 } 124 try (BufferedWriter fileWriter = Files.newBufferedWriter(yaml)) { 125 velocityEngine.getTemplate("yaml.vm").merge(context, fileWriter); 126 } 127 } 128 129 private static String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) { 130 JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc(); 131 String[] text = javaDoc.getFullText().split("\n"); 132 StringBuilder result = new StringBuilder(); 133 for (String line : text) { 134 if (!line.startsWith("@") && !line.trim().isEmpty()) { 135 result.append(line); 136 } 137 } 138 return cleanseTags(result.toString()); 139 } 140 141 private static String cleanseTags(String text) { 142 // {@code XXX} -> <pre>XXX</pre> 143 // {@link XXX} -> ??? pre for now 144 Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})"); 145 Matcher matcher = pattern.matcher(text); 146 if (!matcher.find()) { 147 return text; 148 } 149 int prevEnd = 0; 150 StringBuilder result = new StringBuilder(); 151 do { 152 result.append(text, prevEnd, matcher.start(1)); 153 result.append("<code>"); 154 result.append(matcher.group(2)); 155 result.append("</code>"); 156 prevEnd = matcher.end(1); 157 } while (matcher.find()); 158 result.append(text, prevEnd, text.length()); 159 return result.toString(); 160 } 161 162 private static JavaType<?> parse(Path path) { 163 try { 164 return Roaster.parse(path.toFile()); 165 } catch (IOException e) { 166 throw new UncheckedIOException(e); 167 } 168 } 169 170 private static boolean toBoolean(String value) { 171 return ("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)); 172 } 173 174 /** 175 * Would be record, but... Velocity have no idea what it is nor how to handle it. 176 */ 177 public static class ConfigurationKey { 178 private final String key; 179 private final String defaultValue; 180 private final String fqName; 181 private final String description; 182 private final String since; 183 private final String configurationSource; 184 private final String configurationType; 185 private final boolean supportRepoIdSuffix; 186 187 @SuppressWarnings("checkstyle:parameternumber") 188 public ConfigurationKey( 189 String key, 190 String defaultValue, 191 String fqName, 192 String description, 193 String since, 194 String configurationSource, 195 String configurationType, 196 boolean supportRepoIdSuffix) { 197 this.key = key; 198 this.defaultValue = defaultValue; 199 this.fqName = fqName; 200 this.description = description; 201 this.since = since; 202 this.configurationSource = configurationSource; 203 this.configurationType = configurationType; 204 this.supportRepoIdSuffix = supportRepoIdSuffix; 205 } 206 207 public String getKey() { 208 return key; 209 } 210 211 public String getDefaultValue() { 212 return defaultValue; 213 } 214 215 public String getFqName() { 216 return fqName; 217 } 218 219 public String getDescription() { 220 return description; 221 } 222 223 public String getSince() { 224 return since; 225 } 226 227 public String getConfigurationSource() { 228 return configurationSource; 229 } 230 231 public String getConfigurationType() { 232 return configurationType; 233 } 234 235 public boolean isSupportRepoIdSuffix() { 236 return supportRepoIdSuffix; 237 } 238 } 239 240 private static String nvl(String string, String def) { 241 return string == null ? def : string; 242 } 243 244 private static boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) { 245 return getTag(javaDocCapable, "@configurationSource") != null; 246 } 247 248 private static String getConfigurationType(JavaDocCapable<?> javaDocCapable) { 249 String type = getTag(javaDocCapable, "@configurationType"); 250 if (type != null) { 251 String linkPrefix = "{@link "; 252 String linkSuffix = "}"; 253 if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) { 254 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length()); 255 } 256 String javaLangPackage = "java.lang."; 257 if (type.startsWith(javaLangPackage)) { 258 type = type.substring(javaLangPackage.length()); 259 } 260 } 261 return nvl(type, "n/a"); 262 } 263 264 private static String getConfigurationSource(JavaDocCapable<?> javaDocCapable) { 265 String source = getTag(javaDocCapable, "@configurationSource"); 266 if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) { 267 return "Session Configuration"; 268 } else if ("{@link System#getProperty(String,String)}".equals(source)) { 269 return "Java System Properties"; 270 } else { 271 return source; 272 } 273 } 274 275 private static String getSince(JavaDocCapable<?> javaDocCapable) { 276 List<JavaDocTag> tags; 277 if (javaDocCapable != null) { 278 if (javaDocCapable instanceof FieldSource<?> fieldSource) { 279 tags = fieldSource.getJavaDoc().getTags("@since"); 280 if (tags.isEmpty()) { 281 return getSince(fieldSource.getOrigin()); 282 } else { 283 return tags.get(0).getValue(); 284 } 285 } else if (javaDocCapable instanceof JavaClassSource classSource) { 286 tags = classSource.getJavaDoc().getTags("@since"); 287 if (!tags.isEmpty()) { 288 return tags.get(0).getValue(); 289 } 290 } 291 } 292 return null; 293 } 294 295 private static String getTag(JavaDocCapable<?> javaDocCapable, String tagName) { 296 List<JavaDocTag> tags; 297 if (javaDocCapable != null) { 298 if (javaDocCapable instanceof FieldSource<?> fieldSource) { 299 tags = fieldSource.getJavaDoc().getTags(tagName); 300 if (tags.isEmpty()) { 301 return getTag(fieldSource.getOrigin(), tagName); 302 } else { 303 return tags.get(0).getValue(); 304 } 305 } 306 } 307 return null; 308 } 309 310 private static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);"); 311 312 private static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow(); 313 314 /** 315 * Builds "constant table" for one single class. 316 * 317 * Limitations: 318 * - works only for single class (no inherited constants) 319 * - does not work for fields that are Enum.name() 320 * - more to come 321 */ 322 private static Map<String, String> extractConstants(Path file) { 323 StringWriter out = new StringWriter(); 324 JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString()); 325 Map<String, String> result = new HashMap<>(); 326 out.getBuffer().toString().lines().forEach(l -> { 327 Matcher matcher = CONSTANT_PATTERN.matcher(l); 328 if (matcher.matches()) { 329 result.put(matcher.group(1), matcher.group(2)); 330 } 331 }); 332 return result; 333 } 334}