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 .forEach(p -> { 059 JavaType<?> type = parse(p); 060 if (type instanceof JavaClassSource javaClassSource) { 061 javaClassSource.getFields().stream() 062 .filter(CollectConfiguration::hasConfigurationSource) 063 .forEach(f -> { 064 Map<String, String> constants = extractConstants(Paths.get(p.toString() 065 .replace("/src/main/java/", "/target/classes/") 066 .replace(".java", ".class"))); 067 068 String name = f.getName(); 069 String key = constants.get(name); 070 String fqName = f.getOrigin().getCanonicalName() + "." + name; 071 String configurationType = getConfigurationType(f); 072 String defValue = getTag(f, "@configurationDefaultValue"); 073 if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) { 074 // constant "lookup" 075 String lookupValue = 076 constants.get(defValue.substring(8, defValue.length() - 1)); 077 if (lookupValue == null) { 078 // currently we hard fail if javadoc cannot be looked up 079 // workaround: at cost of redundancy, but declare constants in situ for now 080 // (in same class) 081 throw new IllegalArgumentException( 082 "Could not look up " + defValue + " for configuration " + fqName); 083 } 084 defValue = lookupValue; 085 } 086 if ("java.lang.Long".equals(configurationType) 087 && (defValue.endsWith("l") || defValue.endsWith("L"))) { 088 defValue = defValue.substring(0, defValue.length() - 1); 089 } 090 discoveredKeys.put( 091 key, 092 new ConfigurationKey( 093 key, 094 defValue, 095 fqName, 096 f.getJavaDoc().getText(), 097 nvl(getSince(f), ""), 098 getConfigurationSource(f), 099 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}