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.Pattern;
34
35 import javax.annotation.Nullable;
36
37 import org.apache.maven.doxia.macro.AbstractMacro;
38 import org.apache.maven.doxia.macro.Macro;
39 import org.apache.maven.doxia.macro.MacroExecutionException;
40 import org.apache.maven.doxia.macro.MacroRequest;
41 import org.apache.maven.doxia.sink.Sink;
42 import org.codehaus.plexus.component.annotations.Component;
43
44 import com.puppycrawl.tools.checkstyle.api.DetailNode;
45 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
46
47
48
49
50
51
52 @Component(role = Macro.class, hint = "allCheckSummaries")
53 public class AllCheckSummaries extends AbstractMacro {
54
55
56
57
58
59 private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>");
60
61
62
63
64
65 private static final Pattern TAG_PATTERN =
66 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>");
67
68
69
70
71
72 private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+");
73
74
75
76
77 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)");
78
79
80 private static final String SRC = "src";
81
82
83 private static final String CHECKS = "checks";
84
85
86 private static final Path JAVA_CHECKS_ROOT = Path.of(
87 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS);
88
89
90 private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS);
91
92
93 private static final int MAX_LINE_WIDTH = 86;
94
95
96 private static final String XML_EXTENSION = ".xml";
97
98
99 private static final String HTML_EXTENSION = ".html";
100
101
102 private static final String TD_TAG = "<td>";
103
104
105 private static final String TD_CLOSE_TAG = "</td>";
106
107 @Override
108 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
109 final Map<String, String> xmlHrefMap = buildXmlHtmlMap();
110 final Map<String, CheckInfo> infos = new TreeMap<>();
111
112 processCheckFiles(infos, xmlHrefMap);
113
114 final StringBuilder normalRows = new StringBuilder(4096);
115 final StringBuilder holderRows = new StringBuilder(512);
116
117 buildTableRows(infos, normalRows, holderRows);
118
119 sink.rawText(normalRows.toString());
120
121 if (!holderRows.isEmpty()) {
122 appendHolderSection(sink, holderRows);
123 }
124 }
125
126
127
128
129
130
131
132
133 private static void processCheckFiles(Map<String, CheckInfo> infos,
134 Map<String, String> xmlHrefMap)
135 throws MacroExecutionException {
136 try {
137 final List<Path> checkFiles = new ArrayList<>();
138 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() {
139 @Override
140 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
141 if (isCheckOrHolderFile(file)) {
142 checkFiles.add(file);
143 }
144 return FileVisitResult.CONTINUE;
145 }
146 });
147
148 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap));
149 }
150 catch (IOException | IllegalStateException exception) {
151 throw new MacroExecutionException("Failed to discover checks", exception);
152 }
153 }
154
155
156
157
158
159
160
161 private static boolean isCheckOrHolderFile(Path path) {
162 final boolean result;
163 if (Files.isRegularFile(path)) {
164 final Path fileName = path.getFileName();
165 if (fileName == null) {
166 result = false;
167 }
168 else {
169 final String name = fileName.toString();
170 result = name.endsWith("Check.java") || name.endsWith("Holder.java");
171 }
172 }
173 else {
174 result = false;
175 }
176 return result;
177 }
178
179
180
181
182
183
184
185
186
187 private static void processCheckFile(Path path, Map<String, CheckInfo> infos,
188 Map<String, String> xmlHrefMap) {
189 try {
190 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString());
191 final boolean isHolder = moduleName.endsWith("Holder");
192 final String simpleName;
193 if (isHolder) {
194 simpleName = moduleName;
195 }
196 else {
197 simpleName = moduleName.substring(0, moduleName.length() - "Check".length());
198 }
199 final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path);
200 if (javadoc != null) {
201 final String description = getDescriptionIfPresent(javadoc);
202 if (description != null) {
203 final String summary = createSummary(description);
204 final String category = extractCategory(path);
205 final String href = resolveHref(xmlHrefMap, category, simpleName);
206 addCheckInfo(infos, simpleName, href, summary, isHolder);
207 }
208 }
209
210 }
211 catch (MacroExecutionException exceptionThrown) {
212 throw new IllegalArgumentException(exceptionThrown);
213 }
214 }
215
216
217
218
219
220
221
222 @Nullable
223 private static String getDescriptionIfPresent(DetailNode javadoc) {
224 String result = null;
225 final String desc = getModuleDescriptionSafe(javadoc);
226 if (desc != null && !desc.isEmpty()) {
227 result = desc;
228 }
229 return result;
230 }
231
232
233
234
235
236
237
238 private static String createSummary(String description) {
239 return sanitizeAndFirstSentence(description);
240 }
241
242
243
244
245
246
247
248 private static String extractCategory(Path path) {
249 return extractCategoryFromJavaPath(path);
250 }
251
252
253
254
255
256
257
258
259
260
261 private static void addCheckInfo(Map<String, CheckInfo> infos,
262 String simpleName,
263 String href,
264 String summary,
265 boolean isHolder) {
266 infos.put(simpleName, new CheckInfo(simpleName, href, summary, isHolder));
267 }
268
269
270
271
272
273
274
275 @Nullable
276 private static String getModuleDescriptionSafe(DetailNode javadoc) {
277 String result = null;
278 if (javadoc != null) {
279 try {
280 if (ModuleJavadocParsingUtil
281 .getModuleSinceVersionTagStartNode(javadoc) != null) {
282 result = ModuleJavadocParsingUtil.getModuleDescription(javadoc);
283 }
284 }
285 catch (IllegalStateException exception) {
286 result = null;
287 }
288 }
289 return result;
290 }
291
292
293
294
295
296
297
298
299 private static void buildTableRows(Map<String, CheckInfo> infos,
300 StringBuilder normalRows,
301 StringBuilder holderRows) {
302 appendRows(infos, normalRows, holderRows);
303 finalizeRows(normalRows, holderRows);
304 }
305
306
307
308
309
310
311
312
313 private static void appendRows(Map<String, CheckInfo> infos,
314 StringBuilder normalRows,
315 StringBuilder holderRows) {
316 for (CheckInfo info : infos.values()) {
317 final String row = buildTableRow(info);
318 if (info.isHolder) {
319 holderRows.append(row);
320 }
321 else {
322 normalRows.append(row);
323 }
324 }
325 }
326
327
328
329
330
331
332
333 private static void finalizeRows(StringBuilder normalRows, StringBuilder holderRows) {
334 removeLeadingNewline(normalRows);
335 removeLeadingNewline(holderRows);
336 }
337
338
339
340
341
342
343
344 private static String buildTableRow(CheckInfo info) {
345 final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10;
346 final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12;
347 final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
348 final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16;
349
350 return ind10 + "<tr>"
351 + ind12 + TD_TAG
352 + ind14
353 + "<a href=\""
354 + info.link
355 + "\">"
356 + ind16 + info.simpleName
357 + ind14 + "</a>"
358 + ind12 + TD_CLOSE_TAG
359 + ind12 + TD_TAG
360 + ind14 + wrapSummary(info.summary)
361 + ind12 + TD_CLOSE_TAG
362 + ind10 + "</tr>";
363 }
364
365
366
367
368
369
370 private static void removeLeadingNewline(StringBuilder builder) {
371 while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) {
372 builder.delete(0, 1);
373 }
374 }
375
376
377
378
379
380
381
382 private static void appendHolderSection(Sink sink, StringBuilder holderRows) {
383 final String holderSection = buildHolderSectionHtml(holderRows);
384 sink.rawText(holderSection);
385 }
386
387
388
389
390
391
392
393 private static String buildHolderSectionHtml(StringBuilder holderRows) {
394 return ModuleJavadocParsingUtil.INDENT_LEVEL_8
395 + "</table>"
396 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
397 + "</div>"
398 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
399 + "</section>"
400 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
401 + "<section name=\"Holder Checks\">"
402 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
403 + "<p>"
404 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
405 + "These checks aren't normal checks and are usually"
406 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
407 + "associated with a specialized filter to gather"
408 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
409 + "information the filter can't get on its own."
410 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
411 + "</p>"
412 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
413 + "<div class=\"wrapper\">"
414 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
415 + "<table>"
416 + ModuleJavadocParsingUtil.INDENT_LEVEL_10
417 + holderRows;
418 }
419
420
421
422
423
424
425 private static Map<String, String> buildXmlHtmlMap() {
426 final Map<String, String> map = new TreeMap<>();
427 if (Files.exists(SITE_CHECKS_ROOT)) {
428 try {
429 final List<Path> xmlFiles = new ArrayList<>();
430 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() {
431 @Override
432 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
433 if (isValidXmlFile(file)) {
434 xmlFiles.add(file);
435 }
436 return FileVisitResult.CONTINUE;
437 }
438 });
439
440 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map));
441 }
442 catch (IOException ignored) {
443
444 }
445 }
446 return map;
447 }
448
449
450
451
452
453
454
455 private static boolean isValidXmlFile(Path path) {
456 final boolean result;
457 if (Files.isRegularFile(path)
458 && path.toString().endsWith(XML_EXTENSION)) {
459 final Path fileName = path.getFileName();
460 result = fileName != null
461 && !("index" + XML_EXTENSION)
462 .equalsIgnoreCase(fileName.toString());
463 }
464 else {
465 result = false;
466 }
467 return result;
468 }
469
470
471
472
473
474
475
476 private static void addXmlHtmlMapping(Path path, Map<String, String> map) {
477 final Path fileName = path.getFileName();
478 if (fileName != null) {
479 final String fileNameString = fileName.toString();
480 final int extensionLength = 4;
481 final String base = fileNameString.substring(0,
482 fileNameString.length() - extensionLength)
483 .toLowerCase(Locale.ROOT);
484 final Path relativePath = SITE_CHECKS_ROOT.relativize(path);
485 final String relativePathString = relativePath.toString();
486 final String rel = relativePathString
487 .replace('\\', '/')
488 .replace(XML_EXTENSION, HTML_EXTENSION);
489 map.put(base, CHECKS + "/" + rel);
490 }
491 }
492
493
494
495
496
497
498
499
500
501 private static String resolveHref(Map<String, String> xmlMap, String category,
502 String simpleName) {
503 final String lower = simpleName.toLowerCase(Locale.ROOT);
504 final String href = xmlMap.get(lower);
505 final String result;
506 if (href != null) {
507 result = href + "#" + simpleName;
508 }
509 else {
510 result = String.format(Locale.ROOT, "%s/%s/%s.html#%s",
511 CHECKS, category, lower, simpleName);
512 }
513 return result;
514 }
515
516
517
518
519
520
521
522 private static String extractCategoryFromJavaPath(Path javaPath) {
523 final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath);
524 final Path parent = rel.getParent();
525 final String result;
526 if (parent == null) {
527 result = "";
528 }
529 else {
530 result = parent.toString().replace('\\', '/');
531 }
532 return result;
533 }
534
535
536
537
538
539
540
541 private static String sanitizeAndFirstSentence(String html) {
542 final String result;
543 if (html == null || html.isEmpty()) {
544 result = "";
545 }
546 else {
547 String cleaned = LINK_PATTERN.matcher(html).replaceAll("$1");
548 cleaned = TAG_PATTERN.matcher(cleaned).replaceAll("");
549 cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim();
550 cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&");
551 result = extractFirstSentence(cleaned);
552 }
553 return result;
554 }
555
556
557
558
559
560
561
562 private static String extractFirstSentence(String text) {
563 String result = "";
564 if (text != null && !text.isEmpty()) {
565 int end = -1;
566 for (int index = 0; index < text.length(); index++) {
567 if (text.charAt(index) == '.'
568 && (index == text.length() - 1
569 || Character.isWhitespace(text.charAt(index + 1))
570 || text.charAt(index + 1) == '<')) {
571 end = index;
572 break;
573 }
574 }
575 if (end == -1) {
576 result = text.trim();
577 }
578 else {
579 result = text.substring(0, end + 1).trim();
580 }
581 }
582 return result;
583 }
584
585
586
587
588
589
590
591 private static String wrapSummary(String text) {
592 final String result;
593 if (text == null || text.isEmpty()) {
594 result = "";
595 }
596 else if (text.length() <= MAX_LINE_WIDTH) {
597 result = text;
598 }
599 else {
600 result = performWrapping(text);
601 }
602 return result;
603 }
604
605
606
607
608
609
610
611 private static String performWrapping(String text) {
612 final int textLength = text.length();
613 final StringBuilder result = new StringBuilder(textLength + 100);
614 int pos = 0;
615 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
616 boolean firstLine = true;
617
618 while (pos < textLength) {
619 final int end = Math.min(pos + MAX_LINE_WIDTH, textLength);
620 if (end >= textLength) {
621 if (!firstLine) {
622 result.append(indent);
623 }
624 result.append(text.substring(pos));
625 break;
626 }
627 int breakPos = text.lastIndexOf(' ', end);
628 if (breakPos <= pos) {
629 breakPos = end;
630 }
631 if (!firstLine) {
632 result.append(indent);
633 }
634 result.append(text, pos, breakPos);
635 pos = breakPos + 1;
636 firstLine = false;
637 }
638 return result.toString();
639 }
640
641
642
643
644 private static final class CheckInfo {
645
646 private final String simpleName;
647
648 private final String link;
649
650 private final String summary;
651
652 private final boolean isHolder;
653
654
655
656
657
658
659
660
661
662
663
664
665 private CheckInfo(String simpleName, String link,
666 String summary, boolean isHolder) {
667 this.simpleName = simpleName;
668 this.link = link;
669 this.summary = summary;
670 this.isHolder = isHolder;
671 }
672 }
673 }