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