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}