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}