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