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.io.IOException;
23 import java.nio.file.FileVisitResult;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.SimpleFileVisitor;
27 import java.nio.file.attribute.BasicFileAttributes;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.TreeMap;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 import javax.annotation.Nullable;
37
38 import org.apache.maven.doxia.macro.AbstractMacro;
39 import org.apache.maven.doxia.macro.Macro;
40 import org.apache.maven.doxia.macro.MacroExecutionException;
41 import org.apache.maven.doxia.macro.MacroRequest;
42 import org.apache.maven.doxia.sink.Sink;
43 import org.codehaus.plexus.component.annotations.Component;
44
45 import com.puppycrawl.tools.checkstyle.api.DetailNode;
46 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 @Component(role = Macro.class, hint = "allCheckSummaries")
67 public class AllCheckSummaries extends AbstractMacro {
68
69
70 public static final int CAPACITY = 3000;
71
72
73
74
75
76
77 private static final Pattern TAG_PATTERN =
78 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>");
79
80
81 private static final String WHITESPACE_REGEX = "\\s+";
82
83
84
85
86
87 private static final Pattern SPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
88
89
90
91
92 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)");
93
94
95
96
97
98
99 private static final Pattern HREF_PATTERN =
100 Pattern.compile("href\\s*=\\s*['\"]([^'\"]*)['\"]",
101 Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
102
103
104 private static final String SRC = "src";
105
106
107 private static final String CHECKS = "checks";
108
109
110 private static final Path JAVA_CHECKS_ROOT = Path.of(
111 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS);
112
113
114 private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS);
115
116
117 private static final String XML_EXTENSION = ".xml";
118
119
120 private static final String HTML_EXTENSION = ".html";
121
122
123 private static final String TD_TAG = "<td>";
124
125
126 private static final String TD_CLOSE_TAG = "</td>";
127
128
129 private static final String MISC_PACKAGE = "misc";
130
131
132 private static final String ANNOTATION_PACKAGE = "annotation";
133
134
135 private static final String TABLE_CLOSE_TAG = "</table>";
136
137
138 private static final String DIV_CLOSE_TAG = "</div>";
139
140
141 private static final String SECTION_CLOSE_TAG = "</section>";
142
143
144 private static final String DIV_WRAPPER_TAG = "<div class=\"wrapper\">";
145
146
147 private static final String TABLE_OPEN_TAG = "<table>";
148
149
150 private static final String ANCHOR_SEPARATOR = "#";
151
152
153 private static final String FIRST_CAPTURE_GROUP = "$1";
154
155
156 private static final int MAX_LINE_WIDTH_TOTAL = 100;
157
158
159 private static final int INDENT_WIDTH = 14;
160
161
162 private static final int MAX_CONTENT_WIDTH = MAX_LINE_WIDTH_TOTAL - INDENT_WIDTH;
163
164
165 private static final String CLOSING_ANCHOR_TAG = "</a>";
166
167
168 private static final Pattern CODE_SPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX
169 + "(" + CLOSING_ANCHOR_TAG.substring(0, 2) + "code>)");
170
171 @Override
172 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
173 final String packageFilter = (String) request.getParameter("package");
174
175 final Map<String, String> xmlHrefMap = buildXmlHtmlMap();
176 final Map<String, CheckInfo> infos = new TreeMap<>();
177
178 processCheckFiles(infos, xmlHrefMap, packageFilter);
179
180 final StringBuilder normalRows = new StringBuilder(4096);
181 final StringBuilder holderRows = new StringBuilder(512);
182
183 buildTableRows(infos, normalRows, holderRows);
184
185 sink.rawText(normalRows.toString());
186 if (packageFilter == null && !holderRows.isEmpty()) {
187 appendHolderSection(sink, holderRows);
188 }
189 else if (packageFilter != null && !holderRows.isEmpty()) {
190 appendFilteredHolderSection(sink, holderRows, packageFilter);
191 }
192 }
193
194
195
196
197
198
199
200
201
202 private static void processCheckFiles(Map<String, CheckInfo> infos,
203 Map<String, String> xmlHrefMap,
204 String packageFilter)
205 throws MacroExecutionException {
206 try {
207 final List<Path> checkFiles = new ArrayList<>();
208 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() {
209 @Override
210 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
211 if (isCheckOrHolderFile(file)) {
212 checkFiles.add(file);
213 }
214 return FileVisitResult.CONTINUE;
215 }
216 });
217
218 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter));
219 }
220 catch (IOException | IllegalStateException exception) {
221 throw new MacroExecutionException("Failed to discover checks", exception);
222 }
223 }
224
225
226
227
228
229
230
231 private static boolean isCheckOrHolderFile(Path path) {
232 final Path fileName = path.getFileName();
233 return fileName != null
234 && (fileName.toString().endsWith("Check.java")
235 || fileName.toString().endsWith("Holder.java"))
236 && Files.isRegularFile(path);
237 }
238
239
240
241
242
243
244
245 private static boolean isHolder(String moduleName) {
246 return moduleName.endsWith("Holder");
247 }
248
249
250
251
252
253
254
255
256
257
258 private static void processCheckFile(Path path, Map<String, CheckInfo> infos,
259 Map<String, String> xmlHrefMap,
260 String packageFilter) {
261 try {
262 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString());
263 final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path);
264 if (javadoc != null) {
265 String description = getDescriptionIfPresent(javadoc);
266 if (description != null) {
267 description = sanitizeAnchorUrls(description);
268
269 final String[] moduleInfo = determineModuleInfo(path, moduleName);
270 final String packageName = moduleInfo[1];
271 if (packageFilter == null || packageFilter.equals(packageName)) {
272 final String simpleName = moduleInfo[0];
273 final String summary = sanitizeAndFirstSentence(description);
274 final String href = resolveHref(xmlHrefMap, packageName, simpleName,
275 packageFilter);
276 infos.put(simpleName, new CheckInfo(simpleName, href, summary));
277 }
278 }
279 }
280
281 }
282 catch (MacroExecutionException exceptionThrown) {
283 throw new IllegalArgumentException(exceptionThrown);
284 }
285 }
286
287
288
289
290
291
292
293
294 private static String[] determineModuleInfo(Path path, String moduleName) {
295 String packageName = extractCategoryFromJavaPath(path);
296
297 if ("indentation".equals(packageName)) {
298 packageName = MISC_PACKAGE;
299 }
300 if (isHolder(moduleName)) {
301 packageName = ANNOTATION_PACKAGE;
302 }
303 final String simpleName;
304 if (isHolder(moduleName)) {
305 simpleName = moduleName;
306 }
307 else {
308 simpleName = moduleName.substring(0, moduleName.length() - "Check".length());
309 }
310
311 return new String[] {simpleName, packageName};
312 }
313
314
315
316
317
318
319
320 @Nullable
321 private static String getDescriptionIfPresent(DetailNode javadoc) {
322 String result = null;
323 if (javadoc != null) {
324 try {
325 if (ModuleJavadocParsingUtil
326 .getModuleSinceVersionTagStartNode(javadoc) != null) {
327 final String desc = ModuleJavadocParsingUtil.getModuleDescription(javadoc);
328 if (!desc.isEmpty()) {
329 result = desc;
330 }
331 }
332 }
333 catch (IllegalStateException exception) {
334 result = null;
335 }
336 }
337 return result;
338 }
339
340
341
342
343
344
345
346
347 private static void buildTableRows(Map<String, CheckInfo> infos,
348 StringBuilder normalRows,
349 StringBuilder holderRows) {
350 for (CheckInfo info : infos.values()) {
351 final String row = buildTableRow(info);
352 if (isHolder(info.simpleName())) {
353 holderRows.append(row);
354 }
355 else {
356 normalRows.append(row);
357 }
358 }
359 removeLeadingNewline(normalRows);
360 removeLeadingNewline(holderRows);
361 }
362
363
364
365
366
367
368
369 private static String buildTableRow(CheckInfo info) {
370 final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10;
371 final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12;
372 final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
373 final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16;
374
375 final String cleanSummary = sanitizeAnchorUrls(info.summary());
376
377 return ind10 + "<tr>"
378 + ind12 + TD_TAG
379 + ind14
380 + "<a href=\""
381 + info.link()
382 + "\">"
383 + ind16 + info.simpleName()
384 + ind14 + CLOSING_ANCHOR_TAG
385 + ind12 + TD_CLOSE_TAG
386 + ind12 + TD_TAG
387 + ind14 + wrapSummary(cleanSummary)
388 + ind12 + TD_CLOSE_TAG
389 + ind10 + "</tr>";
390 }
391
392
393
394
395
396
397 private static void removeLeadingNewline(StringBuilder builder) {
398 while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) {
399 builder.delete(0, 1);
400 }
401 }
402
403
404
405
406
407
408
409 private static void appendHolderSection(Sink sink, StringBuilder holderRows) {
410 final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8
411 + TABLE_CLOSE_TAG
412 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
413 + DIV_CLOSE_TAG
414 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
415 + SECTION_CLOSE_TAG
416 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
417 + "<section name=\"Holder Checks\">"
418 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
419 + "<p>"
420 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
421 + "These checks aren't normal checks and are usually"
422 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
423 + "associated with a specialized filter to gather"
424 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
425 + "information the filter can't get on its own."
426 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
427 + "</p>"
428 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
429 + DIV_WRAPPER_TAG
430 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
431 + TABLE_OPEN_TAG
432 + ModuleJavadocParsingUtil.INDENT_LEVEL_10
433 + holderRows;
434 sink.rawText(holderSection);
435 }
436
437
438
439
440
441
442
443
444 private static void appendFilteredHolderSection(Sink sink, StringBuilder holderRows,
445 String packageName) {
446 final String packageTitle = getPackageDisplayName(packageName);
447 final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8
448 + TABLE_CLOSE_TAG
449 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
450 + DIV_CLOSE_TAG
451 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
452 + SECTION_CLOSE_TAG
453 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
454 + "<section name=\"" + packageTitle + " Holder Checks\">"
455 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
456 + DIV_WRAPPER_TAG
457 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
458 + TABLE_OPEN_TAG
459 + ModuleJavadocParsingUtil.INDENT_LEVEL_10
460 + holderRows;
461 sink.rawText(holderSection);
462 }
463
464
465
466
467
468
469
470 private static String getPackageDisplayName(String packageName) {
471 final String result;
472 if (packageName == null || packageName.isEmpty()) {
473 result = packageName;
474 }
475 else {
476 result = packageName.substring(0, 1).toUpperCase(Locale.ENGLISH)
477 + packageName.substring(1);
478 }
479 return result;
480 }
481
482
483
484
485
486
487 private static Map<String, String> buildXmlHtmlMap() {
488 final Map<String, String> map = new TreeMap<>();
489 if (Files.exists(SITE_CHECKS_ROOT)) {
490 try {
491 final List<Path> xmlFiles = new ArrayList<>();
492 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() {
493 @Override
494 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
495 if (isValidXmlFile(file)) {
496 xmlFiles.add(file);
497 }
498 return FileVisitResult.CONTINUE;
499 }
500 });
501
502 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map));
503 }
504 catch (IOException ignored) {
505
506 }
507 }
508 return map;
509 }
510
511
512
513
514
515
516
517 private static boolean isValidXmlFile(Path path) {
518 final Path fileName = path.getFileName();
519 return fileName != null
520 && !("index" + XML_EXTENSION).equalsIgnoreCase(fileName.toString())
521 && path.toString().endsWith(XML_EXTENSION)
522 && Files.isRegularFile(path);
523 }
524
525
526
527
528
529
530
531 private static void addXmlHtmlMapping(Path path, Map<String, String> map) {
532 final Path fileName = path.getFileName();
533 if (fileName != null) {
534 final String fileNameString = fileName.toString();
535 final int extensionLength = 4;
536 final String base = fileNameString.substring(0,
537 fileNameString.length() - extensionLength)
538 .toLowerCase(Locale.ROOT);
539 final Path relativePath = SITE_CHECKS_ROOT.relativize(path);
540 final String relativePathString = relativePath.toString();
541 final String rel = relativePathString
542 .replace('\\', '/')
543 .replace(XML_EXTENSION, HTML_EXTENSION);
544 map.put(base, CHECKS + "/" + rel);
545 }
546 }
547
548
549
550
551
552
553
554
555
556
557
558
559 private static String resolveHref(Map<String, String> xmlMap, String category,
560 String simpleName, @Nullable String packageFilter) {
561 final String lower = simpleName.toLowerCase(Locale.ROOT);
562 final String href = xmlMap.get(lower);
563 final String result;
564
565 if (href != null) {
566 if (packageFilter == null) {
567 result = href + ANCHOR_SEPARATOR + simpleName;
568 }
569 else {
570 final int lastSlash = href.lastIndexOf('/');
571 final String filename;
572 if (lastSlash >= 0) {
573 filename = href.substring(lastSlash + 1);
574 }
575 else {
576 filename = href;
577 }
578 result = filename + ANCHOR_SEPARATOR + simpleName;
579 }
580 }
581 else {
582 if (packageFilter == null) {
583 result = String.format(Locale.ROOT, "%s/%s/%s.html%s%s",
584 CHECKS, category, lower, ANCHOR_SEPARATOR, simpleName);
585 }
586 else {
587 result = String.format(Locale.ROOT, "%s.html%s%s",
588 lower, ANCHOR_SEPARATOR, simpleName);
589 }
590 }
591 return result;
592 }
593
594
595
596
597
598
599
600 private static String extractCategoryFromJavaPath(Path javaPath) {
601 final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath);
602 final Path parent = rel.getParent();
603 final String result;
604 if (parent == null) {
605 result = MISC_PACKAGE;
606 }
607 else {
608 result = parent.toString().replace('\\', '/');
609 }
610 return result;
611 }
612
613
614
615
616
617
618
619 private static String sanitizeAnchorUrls(String html) {
620 final String result;
621 if (html == null || html.isEmpty()) {
622 result = html;
623 }
624 else {
625 final Matcher matcher = HREF_PATTERN.matcher(html);
626 final StringBuilder buffer = new StringBuilder(html.length());
627
628 while (matcher.find()) {
629 final String originalUrl = matcher.group(1);
630 final String cleanedUrl = SPACE_PATTERN.matcher(originalUrl).replaceAll("");
631 final String replacement = "href=\""
632 + Matcher.quoteReplacement(cleanedUrl) + "\"";
633 matcher.appendReplacement(buffer, replacement);
634 }
635 matcher.appendTail(buffer);
636
637 result = buffer.toString();
638 }
639 return result;
640 }
641
642
643
644
645
646
647
648
649
650 private static String sanitizeAndFirstSentence(String html) {
651 final String result;
652 if (html == null || html.isEmpty()) {
653 result = "";
654 }
655 else {
656 String cleaned = sanitizeAnchorUrls(html);
657 cleaned = TAG_PATTERN.matcher(cleaned).replaceAll("");
658 cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim();
659 cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&");
660 cleaned = CODE_SPACE_PATTERN.matcher(cleaned).replaceAll(FIRST_CAPTURE_GROUP);
661 result = extractFirstSentence(cleaned);
662 }
663 return result;
664 }
665
666
667
668
669
670
671
672 private static String extractFirstSentence(String text) {
673 String result = "";
674 if (text != null && !text.isEmpty()) {
675 int end = -1;
676 for (int index = 0; index < text.length(); index++) {
677 if (text.charAt(index) == '.'
678 && (index == text.length() - 1
679 || Character.isWhitespace(text.charAt(index + 1))
680 || text.charAt(index + 1) == '<')) {
681 end = index;
682 break;
683 }
684 }
685 if (end == -1) {
686 result = text.trim();
687 }
688 else {
689 result = text.substring(0, end + 1).trim();
690 }
691 }
692 return result;
693 }
694
695
696
697
698
699
700
701
702 private static String wrapSummary(String text) {
703 String wrapped = "";
704
705 if (text != null && !text.isEmpty()) {
706 final String sanitized = sanitizeAnchorUrls(text);
707
708 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
709 final String clean = sanitized.trim();
710
711 final StringBuilder result = new StringBuilder(CAPACITY);
712 int cleanIndex = 0;
713 final int cleanLen = clean.length();
714
715 while (cleanIndex < cleanLen) {
716 final int remainingChars = cleanLen - cleanIndex;
717
718 if (remainingChars <= MAX_CONTENT_WIDTH) {
719 result.append(indent)
720 .append(clean.substring(cleanIndex))
721 .append('\n');
722 break;
723 }
724
725 final int idealBreak = cleanIndex + MAX_CONTENT_WIDTH;
726 final int actualBreak = calculateBreakPoint(clean, cleanIndex, idealBreak);
727
728 result.append(indent)
729 .append(clean, cleanIndex, actualBreak);
730
731 cleanIndex = actualBreak;
732 while (cleanIndex < cleanLen && clean.charAt(cleanIndex) == ' ') {
733 cleanIndex++;
734 }
735 }
736
737 wrapped = result.toString().trim();
738 }
739
740 return wrapped;
741 }
742
743
744
745
746
747
748
749
750
751
752 private static int calculateBreakPoint(String clean, int cleanIndex, int idealBreak) {
753 final int anchorStart = clean.indexOf("<a ", cleanIndex);
754 final int anchorOpenEnd;
755 if (anchorStart == -1) {
756 anchorOpenEnd = -1;
757 }
758 else {
759 anchorOpenEnd = clean.indexOf('>', anchorStart);
760 }
761
762 final int actualBreak;
763 if (shouldBreakAfterAnchorOpen(anchorStart, anchorOpenEnd, idealBreak)) {
764 actualBreak = anchorOpenEnd + 1;
765 }
766 else if (shouldBreakAfterAnchorContent(anchorStart, anchorOpenEnd,
767 idealBreak, clean)) {
768 actualBreak = anchorOpenEnd + 1;
769 }
770 else {
771 actualBreak = findSafeBreakPoint(clean, cleanIndex, idealBreak);
772 }
773
774 return actualBreak;
775 }
776
777
778
779
780
781
782
783
784
785 private static boolean shouldBreakAfterAnchorOpen(int anchorStart, int anchorOpenEnd,
786 int idealBreak) {
787 return anchorStart != -1 && anchorStart < idealBreak
788 && anchorOpenEnd != -1 && anchorOpenEnd >= idealBreak;
789 }
790
791
792
793
794
795
796
797
798
799
800 private static boolean shouldBreakAfterAnchorContent(int anchorStart, int anchorOpenEnd,
801 int idealBreak, String clean) {
802 final boolean result;
803 if (anchorStart != -1 && anchorStart < idealBreak
804 && anchorOpenEnd != -1 && anchorOpenEnd < idealBreak) {
805 final int anchorCloseStart = clean.indexOf(CLOSING_ANCHOR_TAG, anchorOpenEnd);
806 result = anchorCloseStart != -1 && anchorCloseStart >= idealBreak;
807 }
808 else {
809 result = false;
810 }
811 return result;
812 }
813
814
815
816
817
818
819
820
821
822 private static int findSafeBreakPoint(String text, int start, int idealBreak) {
823 final int actualBreak;
824 final int lastSpace = text.lastIndexOf(' ', idealBreak);
825
826 if (lastSpace > start && lastSpace >= start + MAX_CONTENT_WIDTH / 2) {
827 actualBreak = lastSpace;
828 }
829 else {
830 actualBreak = idealBreak;
831 }
832
833 return actualBreak;
834 }
835
836
837
838
839
840
841
842
843 private record CheckInfo(String simpleName, String link, String summary) {
844 }
845 }