001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.meta;
021
022import java.io.File;
023import java.io.IOException;
024import java.lang.reflect.Field;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.util.ArrayList;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Set;
032import java.util.stream.Stream;
033
034import javax.xml.parsers.ParserConfigurationException;
035import javax.xml.transform.TransformerException;
036
037import org.apache.maven.doxia.macro.MacroExecutionException;
038
039import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
040import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
041import com.puppycrawl.tools.checkstyle.api.DetailNode;
042import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
043import com.puppycrawl.tools.checkstyle.site.ModuleJavadocParsingUtil;
044import com.puppycrawl.tools.checkstyle.site.SiteUtil;
045import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
046import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
047
048/** Class which handles all the metadata generation and writing calls. */
049public final class MetadataGeneratorUtil {
050
051    /** Stop instances being created. **/
052    private MetadataGeneratorUtil() {
053    }
054
055    /**
056     * Generate metadata from the module source files available in the input argument path.
057     *
058     * @param path arguments
059     * @param moduleFolders folders to check
060     * @throws IOException ioException
061     * @throws CheckstyleException checkstyleException
062     */
063    public static void generate(String path, String... moduleFolders)
064            throws IOException, CheckstyleException {
065        final List<File> modulesToProcess =
066            getTargetFiles(path, moduleFolders);
067
068        try {
069            for (File file : modulesToProcess) {
070                final String fileName = file.getName();
071
072                if (fileName.startsWith("Abstract")
073                    && !"AbstractClassNameCheck.java".equals(fileName)) {
074                    continue;
075                }
076
077                final ModuleDetails moduleDetails = getModuleDetails(file);
078                writeMetadataFile(moduleDetails);
079            }
080        }
081        catch (MacroExecutionException macroException) {
082            throw new CheckstyleException(macroException.getMessage(), macroException);
083        }
084    }
085
086    /**
087     * Generate metadata for the given file.
088     *
089     * @param file file to generate metadata for.
090     * @return module details.
091     * @throws MacroExecutionException macroExecutionException
092     */
093    private static ModuleDetails getModuleDetails(File file) throws MacroExecutionException {
094        final String moduleName = SiteUtil.FINAL_CHECK.matcher(SiteUtil.getModuleName(file))
095            .replaceAll("");
096
097        final Object instance = SiteUtil.getModuleInstance(moduleName);
098        final Class<?> clss = instance.getClass();
099        final String fullyQualifiedName = clss.getName();
100
101        final String parentModule = SiteUtil.getParentModule(clss);
102        final Object parentModuleInstance = SiteUtil.getModuleInstance(parentModule);
103        final String parentModuleString = parentModuleInstance.getClass().getName();
104
105        final ModuleType moduleType = getModuleType(moduleName);
106
107        final Set<String> messageKeys = SiteUtil.getMessageKeys(clss);
108
109        final String className = SiteUtil.getModuleName(file);
110        final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance);
111        final Map<String, DetailNode> propertiesJavadocs = SiteUtil
112                .getPropertiesJavadocs(properties, className, file.toPath());
113        final DetailNode moduleJavadoc = SiteUtil.getModuleJavadoc(className, file.toPath());
114        String description = ModuleJavadocParsingUtil
115            .getModuleDescription(moduleJavadoc);
116
117        final String notes = ModuleJavadocParsingUtil.getModuleNotes(moduleJavadoc);
118        if (!notes.isEmpty()) {
119            description = description + "\n\n " + notes;
120        }
121
122        final List<ModulePropertyDetails> propertiesDetails = getPropertiesDetails(
123            properties, propertiesJavadocs, className, instance);
124
125        return new ModuleDetails(moduleName, fullyQualifiedName, parentModuleString,
126            description, moduleType, propertiesDetails, new ArrayList<>(messageKeys));
127    }
128
129    /**
130     * Get module type(check/filter/filefilter) based on module name.
131     *
132     * @param moduleName module name.
133     * @return module type.
134     */
135    private static ModuleType getModuleType(String moduleName) {
136        final ModuleType result;
137        if (moduleName.endsWith("FileFilter")) {
138            result = ModuleType.FILEFILTER;
139        }
140        else if (moduleName.endsWith("Filter")) {
141            result = ModuleType.FILTER;
142        }
143        else {
144            result = ModuleType.CHECK;
145        }
146        return result;
147    }
148
149    /**
150     * Get property details for the given property - name, description, type, default value.
151     *
152     * @param properties properties of the module.
153     * @param javadocs javadocs of the module.
154     * @param className the class name of the module.
155     * @param instance the instance of the module.
156     * @return property details.
157     * @throws MacroExecutionException if an error occurs.
158     */
159    private static List<ModulePropertyDetails> getPropertiesDetails(
160            Set<String> properties, Map<String, DetailNode> javadocs,
161            String className, Object instance)
162            throws MacroExecutionException {
163        final List<ModulePropertyDetails> result = new ArrayList<>(properties.size());
164        for (String property : properties) {
165            final String description = getPropertyDescription(property,
166                    javadocs.get(property));
167            final Field propertyField = SiteUtil.getField(instance.getClass(), property);
168
169            final String type = SiteUtil.getType(propertyField, property, className, instance);
170
171            final String defaultValue = getPropertyDefaultValue(property, propertyField, instance,
172                    className);
173            final String validationType = getValidationType(property, propertyField);
174
175            result.add(new ModulePropertyDetails(property, type, defaultValue,
176                    validationType, description));
177        }
178        return result;
179    }
180
181    /**
182     * Get default value for the given property.
183     *
184     * @param property property name.
185     * @param field field.
186     * @param instance instance of the module.
187     * @param className class name of the module.
188     * @return default value.
189     * @throws MacroExecutionException if an error occurs.
190     */
191    private static String getPropertyDefaultValue(String property, Field field, Object instance,
192            String className) throws MacroExecutionException {
193        final String defaultValue;
194        if (SiteUtil.TOKENS.equals(property)) {
195            final AbstractCheck check = (AbstractCheck) instance;
196            final List<String> configurableTokens = SiteUtil
197                        .getDifference(check.getDefaultTokens(),
198                                check.getRequiredTokens())
199                        .stream()
200                        .map(TokenUtil::getTokenName)
201                        .toList();
202            defaultValue = String.join(SiteUtil.COMMA, configurableTokens);
203        }
204        else if (SiteUtil.JAVADOC_TOKENS.equals(property)) {
205            final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
206            final List<String> configurableTokens = SiteUtil
207                    .getDifference(check.getDefaultJavadocTokens(),
208                            check.getRequiredJavadocTokens())
209                    .stream()
210                    .map(JavadocUtil::getTokenName)
211                    .toList();
212            defaultValue = String.join(SiteUtil.COMMA, configurableTokens);
213        }
214        else {
215            defaultValue = SiteUtil.getDefaultValue(property, field, instance, className);
216        }
217        return defaultValue;
218    }
219
220    /**
221     * Write metadata file for the given module.
222     *
223     * @param moduleDetails module details.
224     * @throws CheckstyleException if an error occurs during writing metadata file.
225     */
226    private static void writeMetadataFile(ModuleDetails moduleDetails)
227            throws CheckstyleException {
228        try {
229            XmlMetaWriter.write(moduleDetails);
230        }
231        catch (TransformerException | ParserConfigurationException example) {
232            throw new CheckstyleException(
233                            "Failed to write metadata into XML file for module: "
234                                    + moduleDetails.getName(), example);
235        }
236    }
237
238    /**
239     * Get validation type for the given property.
240     *
241     * @param propertyName name of property.
242     * @param propertyField field of property.
243     * @return validation type.
244     */
245    private static String getValidationType(String propertyName, Field propertyField) {
246        final String validationType;
247        if (SiteUtil.TOKENS.equals(propertyName) || SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
248            validationType = "tokenSet";
249        }
250        else if (propertyField != null
251                && ModuleJavadocParsingUtil.isPropertySpecialTokenProp(propertyField)) {
252            validationType = "tokenTypesSet";
253        }
254        else {
255            validationType = null;
256        }
257        return validationType;
258    }
259
260    /**
261     * Get property description from property javadoc.
262     *
263     * @param property property name.
264     * @param propertyJavadoc property javadoc.
265     * @return property description.
266     */
267    private static String getPropertyDescription(String property, DetailNode propertyJavadoc) {
268        final String propertyDescription;
269        if (SiteUtil.TOKENS.equals(property)) {
270            propertyDescription = "tokens to check";
271        }
272        else if (SiteUtil.JAVADOC_TOKENS.equals(property)) {
273            propertyDescription = "javadoc tokens to check";
274        }
275        else {
276            final String firstJavadocParagraph = SiteUtil
277                    .getFirstParagraphFromJavadoc(propertyJavadoc);
278            final String setterToString = "Setter to ";
279
280            if (firstJavadocParagraph.contains(setterToString)) {
281                final String unprocessedPropertyDescription = firstJavadocParagraph
282                    .substring(setterToString.length());
283                final String firstLetterCapitalized = unprocessedPropertyDescription
284                        .substring(0, 1)
285                        .toUpperCase(Locale.ROOT);
286
287                propertyDescription = firstLetterCapitalized
288                    + unprocessedPropertyDescription.substring(1);
289            }
290            else {
291                propertyDescription = firstJavadocParagraph;
292            }
293
294        }
295        return propertyDescription;
296    }
297
298    /**
299     * Get files that represent modules.
300     *
301     * @param moduleFolders folders to check
302     * @param path          rootPath
303     * @return files for scrapping javadoc and generation of metadata files
304     * @throws IOException ioException
305     */
306    private static List<File> getTargetFiles(String path, String... moduleFolders)
307            throws IOException {
308        final List<File> validFiles = new ArrayList<>();
309        for (String folder : moduleFolders) {
310            try (Stream<Path> files = Files.walk(Path.of(path + "/" + folder))) {
311                validFiles.addAll(
312                        files.map(Path::toFile)
313                        .filter(file -> {
314                            final String fileName = file.getName();
315                            return fileName.endsWith("SuppressWarningsHolder.java")
316                                    || fileName.endsWith("Check.java")
317                                    || fileName.endsWith("Filter.java");
318                        })
319                        .toList());
320            }
321        }
322
323        return validFiles;
324    }
325}