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.JavaDocCapable; 043import org.jboss.forge.roaster.model.JavaDocTag; 044import org.jboss.forge.roaster.model.JavaType; 045import org.jboss.forge.roaster.model.source.FieldSource; 046import org.jboss.forge.roaster.model.source.JavaClassSource; 047 048public class CollectConfiguration { 049 public static void main(String[] args) throws Exception { 050 Path start = Paths.get(args.length > 0 ? args[0] : "."); 051 Path output = Paths.get(args.length > 1 ? args[1] : "output"); 052 053 TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>(); 054 Files.walk(start) 055 .map(Path::toAbsolutePath) 056 .filter(p -> p.getFileName().toString().endsWith(".java")) 057 .filter(p -> p.toString().contains("/src/main/java/")) 058 .filter(p -> !p.toString().endsWith("/module-info.java")) 059 .forEach(p -> { 060 JavaType<?> type = parse(p); 061 if (type instanceof JavaClassSource javaClassSource) { 062 javaClassSource.getFields().stream() 063 .filter(CollectConfiguration::hasConfigurationSource) 064 .forEach(f -> { 065 Map<String, String> constants = extractConstants(Paths.get(p.toString() 066 .replace("/src/main/java/", "/target/classes/") 067 .replace(".java", ".class"))); 068 069 String name = f.getName(); 070 String key = constants.get(name); 071 String fqName = f.getOrigin().getCanonicalName() + "." + name; 072 String configurationType = getConfigurationType(f); 073 String defValue = getTag(f, "@configurationDefaultValue"); 074 if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) { 075 // constant "lookup" 076 String lookupValue = 077 constants.get(defValue.substring(8, defValue.length() - 1)); 078 if (lookupValue == null) { 079 // currently we hard fail if javadoc cannot be looked up 080 // workaround: at cost of redundancy, but declare constants in situ for now 081 // (in same class) 082 throw new IllegalArgumentException( 083 "Could not look up " + defValue + " for configuration " + fqName); 084 } 085 defValue = lookupValue; 086 } 087 if ("java.lang.Long".equals(configurationType) 088 && (defValue.endsWith("l") || defValue.endsWith("L"))) { 089 defValue = defValue.substring(0, defValue.length() - 1); 090 } 091 discoveredKeys.put( 092 key, 093 new ConfigurationKey( 094 key, 095 defValue, 096 fqName, 097 f.getJavaDoc().getText(), 098 nvl(getSince(f), ""), 099 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}