1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.beans.PropertyDescriptor;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.module.ModuleDescriptor.Version;
26  import java.lang.reflect.Array;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.ParameterizedType;
30  import java.net.URI;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.BitSet;
37  import java.util.Collection;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.LinkedHashMap;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  import java.util.Optional;
45  import java.util.Set;
46  import java.util.TreeSet;
47  import java.util.regex.Pattern;
48  import java.util.stream.Collectors;
49  import java.util.stream.IntStream;
50  import java.util.stream.Stream;
51  
52  import javax.annotation.Nullable;
53  
54  import org.apache.commons.beanutils.PropertyUtils;
55  import org.apache.maven.doxia.macro.MacroExecutionException;
56  
57  import com.puppycrawl.tools.checkstyle.Checker;
58  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
59  import com.puppycrawl.tools.checkstyle.ModuleFactory;
60  import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
61  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
62  import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
63  import com.puppycrawl.tools.checkstyle.PropertyType;
64  import com.puppycrawl.tools.checkstyle.TreeWalker;
65  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
66  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
67  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
68  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
69  import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
70  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
71  import com.puppycrawl.tools.checkstyle.api.DetailNode;
72  import com.puppycrawl.tools.checkstyle.api.Filter;
73  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
74  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
75  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
76  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
77  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
78  import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
79  import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil;
80  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
81  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
82  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
83  
84  
85  
86  
87  public final class SiteUtil {
88  
89      
90      public static final String TOKENS = "tokens";
91      
92      public static final String JAVADOC_TOKENS = "javadocTokens";
93      
94      public static final String DOT = ".";
95      
96      public static final String COMMA = ",";
97      
98      public static final String WHITESPACE = " ";
99      
100     public static final String COMMA_SPACE = COMMA + WHITESPACE;
101     
102     public static final String TOKEN_TYPES = "TokenTypes";
103     
104     public static final String PATH_TO_TOKEN_TYPES =
105             "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
106     
107     public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
108             "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
109     
110     public static final String SINCE_VERSION = "Since version";
111     
112     public static final Pattern FINAL_CHECK = Pattern.compile("Check$");
113     
114     public static final String FILE_EXTENSIONS = "fileExtensions";
115     
116     public static final String CHARSET = "charset";
117     
118     private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
119     
120     private static final String CHECKS = "checks";
121     
122     private static final String NAMING = "naming";
123     
124     private static final String SRC = "src";
125     
126     private static final String TEMPLATE_FILE_EXTENSION = ".xml.template";
127 
128     
129     private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
130 
131     
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     
141     private static final Set<String> CHECK_PROPERTIES =
142             getProperties(AbstractCheck.class);
143 
144     
145     private static final Set<String> JAVADOC_CHECK_PROPERTIES =
146             getProperties(AbstractJavadocCheck.class);
147 
148     
149     private static final Set<String> FILESET_PROPERTIES =
150             getProperties(AbstractFileSetCheck.class);
151 
152     
153 
154 
155     private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
156 
157     
158 
159 
160     private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
161 
162     
163 
164 
165     private static final String API = "api";
166 
167     
168     private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
169         "SuppressWithNearbyCommentFilter.fileContents",
170         "SuppressionCommentFilter.fileContents"
171     );
172 
173     
174     private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
175         
176         "SuppressWarningsHolder.aliasList",
177         
178         HEADER_CHECK_HEADER,
179         REGEXP_HEADER_CHECK_HEADER,
180         
181         "RedundantModifierCheck.jdkVersion",
182         
183         "CustomImportOrderCheck.customImportOrderRules"
184     );
185 
186     
187     private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
188             new HashMap<>();
189 
190     
191     private static final String MAIN_FOLDER_PATH = Path.of(
192             SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
193 
194     
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 
208 
209     private SiteUtil() {
210     }
211 
212     
213 
214 
215 
216 
217 
218 
219     public static Set<String> getMessageKeys(Class<?> module)
220             throws MacroExecutionException {
221         final Set<Field> messageKeyFields = getCheckMessageKeysFields(module);
222         
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 
232 
233 
234 
235 
236 
237 
238 
239 
240 
241     private static Set<Field> getCheckMessageKeysFields(Class<?> module)
242             throws MacroExecutionException {
243         try {
244             final Set<Field> checkstyleMessages = new HashSet<>();
245 
246             
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             
256             final Class<?> superModule = module.getSuperclass();
257 
258             if (superModule != null) {
259                 checkstyleMessages.addAll(getCheckMessageKeysFields(superModule));
260             }
261 
262             
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 
284 
285 
286 
287 
288 
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                 
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 
310 
311 
312 
313 
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 
327 
328 
329 
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 
344 
345 
346 
347 
348 
349 
350 
351 
352     public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
353         return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces);
354     }
355 
356     
357 
358 
359 
360 
361 
362 
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 
376 
377 
378 
379 
380 
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 
398 
399 
400 
401 
402 
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         
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 
439 
440 
441 
442 
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 
457 
458 
459 
460 
461 
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 
472 
473 
474 
475 
476 
477 
478 
479 
480     public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
481                                                                 String moduleName, Path modulePath)
482             throws MacroExecutionException {
483         
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 
510 
511 
512 
513 
514 
515 
516 
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 
535 
536 
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 
555 
556 
557 
558 
559 
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 
595 
596 
597 
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 
614 
615 
616 
617 
618 
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 
631 
632 
633 
634 
635 
636     private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
637         return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
638     }
639 
640     
641 
642 
643 
644 
645 
646 
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 
692 
693 
694 
695 
696 
697 
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 
723 
724 
725 
726 
727 
728 
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 
768 
769 
770 
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 
788 
789 
790 
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 
816 
817 
818 
819 
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 
829 
830 
831 
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 
849 
850 
851 
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 
875 
876 
877 
878 
879 
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 
891 
892 
893 
894 
895 
896 
897 
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 
912 
913 
914 
915 
916 
917 
918 
919 
920 
921 
922 
923     
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 
979 
980 
981 
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 
1004 
1005 
1006 
1007 
1008     private static String removeSquareBrackets(String value) {
1009         return value
1010                 .replace("[", "")
1011                 .replace("]", "");
1012     }
1013 
1014     
1015 
1016 
1017 
1018 
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 
1039 
1040 
1041 
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 
1057 
1058 
1059 
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 
1072 
1073 
1074 
1075 
1076 
1077 
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 
1096 
1097 
1098 
1099 
1100 
1101 
1102 
1103 
1104     
1105     
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 
1155 
1156 
1157 
1158 
1159 
1160 
1161     
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 
1178 
1179 
1180 
1181 
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 
1195 
1196 
1197 
1198 
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 
1220 
1221 
1222 
1223 
1224 
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 
1247 
1248 
1249 
1250 
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 
1268 
1269 
1270 
1271 
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 
1288 
1289 
1290 
1291 
1292     public static String getModuleName(File file) {
1293         final String fullFileName = file.getName();
1294         return CommonUtil.getFileNameWithoutExtension(fullFileName);
1295     }
1296 
1297     
1298 
1299 
1300 
1301 
1302 
1303 
1304 
1305 
1306 
1307     
1308     
1309     
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                         
1348                         || isInHtmlElement && node.getFirstChild() == null
1349                             
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 
1386 
1387 
1388 
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 
1406 
1407 
1408 
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 
1425 
1426 
1427 
1428 
1429 
1430 
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 
1445 
1446 
1447 
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     
1463     private static final class DescriptionExtractor {
1464 
1465         
1466 
1467 
1468 
1469 
1470 
1471 
1472 
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             
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 }