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}