001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.site;
021
022import java.beans.PropertyDescriptor;
023import java.io.File;
024import java.io.IOException;
025import java.lang.module.ModuleDescriptor.Version;
026import java.lang.reflect.Array;
027import java.lang.reflect.Field;
028import java.lang.reflect.InvocationTargetException;
029import java.lang.reflect.ParameterizedType;
030import java.net.URI;
031import java.nio.charset.StandardCharsets;
032import java.nio.file.Files;
033import java.nio.file.Path;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.BitSet;
037import java.util.Collection;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Locale;
043import java.util.Map;
044import java.util.Optional;
045import java.util.Set;
046import java.util.TreeSet;
047import java.util.regex.Pattern;
048import java.util.stream.Collectors;
049import java.util.stream.IntStream;
050import java.util.stream.Stream;
051
052import javax.annotation.Nullable;
053
054import org.apache.commons.beanutils.PropertyUtils;
055import org.apache.maven.doxia.macro.MacroExecutionException;
056
057import com.puppycrawl.tools.checkstyle.Checker;
058import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
059import com.puppycrawl.tools.checkstyle.ModuleFactory;
060import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
061import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
062import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
063import com.puppycrawl.tools.checkstyle.PropertyType;
064import com.puppycrawl.tools.checkstyle.TreeWalker;
065import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
066import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
067import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
068import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
069import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
070import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
071import com.puppycrawl.tools.checkstyle.api.DetailNode;
072import com.puppycrawl.tools.checkstyle.api.Filter;
073import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
074import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
075import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
076import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
077import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
078import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
079import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil;
080import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
081import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
082import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
083
084/**
085 * Utility class for site generation.
086 */
087public final class SiteUtil {
088
089    /** The string 'tokens'. */
090    public static final String TOKENS = "tokens";
091    /** The string 'javadocTokens'. */
092    public static final String JAVADOC_TOKENS = "javadocTokens";
093    /** The string '.'. */
094    public static final String DOT = ".";
095    /** The string ','. */
096    public static final String COMMA = ",";
097    /** The whitespace. */
098    public static final String WHITESPACE = " ";
099    /** The string ', '. */
100    public static final String COMMA_SPACE = COMMA + WHITESPACE;
101    /** The string 'TokenTypes'. */
102    public static final String TOKEN_TYPES = "TokenTypes";
103    /** The path to the TokenTypes.html file. */
104    public static final String PATH_TO_TOKEN_TYPES =
105            "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
106    /** The path to the JavadocTokenTypes.html file. */
107    public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
108            "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
109    /** The string of JavaDoc module marking 'Since version'. */
110    public static final String SINCE_VERSION = "Since version";
111    /** The 'Check' pattern at the end of string. */
112    public static final Pattern FINAL_CHECK = Pattern.compile("Check$");
113    /** The string 'fileExtensions'. */
114    public static final String FILE_EXTENSIONS = "fileExtensions";
115    /** The string 'charset'. */
116    public static final String CHARSET = "charset";
117    /** The url of the checkstyle website. */
118    private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
119    /** The string 'checks'. */
120    private static final String CHECKS = "checks";
121    /** The string 'naming'. */
122    private static final String NAMING = "naming";
123    /** The string 'src'. */
124    private static final String SRC = "src";
125    /** Template file extension. */
126    private static final String TEMPLATE_FILE_EXTENSION = ".xml.template";
127
128    /** Precompiled regex pattern to remove the "Setter to " prefix from strings. */
129    private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
130
131    /** Class name and their corresponding parent module name. */
132    private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
133        Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
134        Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
135        Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
136        Map.entry(Filter.class, Checker.class.getSimpleName()),
137        Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
138    );
139
140    /** Set of properties that every check has. */
141    private static final Set<String> CHECK_PROPERTIES =
142            getProperties(AbstractCheck.class);
143
144    /** Set of properties that every Javadoc check has. */
145    private static final Set<String> JAVADOC_CHECK_PROPERTIES =
146            getProperties(AbstractJavadocCheck.class);
147
148    /** Set of properties that every FileSet check has. */
149    private static final Set<String> FILESET_PROPERTIES =
150            getProperties(AbstractFileSetCheck.class);
151
152    /**
153     * Check and property name.
154     */
155    private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
156
157    /**
158     * Check and property name.
159     */
160    private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
161
162    /**
163     * The string 'api'.
164     */
165    private static final String API = "api";
166
167    /** Set of properties that are undocumented. Those are internal properties. */
168    private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
169        "SuppressWithNearbyCommentFilter.fileContents",
170        "SuppressionCommentFilter.fileContents"
171    );
172
173    /** Properties that can not be gathered from class instance. */
174    private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
175        // static field (all upper case)
176        "SuppressWarningsHolder.aliasList",
177        // loads string into memory similar to file
178        HEADER_CHECK_HEADER,
179        REGEXP_HEADER_CHECK_HEADER,
180        // property is an int, but we cut off excess to accommodate old versions
181        "RedundantModifierCheck.jdkVersion",
182        // until https://github.com/checkstyle/checkstyle/issues/13376
183        "CustomImportOrderCheck.customImportOrderRules"
184    );
185
186    /** Map of all superclasses properties and their javadocs. */
187    private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
188            new HashMap<>();
189
190    /** Path to main source code folder. */
191    private static final String MAIN_FOLDER_PATH = Path.of(
192            SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
193
194    /** List of files who are superclasses and contain certain properties that checks inherit. */
195    private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of(
196        Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"),
197        Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"),
198        Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"),
199        Path.of(MAIN_FOLDER_PATH, API, "AbstractFileSetCheck.java"),
200        Path.of(MAIN_FOLDER_PATH, API, "AbstractCheck.java"),
201        Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"),
202        Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"),
203        Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java")
204    );
205
206    /**
207     * Private utility constructor.
208     */
209    private SiteUtil() {
210    }
211
212    /**
213     * Get string values of the message keys from the given check class.
214     *
215     * @param module class to examine.
216     * @return a set of checkstyle's module message keys.
217     * @throws MacroExecutionException if extraction of message keys fails.
218     */
219    public static Set<String> getMessageKeys(Class<?> module)
220            throws MacroExecutionException {
221        final Set<Field> messageKeyFields = getCheckMessageKeysFields(module);
222        // We use a TreeSet to sort the message keys alphabetically
223        final Set<String> messageKeys = new TreeSet<>();
224        for (Field field : messageKeyFields) {
225            messageKeys.add(getFieldValue(field, module).toString());
226        }
227        return messageKeys;
228    }
229
230    /**
231     * Gets the check's messages keys.
232     *
233     * @param module class to examine.
234     * @return a set of checkstyle's module message fields.
235     * @throws MacroExecutionException if the attempt to read a protected class fails.
236     * @noinspection ChainOfInstanceofChecks
237     * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
238     *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
239     *
240     */
241    private static Set<Field> getCheckMessageKeysFields(Class<?> module)
242            throws MacroExecutionException {
243        try {
244            final Set<Field> checkstyleMessages = new HashSet<>();
245
246            // get all fields from current class
247            final Field[] fields = module.getDeclaredFields();
248
249            for (Field field : fields) {
250                if (field.getName().startsWith("MSG_")) {
251                    checkstyleMessages.add(field);
252                }
253            }
254
255            // deep scan class through hierarchy
256            final Class<?> superModule = module.getSuperclass();
257
258            if (superModule != null) {
259                checkstyleMessages.addAll(getCheckMessageKeysFields(superModule));
260            }
261
262            // special cases that require additional classes
263            if (module == RegexpMultilineCheck.class) {
264                checkstyleMessages.addAll(getCheckMessageKeysFields(Class
265                    .forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
266            }
267            else if (module == RegexpSinglelineCheck.class
268                    || module == RegexpSinglelineJavaCheck.class) {
269                checkstyleMessages.addAll(getCheckMessageKeysFields(Class
270                    .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
271            }
272
273            return checkstyleMessages;
274        }
275        catch (ClassNotFoundException exc) {
276            final String message = String.format(Locale.ROOT, "Couldn't find class: %s",
277                    module.getName());
278            throw new MacroExecutionException(message, exc);
279        }
280    }
281
282    /**
283     * Returns the value of the given field.
284     *
285     * @param field the field.
286     * @param instance the instance of the module.
287     * @return the value of the field.
288     * @throws MacroExecutionException if the value could not be retrieved.
289     */
290    public static Object getFieldValue(Field field, Object instance)
291            throws MacroExecutionException {
292        try {
293            Object fieldValue = null;
294
295            if (field != null) {
296                // required for package/private classes
297                field.trySetAccessible();
298                fieldValue = field.get(instance);
299            }
300
301            return fieldValue;
302        }
303        catch (IllegalAccessException exc) {
304            throw new MacroExecutionException("Couldn't get field value", exc);
305        }
306    }
307
308    /**
309     * Returns the instance of the module with the given name.
310     *
311     * @param moduleName the name of the module.
312     * @return the instance of the module.
313     * @throws MacroExecutionException if the module could not be created.
314     */
315    public static Object getModuleInstance(String moduleName) throws MacroExecutionException {
316        final ModuleFactory factory = getPackageObjectFactory();
317        try {
318            return factory.createModule(moduleName);
319        }
320        catch (CheckstyleException exc) {
321            throw new MacroExecutionException("Couldn't find class: " + moduleName, exc);
322        }
323    }
324
325    /**
326     * Returns the default PackageObjectFactory with the default package names.
327     *
328     * @return the default PackageObjectFactory.
329     * @throws MacroExecutionException if the PackageObjectFactory cannot be created.
330     */
331    private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException {
332        try {
333            final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader();
334            final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
335            return new PackageObjectFactory(packageNames, cl);
336        }
337        catch (CheckstyleException exc) {
338            throw new MacroExecutionException("Couldn't load checkstyle modules", exc);
339        }
340    }
341
342    /**
343     * Construct a string with a leading newline character and followed by
344     * the given amount of spaces. We use this method only to match indentation in
345     * regular xdocs and have minimal diff when parsing the templates.
346     * This method exists until
347     * <a href="https://github.com/checkstyle/checkstyle/issues/13426">13426</a>
348     *
349     * @param amountOfSpaces the amount of spaces to add after the newline.
350     * @return the constructed string.
351     */
352    public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
353        return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces);
354    }
355
356    /**
357     * Returns path to the template for the given module name or throws an exception if the
358     * template cannot be found.
359     *
360     * @param moduleName the module whose template we are looking for.
361     * @return path to the template.
362     * @throws MacroExecutionException if the template cannot be found.
363     */
364    public static Path getTemplatePath(String moduleName) throws MacroExecutionException {
365        final String fileNamePattern = ".*[\\\\/]"
366                + moduleName.toLowerCase(Locale.ROOT) + "\\..*";
367        return getXdocsTemplatesFilePaths()
368            .stream()
369            .filter(path -> path.toString().matches(fileNamePattern))
370            .findFirst()
371            .orElse(null);
372    }
373
374    /**
375     * Gets xdocs template file paths. These are files ending with .xml.template.
376     * This method will be changed to gather .xml once
377     * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
378     *
379     * @return a set of xdocs template file paths.
380     * @throws MacroExecutionException if an I/O error occurs.
381     */
382    public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException {
383        final Path directory = Path.of("src/site/xdoc");
384        try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
385                (path, attr) -> {
386                    return attr.isRegularFile()
387                            && path.toString().endsWith(TEMPLATE_FILE_EXTENSION);
388                })) {
389            return stream.collect(Collectors.toUnmodifiableSet());
390        }
391        catch (IOException ioException) {
392            throw new MacroExecutionException("Failed to find xdocs templates", ioException);
393        }
394    }
395
396    /**
397     * Returns the parent module name for the given module class. Returns either
398     * "TreeWalker" or "Checker". Returns null if the module class is null.
399     *
400     * @param moduleClass the module class.
401     * @return the parent module name as a string.
402     * @throws MacroExecutionException if the parent module cannot be found.
403     */
404    public static String getParentModule(Class<?> moduleClass)
405                throws MacroExecutionException {
406        String parentModuleName = "";
407        Class<?> parentClass = moduleClass.getSuperclass();
408
409        while (parentClass != null) {
410            parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass);
411            if (parentModuleName != null) {
412                break;
413            }
414            parentClass = parentClass.getSuperclass();
415        }
416
417        // If parent class is not found, check interfaces
418        if (parentModuleName == null || parentModuleName.isEmpty()) {
419            final Class<?>[] interfaces = moduleClass.getInterfaces();
420            for (Class<?> interfaceClass : interfaces) {
421                parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass);
422                if (parentModuleName != null) {
423                    break;
424                }
425            }
426        }
427
428        if (parentModuleName == null || parentModuleName.isEmpty()) {
429            final String message = String.format(Locale.ROOT,
430                    "Failed to find parent module for %s", moduleClass.getSimpleName());
431            throw new MacroExecutionException(message);
432        }
433
434        return parentModuleName;
435    }
436
437    /**
438     * Get a set of properties for the given class that should be documented.
439     *
440     * @param clss the class to get the properties for.
441     * @param instance the instance of the module.
442     * @return a set of properties for the given class.
443     */
444    public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
445        final Set<String> properties =
446                getProperties(clss).stream()
447                    .filter(prop -> {
448                        return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop);
449                    })
450                    .collect(Collectors.toCollection(HashSet::new));
451        properties.addAll(getNonExplicitProperties(instance, clss));
452        return new TreeSet<>(properties);
453    }
454
455    /**
456     * Gets the javadoc of module class.
457     *
458     * @param moduleClassName name of module class.
459     * @param modulePath module's path.
460     * @return javadoc of module.
461     * @throws MacroExecutionException if an error occurs during processing.
462     */
463    public static DetailNode getModuleJavadoc(String moduleClassName, Path modulePath)
464            throws MacroExecutionException {
465
466        processModule(moduleClassName, modulePath);
467        return JavadocScraperResultUtil.getModuleJavadocNode();
468    }
469
470    /**
471     * Get the javadocs of the properties of the module. If the property is not present in the
472     * module, then the javadoc of the property from the superclass(es) is used.
473     *
474     * @param properties the properties of the module.
475     * @param moduleName the name of the module.
476     * @param modulePath the module file path.
477     * @return the javadocs of the properties of the module.
478     * @throws MacroExecutionException if an error occurs during processing.
479     */
480    public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
481                                                                String moduleName, Path modulePath)
482            throws MacroExecutionException {
483        // lazy initialization
484        if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) {
485            processSuperclasses();
486        }
487
488        processModule(moduleName, modulePath);
489
490        final Map<String, DetailNode> unmodifiablePropertiesJavadocs =
491                JavadocScraperResultUtil.getPropertiesJavadocNode();
492        final Map<String, DetailNode> propertiesJavadocs =
493            new LinkedHashMap<>(unmodifiablePropertiesJavadocs);
494
495        properties.forEach(property -> {
496            final DetailNode superClassPropertyJavadoc =
497                    SUPER_CLASS_PROPERTIES_JAVADOCS.get(property);
498            if (superClassPropertyJavadoc != null) {
499                propertiesJavadocs.putIfAbsent(property, superClassPropertyJavadoc);
500            }
501        });
502
503        assertAllPropertySetterJavadocsAreFound(properties, moduleName, propertiesJavadocs);
504
505        return propertiesJavadocs;
506    }
507
508    /**
509     * Assert that each property has a corresponding setter javadoc that is not null.
510     * 'tokens' and 'javadocTokens' are excluded from this check, because their
511     * description is different from the description of the setter.
512     *
513     * @param properties the properties of the module.
514     * @param moduleName the name of the module.
515     * @param javadocs the javadocs of the properties of the module.
516     * @throws MacroExecutionException if an error occurs during processing.
517     */
518    private static void assertAllPropertySetterJavadocsAreFound(
519            Set<String> properties, String moduleName, Map<String, DetailNode> javadocs)
520            throws MacroExecutionException {
521        for (String property : properties) {
522            final boolean isDocumented = javadocs.containsKey(property)
523                   || SUPER_CLASS_PROPERTIES_JAVADOCS.containsKey(property)
524                   || TOKENS.equals(property) || JAVADOC_TOKENS.equals(property);
525            if (!isDocumented) {
526                throw new MacroExecutionException(String.format(Locale.ROOT,
527                   "%s: Missing documentation for property '%s'. Check superclasses.",
528                        moduleName, property));
529            }
530        }
531    }
532
533    /**
534     * Collect the properties setters javadocs of the superclasses.
535     *
536     * @throws MacroExecutionException if an error occurs during processing.
537     */
538    private static void processSuperclasses() throws MacroExecutionException {
539        for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) {
540            final Path fileNamePath = superclassPath.getFileName();
541            if (fileNamePath == null) {
542                throw new MacroExecutionException("Invalid superclass path: " + superclassPath);
543            }
544            final String superclassName = CommonUtil.getFileNameWithoutExtension(
545                fileNamePath.toString());
546            processModule(superclassName, superclassPath);
547            final Map<String, DetailNode> superclassPropertiesJavadocs =
548                JavadocScraperResultUtil.getPropertiesJavadocNode();
549            SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassPropertiesJavadocs);
550        }
551    }
552
553    /**
554     * Scrape the Javadocs of the class and its properties setters with
555     * ClassAndPropertiesSettersJavadocScraper.
556     *
557     * @param moduleName the name of the module.
558     * @param modulePath the module Path.
559     * @throws MacroExecutionException if an error occurs during processing.
560     */
561    private static void processModule(String moduleName, Path modulePath)
562            throws MacroExecutionException {
563        if (!Files.isRegularFile(modulePath)) {
564            final String message = String.format(Locale.ROOT,
565                    "File %s is not a file. Please check the 'modulePath' property.", modulePath);
566            throw new MacroExecutionException(message);
567        }
568        ClassAndPropertiesSettersJavadocScraper.initialize(moduleName);
569        final Checker checker = new Checker();
570        checker.setModuleClassLoader(Checker.class.getClassLoader());
571        final DefaultConfiguration scraperCheckConfig =
572                        new DefaultConfiguration(
573                                ClassAndPropertiesSettersJavadocScraper.class.getName());
574        final DefaultConfiguration defaultConfiguration =
575                new DefaultConfiguration("configuration");
576        final DefaultConfiguration treeWalkerConfig =
577                new DefaultConfiguration(TreeWalker.class.getName());
578        defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name());
579        defaultConfiguration.addChild(treeWalkerConfig);
580        treeWalkerConfig.addChild(scraperCheckConfig);
581        try {
582            checker.configure(defaultConfiguration);
583            final List<File> filesToProcess = List.of(modulePath.toFile());
584            checker.process(filesToProcess);
585            checker.destroy();
586        }
587        catch (CheckstyleException checkstyleException) {
588            final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
589            throw new MacroExecutionException(message, checkstyleException);
590        }
591    }
592
593    /**
594     * Get a set of properties for the given class.
595     *
596     * @param clss the class to get the properties for.
597     * @return a set of properties for the given class.
598     */
599    public static Set<String> getProperties(Class<?> clss) {
600        final Set<String> result = new TreeSet<>();
601        final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
602
603        for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
604            if (propertyDescriptor.getWriteMethod() != null) {
605                result.add(propertyDescriptor.getName());
606            }
607        }
608
609        return result;
610    }
611
612    /**
613     * Checks if the property is a global property. Global properties come from the base classes
614     * and are common to all checks. For example id, severity, tabWidth, etc.
615     *
616     * @param clss the class of the module.
617     * @param propertyName the name of the property.
618     * @return true if the property is a global property.
619     */
620    private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
621        return AbstractCheck.class.isAssignableFrom(clss)
622                    && CHECK_PROPERTIES.contains(propertyName)
623                || AbstractJavadocCheck.class.isAssignableFrom(clss)
624                    && JAVADOC_CHECK_PROPERTIES.contains(propertyName)
625                || AbstractFileSetCheck.class.isAssignableFrom(clss)
626                    && FILESET_PROPERTIES.contains(propertyName);
627    }
628
629    /**
630     * Checks if the property is supposed to be documented.
631     *
632     * @param clss the class of the module.
633     * @param propertyName the name of the property.
634     * @return true if the property is supposed to be documented.
635     */
636    private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
637        return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
638    }
639
640    /**
641     * Gets properties that are not explicitly captured but should be documented if
642     * certain conditions are met.
643     *
644     * @param instance the instance of the module.
645     * @param clss the class of the module.
646     * @return the non explicit properties.
647     */
648    private static Set<String> getNonExplicitProperties(
649            Object instance, Class<?> clss) {
650        final Set<String> result = new TreeSet<>();
651        if (AbstractCheck.class.isAssignableFrom(clss)) {
652            final AbstractCheck check = (AbstractCheck) instance;
653
654            final int[] acceptableTokens = check.getAcceptableTokens();
655            Arrays.sort(acceptableTokens);
656            final int[] defaultTokens = check.getDefaultTokens();
657            Arrays.sort(defaultTokens);
658            final int[] requiredTokens = check.getRequiredTokens();
659            Arrays.sort(requiredTokens);
660
661            if (!Arrays.equals(acceptableTokens, defaultTokens)
662                    || !Arrays.equals(acceptableTokens, requiredTokens)) {
663                result.add(TOKENS);
664            }
665        }
666
667        if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
668            final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
669            result.add("violateExecutionOnNonTightHtml");
670
671            final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
672            Arrays.sort(acceptableJavadocTokens);
673            final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
674            Arrays.sort(defaultJavadocTokens);
675            final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
676            Arrays.sort(requiredJavadocTokens);
677
678            if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
679                    || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
680                result.add(JAVADOC_TOKENS);
681            }
682        }
683
684        if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
685            result.add(FILE_EXTENSIONS);
686        }
687        return result;
688    }
689
690    /**
691     * Get the description of the property.
692     *
693     * @param propertyName the name of the property.
694     * @param javadoc the Javadoc of the property setter method.
695     * @param moduleName the name of the module.
696     * @return the description of the property.
697     * @throws MacroExecutionException if the description could not be extracted.
698     */
699    public static String getPropertyDescriptionForXdoc(
700            String propertyName, DetailNode javadoc, String moduleName)
701            throws MacroExecutionException {
702        final String description;
703        if (TOKENS.equals(propertyName)) {
704            description = "tokens to check";
705        }
706        else if (JAVADOC_TOKENS.equals(propertyName)) {
707            description = "javadoc tokens to check";
708        }
709        else {
710            final String descriptionString = SETTER_PATTERN.matcher(
711                    getDescriptionFromJavadocForXdoc(javadoc, moduleName))
712                    .replaceFirst("");
713
714            final String firstLetterCapitalized = descriptionString.substring(0, 1)
715                    .toUpperCase(Locale.ROOT);
716            description = firstLetterCapitalized + descriptionString.substring(1);
717        }
718        return description;
719    }
720
721    /**
722     * Get the since version of the property.
723     *
724     * @param moduleName the name of the module.
725     * @param moduleJavadoc the Javadoc of the module.
726     * @param propertyJavadoc the Javadoc of the property setter method.
727     * @return the since version of the property.
728     * @throws MacroExecutionException if the module since version could not be extracted.
729     */
730    public static String getPropertySinceVersion(String moduleName, DetailNode moduleJavadoc,
731                                                 DetailNode propertyJavadoc)
732            throws MacroExecutionException {
733        final String sinceVersion;
734
735        final Optional<String> specifiedPropertyVersionInPropertyJavadoc =
736            getPropertyVersionFromItsJavadoc(propertyJavadoc);
737
738        if (specifiedPropertyVersionInPropertyJavadoc.isPresent()) {
739            sinceVersion = specifiedPropertyVersionInPropertyJavadoc.get();
740        }
741        else {
742            final String moduleSince = getSinceVersionFromJavadoc(moduleJavadoc);
743
744            if (moduleSince == null) {
745                throw new MacroExecutionException(
746                        "Missing @since on module " + moduleName);
747            }
748
749            String propertySetterSince = null;
750            if (propertyJavadoc != null) {
751                propertySetterSince = getSinceVersionFromJavadoc(propertyJavadoc);
752            }
753
754            if (propertySetterSince != null
755                    && isVersionAtLeast(propertySetterSince, moduleSince)) {
756                sinceVersion = propertySetterSince;
757            }
758            else {
759                sinceVersion = moduleSince;
760            }
761        }
762
763        return sinceVersion;
764    }
765
766    /**
767     * Extract the property since version from its Javadoc.
768     *
769     * @param propertyJavadoc the property Javadoc to extract the since version from.
770     * @return the Optional of property version specified in its javadoc.
771     */
772    @Nullable
773    private static Optional<String> getPropertyVersionFromItsJavadoc(DetailNode propertyJavadoc) {
774        final Optional<DetailNode> propertyJavadocTag =
775            getPropertySinceJavadocTag(propertyJavadoc);
776
777        return propertyJavadocTag
778            .map(tag -> JavadocUtil.findFirstToken(tag, JavadocCommentsTokenTypes.DESCRIPTION))
779            .map(description -> {
780                return JavadocUtil.findFirstToken(description, JavadocCommentsTokenTypes.TEXT);
781            })
782            .map(DetailNode::getText)
783            .map(String::trim);
784    }
785
786    /**
787     * Find the propertySince Javadoc tag node in the given property Javadoc.
788     *
789     * @param javadoc the Javadoc to search.
790     * @return the Optional of propertySince Javadoc tag node or null if not found.
791     */
792    private static Optional<DetailNode> getPropertySinceJavadocTag(DetailNode javadoc) {
793        Optional<DetailNode> propertySinceJavadocTag = Optional.empty();
794        DetailNode child = javadoc.getFirstChild();
795
796        while (child != null) {
797            if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
798                final DetailNode customBlockTag = JavadocUtil.findFirstToken(
799                        child, JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG);
800
801                if (customBlockTag != null
802                        && "propertySince".equals(JavadocUtil.findFirstToken(
803                            customBlockTag, JavadocCommentsTokenTypes.TAG_NAME).getText())) {
804                    propertySinceJavadocTag = Optional.of(customBlockTag);
805                    break;
806                }
807            }
808            child = child.getNextSibling();
809        }
810
811        return propertySinceJavadocTag;
812    }
813
814    /**
815     * Gets all javadoc nodes of selected type.
816     *
817     * @param allNodes Nodes to choose from.
818     * @param neededType the Javadoc token type to select.
819     * @return the List of DetailNodes of selected type.
820     */
821    public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) {
822        return Arrays.stream(allNodes)
823            .filter(child -> child.getType() == neededType)
824            .toList();
825    }
826
827    /**
828     * Extract the since version from the Javadoc.
829     *
830     * @param javadoc the Javadoc to extract the since version from.
831     * @return the since version of the setter.
832     */
833    @Nullable
834    private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
835        final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
836        return Optional.ofNullable(sinceJavadocTag)
837            .map(tag -> JavadocUtil.findFirstToken(tag, JavadocCommentsTokenTypes.DESCRIPTION))
838            .map(description -> {
839                return JavadocUtil.findFirstToken(
840                        description, JavadocCommentsTokenTypes.TEXT);
841            })
842            .map(DetailNode::getText)
843            .map(String::trim)
844            .orElse(null);
845    }
846
847    /**
848     * Find the since Javadoc tag node in the given Javadoc.
849     *
850     * @param javadoc the Javadoc to search.
851     * @return the since Javadoc tag node or null if not found.
852     */
853    private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
854        DetailNode child = javadoc.getFirstChild();
855        DetailNode javadocTagWithSince = null;
856
857        while (child != null) {
858            if (child.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
859                final DetailNode sinceNode = JavadocUtil.findFirstToken(
860                        child, JavadocCommentsTokenTypes.SINCE_BLOCK_TAG);
861
862                if (sinceNode != null) {
863                    javadocTagWithSince = sinceNode;
864                    break;
865                }
866            }
867            child = child.getNextSibling();
868        }
869
870        return javadocTagWithSince;
871    }
872
873    /**
874     * Returns {@code true} if {@code actualVersion} ≥ {@code requiredVersion}.
875     * Both versions have any trailing "-SNAPSHOT" stripped before comparison.
876     *
877     * @param actualVersion   e.g. "8.3" or "8.3-SNAPSHOT"
878     * @param requiredVersion e.g. "8.3"
879     * @return {@code true} if actualVersion exists, and, numerically, is at least requiredVersion
880     */
881    private static boolean isVersionAtLeast(String actualVersion,
882                                            String requiredVersion) {
883        final Version actualVersionParsed = Version.parse(actualVersion);
884        final Version requiredVersionParsed = Version.parse(requiredVersion);
885
886        return actualVersionParsed.compareTo(requiredVersionParsed) >= 0;
887    }
888
889    /**
890     * Get the type of the property.
891     *
892     * @param field the field to get the type of.
893     * @param propertyName the name of the property.
894     * @param moduleName the name of the module.
895     * @param instance the instance of the module.
896     * @return the type of the property.
897     * @throws MacroExecutionException if an error occurs during getting the type.
898     */
899    public static String getType(Field field, String propertyName,
900                                 String moduleName, Object instance)
901            throws MacroExecutionException {
902        final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
903        return Optional.ofNullable(field)
904                .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
905                .filter(propertyType -> propertyType.value() != PropertyType.TOKEN_ARRAY)
906                .map(propertyType -> propertyType.value().getDescription())
907                .orElseGet(fieldClass::getTypeName);
908    }
909
910    /**
911     * Get the default value of the property.
912     *
913     * @param propertyName the name of the property.
914     * @param field the field to get the default value of.
915     * @param classInstance the instance of the class to get the default value of.
916     * @param moduleName the name of the module.
917     * @return the default value of the property.
918     * @throws MacroExecutionException if an error occurs during getting the default value.
919     * @noinspection IfStatementWithTooManyBranches
920     * @noinspectionreason IfStatementWithTooManyBranches - complex nature of getting properties
921     *      from XML files requires giant if/else statement
922     */
923    // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
924    public static String getDefaultValue(String propertyName, Field field,
925                                         Object classInstance, String moduleName)
926            throws MacroExecutionException {
927        final Object value = getFieldValue(field, classInstance);
928        final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance);
929        String result = null;
930
931        if (classInstance instanceof PropertyCacheFile) {
932            result = "null (no cache file)";
933        }
934        else if (fieldClass == boolean.class
935                || fieldClass == int.class
936                || fieldClass == URI.class
937                || fieldClass == String.class) {
938            if (value != null) {
939                result = value.toString();
940            }
941        }
942        else if (fieldClass == int[].class
943                || ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
944            result = getIntArrayPropertyValue(value);
945        }
946        else if (fieldClass == double[].class) {
947            result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", ""));
948        }
949        else if (fieldClass == String[].class) {
950            result = getStringArrayPropertyValue(value);
951        }
952        else if (fieldClass == Pattern.class) {
953            if (value != null) {
954                result = value.toString().replace("\n", "\\n").replace("\t", "\\t")
955                        .replace("\r", "\\r").replace("\f", "\\f");
956            }
957        }
958        else if (fieldClass == Pattern[].class) {
959            result = getPatternArrayPropertyValue(value);
960        }
961        else if (fieldClass.isEnum()) {
962            if (value != null) {
963                result = value.toString().toLowerCase(Locale.ENGLISH);
964            }
965        }
966        else if (fieldClass == AccessModifierOption[].class) {
967            result = removeSquareBrackets(Arrays.toString((Object[]) value));
968        }
969
970        if (result == null) {
971            result = "null";
972        }
973
974        return result;
975    }
976
977    /**
978     * Gets the name of the bean property's default value for the Pattern array class.
979     *
980     * @param fieldValue The bean property's value
981     * @return String form of property's default value
982     */
983    private static String getPatternArrayPropertyValue(Object fieldValue) {
984        Object value = fieldValue;
985        if (value instanceof Collection<?> collection) {
986            value = collection.stream()
987                    .map(Pattern.class::cast)
988                    .toArray(Pattern[]::new);
989        }
990
991        String result = "";
992        if (value != null && Array.getLength(value) > 0) {
993            result = removeSquareBrackets(
994                    Arrays.stream((Pattern[]) value)
995                    .map(Pattern::pattern)
996                    .collect(Collectors.joining(COMMA_SPACE)));
997        }
998
999        return result;
1000    }
1001
1002    /**
1003     * Removes square brackets [ and ] from the given string.
1004     *
1005     * @param value the string to remove square brackets from.
1006     * @return the string without square brackets.
1007     */
1008    private static String removeSquareBrackets(String value) {
1009        return value
1010                .replace("[", "")
1011                .replace("]", "");
1012    }
1013
1014    /**
1015     * Gets the name of the bean property's default value for the string array class.
1016     *
1017     * @param value The bean property's value
1018     * @return String form of property's default value
1019     */
1020    private static String getStringArrayPropertyValue(Object value) {
1021        final String result;
1022        if (value == null) {
1023            result = "";
1024        }
1025        else {
1026            try (Stream<?> valuesStream = getValuesStream(value)) {
1027                result = valuesStream
1028                    .map(String.class::cast)
1029                    .sorted()
1030                    .collect(Collectors.joining(COMMA_SPACE));
1031            }
1032        }
1033
1034        return result;
1035    }
1036
1037    /**
1038     * Generates a stream of values from the given value.
1039     *
1040     * @param value the value to generate the stream from.
1041     * @return the stream of values.
1042     */
1043    private static Stream<?> getValuesStream(Object value) {
1044        final Stream<?> valuesStream;
1045        if (value instanceof Collection<?> collection) {
1046            valuesStream = collection.stream();
1047        }
1048        else {
1049            final Object[] array = (Object[]) value;
1050            valuesStream = Arrays.stream(array);
1051        }
1052        return valuesStream;
1053    }
1054
1055    /**
1056     * Returns the name of the bean property's default value for the int array class.
1057     *
1058     * @param value The bean property's value.
1059     * @return String form of property's default value.
1060     */
1061    private static String getIntArrayPropertyValue(Object value) {
1062        try (IntStream stream = getIntStream(value)) {
1063            return stream
1064                    .mapToObj(TokenUtil::getTokenName)
1065                    .sorted()
1066                    .collect(Collectors.joining(COMMA_SPACE));
1067        }
1068    }
1069
1070    /**
1071     * Get the int stream from the given value.
1072     *
1073     * @param value the value to get the int stream from.
1074     * @return the int stream.
1075     * @noinspection ChainOfInstanceofChecks
1076     * @noinspectionreason ChainOfInstanceofChecks - We will deal with this at
1077     *                     <a href="https://github.com/checkstyle/checkstyle/issues/13500">13500</a>
1078     */
1079    private static IntStream getIntStream(Object value) {
1080        final IntStream stream;
1081        if (value instanceof Collection<?> collection) {
1082            stream = collection.stream()
1083                    .mapToInt(int.class::cast);
1084        }
1085        else if (value instanceof BitSet set) {
1086            stream = set.stream();
1087        }
1088        else {
1089            stream = Arrays.stream((int[]) value);
1090        }
1091        return stream;
1092    }
1093
1094    /**
1095     * Gets the class of the given field.
1096     *
1097     * @param field the field to get the class of.
1098     * @param propertyName the name of the property.
1099     * @param moduleName the name of the module.
1100     * @param instance the instance of the module.
1101     * @return the class of the field.
1102     * @throws MacroExecutionException if an error occurs during getting the class.
1103     */
1104    // -@cs[CyclomaticComplexity] Splitting would not make the code more readable
1105    // -@cs[ForbidWildcardAsReturnType] Implied by design to return different types
1106    public static Class<?> getFieldClass(Field field, String propertyName,
1107                                          String moduleName, Object instance)
1108            throws MacroExecutionException {
1109        Class<?> result = null;
1110
1111        if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1112                .contains(moduleName + DOT + propertyName)) {
1113            result = getPropertyClass(propertyName, instance);
1114        }
1115        if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1116            result = String[].class;
1117        }
1118        if (field != null && result == null) {
1119            result = field.getType();
1120        }
1121
1122        if (result == null) {
1123            throw new MacroExecutionException(
1124                    "Could not find field " + propertyName + " in class " + moduleName);
1125        }
1126
1127        if (field != null && (result == List.class || result == Set.class)) {
1128            final ParameterizedType type = (ParameterizedType) field.getGenericType();
1129            final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1130
1131            if (parameterClass == Integer.class) {
1132                result = int[].class;
1133            }
1134            else if (parameterClass == String.class) {
1135                result = String[].class;
1136            }
1137            else if (parameterClass == Pattern.class) {
1138                result = Pattern[].class;
1139            }
1140            else {
1141                final String message = "Unknown parameterized type: "
1142                        + parameterClass.getSimpleName();
1143                throw new MacroExecutionException(message);
1144            }
1145        }
1146        else if (result == BitSet.class) {
1147            result = int[].class;
1148        }
1149
1150        return result;
1151    }
1152
1153    /**
1154     * Gets the class of the given java property.
1155     *
1156     * @param propertyName the name of the property.
1157     * @param instance the instance of the module.
1158     * @return the class of the java property.
1159     * @throws MacroExecutionException if an error occurs during getting the class.
1160     */
1161    // -@cs[ForbidWildcardAsReturnType] Object is received as param, no prediction on type of field
1162    public static Class<?> getPropertyClass(String propertyName, Object instance)
1163            throws MacroExecutionException {
1164        final Class<?> result;
1165        try {
1166            final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1167                    propertyName);
1168            result = descriptor.getPropertyType();
1169        }
1170        catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1171            throw new MacroExecutionException(exc.getMessage(), exc);
1172        }
1173        return result;
1174    }
1175
1176    /**
1177     * Get the difference between two lists of tokens.
1178     *
1179     * @param tokens the list of tokens to remove from.
1180     * @param subtractions the tokens to remove.
1181     * @return the difference between the two lists.
1182     */
1183    public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1184        final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1185                .boxed()
1186                .collect(Collectors.toUnmodifiableSet());
1187        return Arrays.stream(tokens)
1188                .boxed()
1189                .filter(token -> !subtractionsSet.contains(token))
1190                .toList();
1191    }
1192
1193    /**
1194     * Gets the field with the given name from the given class.
1195     *
1196     * @param fieldClass the class to get the field from.
1197     * @param propertyName the name of the field.
1198     * @return the field we are looking for.
1199     */
1200    public static Field getField(Class<?> fieldClass, String propertyName) {
1201        Field result = null;
1202        Class<?> currentClass = fieldClass;
1203
1204        while (!Object.class.equals(currentClass)) {
1205            try {
1206                result = currentClass.getDeclaredField(propertyName);
1207                result.trySetAccessible();
1208                break;
1209            }
1210            catch (NoSuchFieldException ignored) {
1211                currentClass = currentClass.getSuperclass();
1212            }
1213        }
1214
1215        return result;
1216    }
1217
1218    /**
1219     * Constructs string with relative link to the provided document.
1220     *
1221     * @param moduleName the name of the module.
1222     * @param document the path of the document.
1223     * @return relative link to the document.
1224     * @throws MacroExecutionException if link to the document cannot be constructed.
1225     */
1226    public static String getLinkToDocument(String moduleName, String document)
1227            throws MacroExecutionException {
1228        final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll(""));
1229        if (templatePath == null) {
1230            throw new MacroExecutionException(
1231                    String.format(Locale.ROOT,
1232                            "Could not find template for %s", moduleName));
1233        }
1234        final Path templatePathParent = templatePath.getParent();
1235        if (templatePathParent == null) {
1236            throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1237        }
1238        return templatePathParent
1239                .relativize(Path.of(SRC, "site/xdoc", document))
1240                .toString()
1241                .replace(".xml", ".html")
1242                .replace('\\', '/');
1243    }
1244
1245    /**
1246     * Get all templates whose content contains properties macro.
1247     *
1248     * @return templates whose content contains properties macro.
1249     * @throws CheckstyleException if file could not be read.
1250     * @throws MacroExecutionException if template file is not found.
1251     */
1252    public static List<Path> getTemplatesThatContainPropertiesMacro()
1253            throws CheckstyleException, MacroExecutionException {
1254        final List<Path> result = new ArrayList<>();
1255        final Set<Path> templatesPaths = getXdocsTemplatesFilePaths();
1256        for (Path templatePath: templatesPaths) {
1257            final String content = getFileContents(templatePath);
1258            final String propertiesMacroDefinition = "<macro name=\"properties\"";
1259            if (content.contains(propertiesMacroDefinition)) {
1260                result.add(templatePath);
1261            }
1262        }
1263        return result;
1264    }
1265
1266    /**
1267     * Get file contents as string.
1268     *
1269     * @param pathToFile path to file.
1270     * @return file contents as string.
1271     * @throws CheckstyleException if file could not be read.
1272     */
1273    private static String getFileContents(Path pathToFile) throws CheckstyleException {
1274        final String content;
1275        try {
1276            content = Files.readString(pathToFile);
1277        }
1278        catch (IOException ioException) {
1279            final String message = String.format(Locale.ROOT, "Failed to read file: %s",
1280                    pathToFile);
1281            throw new CheckstyleException(message, ioException);
1282        }
1283        return content;
1284    }
1285
1286    /**
1287     * Get the module name from the file. The module name is the file name without the extension.
1288     *
1289     * @param file file to extract the module name from.
1290     * @return module name.
1291     */
1292    public static String getModuleName(File file) {
1293        final String fullFileName = file.getName();
1294        return CommonUtil.getFileNameWithoutExtension(fullFileName);
1295    }
1296
1297    /**
1298     * Extracts the description from the javadoc detail node. Performs a DFS traversal on the
1299     * detail node and extracts the text nodes. This description is additionally processed to
1300     * fit Xdoc format.
1301     *
1302     * @param javadoc the Javadoc to extract the description from.
1303     * @param moduleName the name of the module.
1304     * @return the description of the setter.
1305     * @throws MacroExecutionException if the description could not be extracted.
1306     */
1307    // -@cs[NPathComplexity] Splitting would not make the code more readable
1308    // -@cs[CyclomaticComplexity] Splitting would not make the code more readable.
1309    // -@cs[ExecutableStatementCount] Splitting would not make the code more readable.
1310    private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName)
1311            throws MacroExecutionException {
1312        boolean isInCodeLiteral = false;
1313        boolean isInHtmlElement = false;
1314        boolean isInHrefAttribute = false;
1315        final StringBuilder description = new StringBuilder(128);
1316        final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc);
1317        DetailNode node = descriptionNodes.get(0);
1318        final DetailNode endNode = descriptionNodes.get(descriptionNodes.size() - 1);
1319
1320        while (node != null) {
1321            if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME
1322                    && "href".equals(node.getText())) {
1323                isInHrefAttribute = true;
1324            }
1325            if (isInHrefAttribute && node.getType()
1326                     == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) {
1327                final String href = node.getText();
1328                if (href.contains(CHECKSTYLE_ORG_URL)) {
1329                    DescriptionExtractor.handleInternalLink(description, moduleName, href);
1330                }
1331                else {
1332                    description.append(href);
1333                }
1334
1335                isInHrefAttribute = false;
1336            }
1337            else {
1338                if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
1339                    isInHtmlElement = true;
1340                }
1341                if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE
1342                        && node.getParent().getType() == JavadocCommentsTokenTypes.HTML_TAG_END) {
1343                    description.append(node.getText());
1344                    isInHtmlElement = false;
1345                }
1346                if (node.getType() == JavadocCommentsTokenTypes.TEXT
1347                        // If a node has children, its text is not part of the description
1348                        || isInHtmlElement && node.getFirstChild() == null
1349                            // Some HTML elements span multiple lines, so we avoid the asterisk
1350                            && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) {
1351                    if (isInCodeLiteral) {
1352                        description.append(node.getText().trim());
1353                    }
1354                    else {
1355                        description.append(node.getText());
1356                    }
1357                }
1358                if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1359                        && node.getParent().getType()
1360                                  == JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
1361                    isInCodeLiteral = true;
1362                    description.append("<code>");
1363                }
1364                if (isInCodeLiteral
1365                        && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1366                    isInCodeLiteral = false;
1367                    description.append("</code>");
1368                }
1369
1370            }
1371
1372            DetailNode toVisit = node.getFirstChild();
1373            while (node != endNode && toVisit == null) {
1374                toVisit = node.getNextSibling();
1375                node = node.getParent();
1376            }
1377
1378            node = toVisit;
1379        }
1380
1381        return description.toString().trim();
1382    }
1383
1384    /**
1385     * Get 1st paragraph from the Javadoc with no additional processing.
1386     *
1387     * @param javadoc the Javadoc to extract first paragraph from.
1388     * @return first paragraph of javadoc.
1389     */
1390    public static String getFirstParagraphFromJavadoc(DetailNode javadoc) {
1391        final String result;
1392        final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc);
1393        if (firstParagraphNodes.isEmpty()) {
1394            result = "";
1395        }
1396        else {
1397            final DetailNode startNode = firstParagraphNodes.get(0);
1398            final DetailNode endNode = firstParagraphNodes.get(firstParagraphNodes.size() - 1);
1399            result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode);
1400        }
1401        return result;
1402    }
1403
1404    /**
1405     * Extracts first paragraph nodes from javadoc.
1406     *
1407     * @param javadoc the Javadoc to extract the description from.
1408     * @return the first paragraph nodes of the setter.
1409     */
1410    public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) {
1411        final List<DetailNode> firstParagraphNodes = new ArrayList<>();
1412
1413        for (DetailNode child = javadoc.getFirstChild();
1414                child != null; child = child.getNextSibling()) {
1415            if (isEndOfFirstJavadocParagraph(child)) {
1416                break;
1417            }
1418            firstParagraphNodes.add(child);
1419        }
1420        return firstParagraphNodes;
1421    }
1422
1423    /**
1424     * Determines if the given child index is the end of the first Javadoc paragraph. The end
1425     * of the description is defined as 4 consecutive nodes of type NEWLINE, LEADING_ASTERISK,
1426     * NEWLINE, LEADING_ASTERISK. This is an asterisk that is alone on a line. Just like the
1427     * one below this line.
1428     *
1429     * @param child the child to check.
1430     * @return true if the given child index is the end of the first javadoc paragraph.
1431     */
1432    public static boolean isEndOfFirstJavadocParagraph(DetailNode child) {
1433        final DetailNode nextSibling = child.getNextSibling();
1434        final DetailNode secondNextSibling = nextSibling.getNextSibling();
1435        final DetailNode thirdNextSibling = secondNextSibling.getNextSibling();
1436
1437        return child.getType() == JavadocCommentsTokenTypes.NEWLINE
1438                    && nextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
1439                    && secondNextSibling.getType() == JavadocCommentsTokenTypes.NEWLINE
1440                    && thirdNextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
1441    }
1442
1443    /**
1444     * Simplifies type name just to the name of the class, rather than entire package.
1445     *
1446     * @param fullTypeName full type name.
1447     * @return simplified type name, that is, name of the class.
1448     */
1449    public static String simplifyTypeName(String fullTypeName) {
1450        final int simplifiedStartIndex;
1451
1452        if (fullTypeName.contains("$")) {
1453            simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1;
1454        }
1455        else {
1456            simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1;
1457        }
1458
1459        return fullTypeName.substring(simplifiedStartIndex);
1460    }
1461
1462    /** Utility class for extracting description from a method's Javadoc. */
1463    private static final class DescriptionExtractor {
1464
1465        /**
1466         * Converts the href value to a relative link to the document and appends it to the
1467         * description.
1468         *
1469         * @param description the description to append the relative link to.
1470         * @param moduleName the name of the module.
1471         * @param value the href value.
1472         * @throws MacroExecutionException if the relative link could not be created.
1473         */
1474        private static void handleInternalLink(StringBuilder description,
1475                                               String moduleName, String value)
1476                throws MacroExecutionException {
1477            String href = value;
1478            href = href.replace(CHECKSTYLE_ORG_URL, "");
1479            // Remove first and last characters, they are always double quotes
1480            href = href.substring(1, href.length() - 1);
1481
1482            final String relativeHref = getLinkToDocument(moduleName, href);
1483            final char doubleQuote = '\"';
1484            description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1485        }
1486    }
1487}