001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.site; 021 022import java.io.IOException; 023import java.nio.file.FileVisitResult; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.SimpleFileVisitor; 027import java.nio.file.attribute.BasicFileAttributes; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.TreeMap; 033import java.util.regex.Pattern; 034 035import javax.annotation.Nullable; 036 037import org.apache.maven.doxia.macro.AbstractMacro; 038import org.apache.maven.doxia.macro.Macro; 039import org.apache.maven.doxia.macro.MacroExecutionException; 040import org.apache.maven.doxia.macro.MacroRequest; 041import org.apache.maven.doxia.sink.Sink; 042import org.codehaus.plexus.component.annotations.Component; 043 044import com.puppycrawl.tools.checkstyle.api.DetailNode; 045import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 046 047/** 048 * Macro to generate table rows for all Checkstyle modules. 049 * Includes every Check.java file that has a Javadoc. 050 * Uses href path structure based on src/site/xdoc/checks. 051 * 052 * <p>Supports optional "package" parameter to filter checks by package. 053 * When package parameter is provided, only checks from that package are included. 054 * Usage: 055 * <pre> 056 * <macro name="allCheckSummaries"/> (all checks) 057 * <macro name="allCheckSummaries"> 058 * <param name="package" value="annotation"/> 059 * </macro> 060 * </pre> 061 */ 062@Component(role = Macro.class, hint = "allCheckSummaries") 063public class AllCheckSummaries extends AbstractMacro { 064 065 /** 066 * Matches HTML anchor tags and captures their inner text. 067 * Used to strip <a> elements while keeping their display text. 068 */ 069 private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>"); 070 071 /** 072 * Matches common HTML tags such as paragraph, div, span, strong, and em. 073 * Used to remove formatting tags from the Javadoc HTML content. 074 */ 075 private static final Pattern TAG_PATTERN = 076 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>"); 077 078 /** 079 * Matches one or more whitespace characters. 080 * Used to normalize spacing in sanitized text. 081 */ 082 private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+"); 083 084 /** 085 * Matches '&' characters that are not part of a valid HTML entity. 086 */ 087 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)"); 088 089 /** Pattern to match trailing spaces before closing code tags. */ 090 private static final Pattern CODE_SPACE_PATTERN = Pattern.compile("\\s+(</code>)"); 091 092 /** Path component for source directory. */ 093 private static final String SRC = "src"; 094 095 /** Path component for checks directory. */ 096 private static final String CHECKS = "checks"; 097 098 /** Root path for Java check files. */ 099 private static final Path JAVA_CHECKS_ROOT = Path.of( 100 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS); 101 102 /** Root path for site check XML files. */ 103 private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS); 104 105 /** Maximum line width considering indentation. */ 106 private static final int MAX_LINE_WIDTH = 86; 107 108 /** XML file extension. */ 109 private static final String XML_EXTENSION = ".xml"; 110 111 /** HTML file extension. */ 112 private static final String HTML_EXTENSION = ".html"; 113 114 /** TD opening tag. */ 115 private static final String TD_TAG = "<td>"; 116 117 /** TD closing tag. */ 118 private static final String TD_CLOSE_TAG = "</td>"; 119 120 /** Package name for miscellaneous checks. */ 121 private static final String MISC_PACKAGE = "misc"; 122 123 /** Package name for annotation checks. */ 124 private static final String ANNOTATION_PACKAGE = "annotation"; 125 126 /** HTML table closing tag. */ 127 private static final String TABLE_CLOSE_TAG = "</table>"; 128 129 /** HTML div closing tag. */ 130 private static final String DIV_CLOSE_TAG = "</div>"; 131 132 /** HTML section closing tag. */ 133 private static final String SECTION_CLOSE_TAG = "</section>"; 134 135 /** HTML div wrapper opening tag. */ 136 private static final String DIV_WRAPPER_TAG = "<div class=\"wrapper\">"; 137 138 /** HTML table opening tag. */ 139 private static final String TABLE_OPEN_TAG = "<table>"; 140 141 /** HTML anchor separator. */ 142 private static final String ANCHOR_SEPARATOR = "#"; 143 144 /** Regex replacement for first capture group. */ 145 private static final String FIRST_CAPTURE_GROUP = "$1"; 146 147 @Override 148 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 149 final String packageFilter = (String) request.getParameter("package"); 150 151 final Map<String, String> xmlHrefMap = buildXmlHtmlMap(); 152 final Map<String, CheckInfo> infos = new TreeMap<>(); 153 154 processCheckFiles(infos, xmlHrefMap, packageFilter); 155 156 final StringBuilder normalRows = new StringBuilder(4096); 157 final StringBuilder holderRows = new StringBuilder(512); 158 159 buildTableRows(infos, normalRows, holderRows); 160 161 sink.rawText(normalRows.toString()); 162 if (packageFilter == null && !holderRows.isEmpty()) { 163 appendHolderSection(sink, holderRows); 164 } 165 else if (packageFilter != null && !holderRows.isEmpty()) { 166 appendFilteredHolderSection(sink, holderRows, packageFilter); 167 } 168 } 169 170 /** 171 * Scans Java sources and populates info map with modules having Javadoc. 172 * 173 * @param infos map of collected module info 174 * @param xmlHrefMap map of XML-to-HTML hrefs 175 * @param packageFilter optional package to filter by, null for all 176 * @throws MacroExecutionException if file walk fails 177 */ 178 private static void processCheckFiles(Map<String, CheckInfo> infos, 179 Map<String, String> xmlHrefMap, 180 String packageFilter) 181 throws MacroExecutionException { 182 try { 183 final List<Path> checkFiles = new ArrayList<>(); 184 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() { 185 @Override 186 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 187 if (isCheckOrHolderFile(file)) { 188 checkFiles.add(file); 189 } 190 return FileVisitResult.CONTINUE; 191 } 192 }); 193 194 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter)); 195 } 196 catch (IOException | IllegalStateException exception) { 197 throw new MacroExecutionException("Failed to discover checks", exception); 198 } 199 } 200 201 /** 202 * Checks if a path is a Check or Holder Java file. 203 * 204 * @param path the path to check 205 * @return true if the path is a Check or Holder file, false otherwise 206 */ 207 private static boolean isCheckOrHolderFile(Path path) { 208 final Path fileName = path.getFileName(); 209 return fileName != null 210 && (fileName.toString().endsWith("Check.java") 211 || fileName.toString().endsWith("Holder.java")) 212 && Files.isRegularFile(path); 213 } 214 215 /** 216 * Processes a single check class file and extracts metadata. 217 * 218 * @param path the check class file 219 * @param infos map of results 220 * @param xmlHrefMap map of XML hrefs 221 * @param packageFilter optional package to filter by, null for all 222 * @throws IllegalArgumentException if macro execution fails 223 */ 224 private static void processCheckFile(Path path, Map<String, CheckInfo> infos, 225 Map<String, String> xmlHrefMap, 226 String packageFilter) { 227 try { 228 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString()); 229 final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path); 230 if (javadoc != null) { 231 final String description = getDescriptionIfPresent(javadoc); 232 if (description != null) { 233 final boolean isHolder = moduleName.endsWith("Holder"); 234 final String simpleName = determineSimpleName(moduleName, isHolder); 235 final String packageName = determinePackageName(path, simpleName, isHolder); 236 237 // Filter by package if specified 238 if (packageFilter == null || packageFilter.equals(packageName)) { 239 final String summary = createSummary(description); 240 final String href = resolveHref(xmlHrefMap, packageName, simpleName, 241 packageFilter); 242 addCheckInfo(infos, simpleName, href, summary, isHolder); 243 } 244 } 245 } 246 247 } 248 catch (MacroExecutionException exceptionThrown) { 249 throw new IllegalArgumentException(exceptionThrown); 250 } 251 } 252 253 /** 254 * Determines the simple name of a check module. 255 * 256 * @param moduleName the full module name 257 * @param isHolder whether the module is a holder 258 * @return the simple name 259 */ 260 private static String determineSimpleName(String moduleName, boolean isHolder) { 261 final String simpleName; 262 if (isHolder) { 263 simpleName = moduleName; 264 } 265 else { 266 simpleName = moduleName.substring(0, moduleName.length() - "Check".length()); 267 } 268 return simpleName; 269 } 270 271 /** 272 * Determines the package name for a check, applying remapping rules. 273 * 274 * @param path the check class file 275 * @param simpleName simple name of the check 276 * @param isHolder whether the module is a holder 277 * @return the package name 278 */ 279 private static String determinePackageName(Path path, String simpleName, boolean isHolder) { 280 String packageName = extractCategory(path); 281 282 // Apply remapping for indentation -> misc 283 if ("indentation".equals(packageName)) { 284 packageName = MISC_PACKAGE; 285 } 286 287 // Apply holder overrides 288 if (isHolder && "SuppressWarningsHolder".equals(simpleName)) { 289 packageName = ANNOTATION_PACKAGE; 290 } 291 else if (isHolder && "SuppressXpathWarningsHolder".equals(simpleName)) { 292 packageName = ANNOTATION_PACKAGE; 293 } 294 295 return packageName; 296 } 297 298 /** 299 * Returns the module description if present and non-empty. 300 * 301 * @param javadoc the parsed Javadoc node 302 * @return the description text, or {@code null} if not present 303 */ 304 @Nullable 305 private static String getDescriptionIfPresent(DetailNode javadoc) { 306 String result = null; 307 final String desc = getModuleDescriptionSafe(javadoc); 308 if (desc != null && !desc.isEmpty()) { 309 result = desc; 310 } 311 return result; 312 } 313 314 /** 315 * Produces a concise, sanitized summary from the full Javadoc description. 316 * 317 * @param description full Javadoc text 318 * @return sanitized first sentence of the description 319 */ 320 private static String createSummary(String description) { 321 return sanitizeAndFirstSentence(description); 322 } 323 324 /** 325 * Extracts category name from the given Java source path. 326 * 327 * @param path source path of the class 328 * @return category name string 329 */ 330 private static String extractCategory(Path path) { 331 return extractCategoryFromJavaPath(path); 332 } 333 334 /** 335 * Adds a new {@link CheckInfo} record to the provided map. 336 * 337 * @param infos map to update 338 * @param simpleName simple class name 339 * @param href documentation href 340 * @param summary short summary of the check 341 * @param isHolder true if the check is a holder module 342 */ 343 private static void addCheckInfo(Map<String, CheckInfo> infos, 344 String simpleName, 345 String href, 346 String summary, 347 boolean isHolder) { 348 infos.put(simpleName, new CheckInfo(simpleName, href, summary, isHolder)); 349 } 350 351 /** 352 * Retrieves Javadoc description node safely. 353 * 354 * @param javadoc DetailNode root 355 * @return module description or null 356 */ 357 @Nullable 358 private static String getModuleDescriptionSafe(DetailNode javadoc) { 359 String result = null; 360 if (javadoc != null) { 361 try { 362 if (ModuleJavadocParsingUtil 363 .getModuleSinceVersionTagStartNode(javadoc) != null) { 364 result = ModuleJavadocParsingUtil.getModuleDescription(javadoc); 365 } 366 } 367 catch (IllegalStateException exception) { 368 result = null; 369 } 370 } 371 return result; 372 } 373 374 /** 375 * Builds HTML rows for both normal and holder check modules. 376 * 377 * @param infos map of collected module info 378 * @param normalRows builder for normal check rows 379 * @param holderRows builder for holder check rows 380 */ 381 private static void buildTableRows(Map<String, CheckInfo> infos, 382 StringBuilder normalRows, 383 StringBuilder holderRows) { 384 appendRows(infos, normalRows, holderRows); 385 finalizeRows(normalRows, holderRows); 386 } 387 388 /** 389 * Iterates over collected check info entries and appends corresponding rows. 390 * 391 * @param infos map of check info entries 392 * @param normalRows builder for normal check rows 393 * @param holderRows builder for holder check rows 394 */ 395 private static void appendRows(Map<String, CheckInfo> infos, 396 StringBuilder normalRows, 397 StringBuilder holderRows) { 398 for (CheckInfo info : infos.values()) { 399 final String row = buildTableRow(info); 400 if (info.isHolder) { 401 holderRows.append(row); 402 } 403 else { 404 normalRows.append(row); 405 } 406 } 407 } 408 409 /** 410 * Removes leading newlines from the generated table row builders. 411 * 412 * @param normalRows builder for normal check rows 413 * @param holderRows builder for holder check rows 414 */ 415 private static void finalizeRows(StringBuilder normalRows, StringBuilder holderRows) { 416 removeLeadingNewline(normalRows); 417 removeLeadingNewline(holderRows); 418 } 419 420 /** 421 * Builds a single table row for a check module. 422 * 423 * @param info check module information 424 * @return the HTML table row as a string 425 */ 426 private static String buildTableRow(CheckInfo info) { 427 final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10; 428 final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12; 429 final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 430 final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16; 431 432 return ind10 + "<tr>" 433 + ind12 + TD_TAG 434 + ind14 435 + "<a href=\"" 436 + info.link 437 + "\">" 438 + ind16 + info.simpleName 439 + ind14 + "</a>" 440 + ind12 + TD_CLOSE_TAG 441 + ind12 + TD_TAG 442 + ind14 + wrapSummary(info.summary) 443 + ind12 + TD_CLOSE_TAG 444 + ind10 + "</tr>"; 445 } 446 447 /** 448 * Removes leading newline characters from a StringBuilder. 449 * 450 * @param builder the StringBuilder to process 451 */ 452 private static void removeLeadingNewline(StringBuilder builder) { 453 while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) { 454 builder.delete(0, 1); 455 } 456 } 457 458 /** 459 * Appends the Holder Checks HTML section. 460 * 461 * @param sink the output sink 462 * @param holderRows the holder rows content 463 */ 464 private static void appendHolderSection(Sink sink, StringBuilder holderRows) { 465 final String holderSection = buildHolderSectionHtml(holderRows); 466 sink.rawText(holderSection); 467 } 468 469 /** 470 * Builds the HTML for the Holder Checks section. 471 * 472 * @param holderRows the holder rows content 473 * @return the complete HTML section as a string 474 */ 475 private static String buildHolderSectionHtml(StringBuilder holderRows) { 476 return ModuleJavadocParsingUtil.INDENT_LEVEL_8 477 + TABLE_CLOSE_TAG 478 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 479 + DIV_CLOSE_TAG 480 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 481 + SECTION_CLOSE_TAG 482 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 483 + "<section name=\"Holder Checks\">" 484 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 485 + "<p>" 486 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 487 + "These checks aren't normal checks and are usually" 488 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 489 + "associated with a specialized filter to gather" 490 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 491 + "information the filter can't get on its own." 492 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 493 + "</p>" 494 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 495 + DIV_WRAPPER_TAG 496 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 497 + TABLE_OPEN_TAG 498 + ModuleJavadocParsingUtil.INDENT_LEVEL_10 499 + holderRows; 500 } 501 502 /** 503 * Appends the filtered Holder Checks section for package views. 504 * 505 * @param sink the output sink 506 * @param holderRows the holder rows content 507 * @param packageName the package name 508 */ 509 private static void appendFilteredHolderSection(Sink sink, StringBuilder holderRows, 510 String packageName) { 511 final String packageTitle = getPackageDisplayName(packageName); 512 final String holderSection = buildFilteredHolderSectionHtml(holderRows, packageTitle); 513 sink.rawText(holderSection); 514 } 515 516 /** 517 * Builds the HTML for the filtered Holder Checks section. 518 * 519 * @param holderRows the holder rows content 520 * @param packageTitle the display name of the package 521 * @return the complete HTML section as a string 522 */ 523 private static String buildFilteredHolderSectionHtml(StringBuilder holderRows, 524 String packageTitle) { 525 return ModuleJavadocParsingUtil.INDENT_LEVEL_8 526 + TABLE_CLOSE_TAG 527 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 528 + DIV_CLOSE_TAG 529 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 530 + SECTION_CLOSE_TAG 531 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 532 + "<section name=\"" + packageTitle + " Holder Checks\">" 533 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 534 + DIV_WRAPPER_TAG 535 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 536 + TABLE_OPEN_TAG 537 + ModuleJavadocParsingUtil.INDENT_LEVEL_10 538 + holderRows; 539 } 540 541 /** 542 * Get display name for package (capitalize first letter). 543 * 544 * @param packageName the package name 545 * @return the capitalized package name 546 */ 547 private static String getPackageDisplayName(String packageName) { 548 final String result; 549 if (packageName == null || packageName.isEmpty()) { 550 result = packageName; 551 } 552 else { 553 result = packageName.substring(0, 1).toUpperCase(Locale.ENGLISH) 554 + packageName.substring(1); 555 } 556 return result; 557 } 558 559 /** 560 * Builds map of XML file names to HTML documentation paths. 561 * 562 * @return map of lowercase check names to hrefs 563 */ 564 private static Map<String, String> buildXmlHtmlMap() { 565 final Map<String, String> map = new TreeMap<>(); 566 if (Files.exists(SITE_CHECKS_ROOT)) { 567 try { 568 final List<Path> xmlFiles = new ArrayList<>(); 569 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() { 570 @Override 571 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 572 if (isValidXmlFile(file)) { 573 xmlFiles.add(file); 574 } 575 return FileVisitResult.CONTINUE; 576 } 577 }); 578 579 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map)); 580 } 581 catch (IOException ignored) { 582 // ignore 583 } 584 } 585 return map; 586 } 587 588 /** 589 * Checks if a path is a valid XML file for processing. 590 * 591 * @param path the path to check 592 * @return true if the path is a valid XML file, false otherwise 593 */ 594 private static boolean isValidXmlFile(Path path) { 595 final Path fileName = path.getFileName(); 596 return fileName != null 597 && !("index" + XML_EXTENSION).equalsIgnoreCase(fileName.toString()) 598 && path.toString().endsWith(XML_EXTENSION) 599 && Files.isRegularFile(path); 600 } 601 602 /** 603 * Adds XML-to-HTML mapping entry to map. 604 * 605 * @param path the XML file path 606 * @param map the mapping to update 607 */ 608 private static void addXmlHtmlMapping(Path path, Map<String, String> map) { 609 final Path fileName = path.getFileName(); 610 if (fileName != null) { 611 final String fileNameString = fileName.toString(); 612 final int extensionLength = 4; 613 final String base = fileNameString.substring(0, 614 fileNameString.length() - extensionLength) 615 .toLowerCase(Locale.ROOT); 616 final Path relativePath = SITE_CHECKS_ROOT.relativize(path); 617 final String relativePathString = relativePath.toString(); 618 final String rel = relativePathString 619 .replace('\\', '/') 620 .replace(XML_EXTENSION, HTML_EXTENSION); 621 map.put(base, CHECKS + "/" + rel); 622 } 623 } 624 625 /** 626 * Resolves the href for a given check module. 627 * When packageFilter is null, returns full path: checks/category/filename.html#CheckName 628 * When packageFilter is set, returns relative path: filename.html#CheckName 629 * 630 * @param xmlMap map of XML file names to HTML paths 631 * @param category the category of the check 632 * @param simpleName simple name of the check 633 * @param packageFilter optional package filter, null for all checks 634 * @return the resolved href for the check 635 */ 636 private static String resolveHref(Map<String, String> xmlMap, String category, 637 String simpleName, @Nullable String packageFilter) { 638 final String lower = simpleName.toLowerCase(Locale.ROOT); 639 final String href = xmlMap.get(lower); 640 final String result; 641 642 if (href != null) { 643 // XML file exists in the map 644 if (packageFilter == null) { 645 // Full path for all checks view 646 result = href + ANCHOR_SEPARATOR + simpleName; 647 } 648 else { 649 // Extract just the filename for filtered view 650 final int lastSlash = href.lastIndexOf('/'); 651 final String filename; 652 if (lastSlash >= 0) { 653 filename = href.substring(lastSlash + 1); 654 } 655 else { 656 filename = href; 657 } 658 result = filename + ANCHOR_SEPARATOR + simpleName; 659 } 660 } 661 else { 662 // XML file not found, construct default path 663 if (packageFilter == null) { 664 // Full path for all checks view 665 result = String.format(Locale.ROOT, "%s/%s/%s.html%s%s", 666 CHECKS, category, lower, ANCHOR_SEPARATOR, simpleName); 667 } 668 else { 669 // Just filename for filtered view 670 result = String.format(Locale.ROOT, "%s.html%s%s", 671 lower, ANCHOR_SEPARATOR, simpleName); 672 } 673 } 674 return result; 675 } 676 677 /** 678 * Extracts category path from a Java file path. 679 * 680 * @param javaPath the Java source file path 681 * @return the category path extracted from the Java path 682 */ 683 private static String extractCategoryFromJavaPath(Path javaPath) { 684 final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath); 685 final Path parent = rel.getParent(); 686 final String result; 687 if (parent == null) { 688 // Root-level checks go to misc 689 result = MISC_PACKAGE; 690 } 691 else { 692 result = parent.toString().replace('\\', '/'); 693 } 694 return result; 695 } 696 697 /** 698 * Sanitizes HTML and extracts first sentence. 699 * 700 * @param html the HTML string to process 701 * @return the sanitized first sentence 702 */ 703 private static String sanitizeAndFirstSentence(String html) { 704 final String result; 705 if (html == null || html.isEmpty()) { 706 result = ""; 707 } 708 else { 709 String cleaned = LINK_PATTERN.matcher(html).replaceAll(FIRST_CAPTURE_GROUP); 710 cleaned = TAG_PATTERN.matcher(cleaned).replaceAll(""); 711 cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim(); 712 cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&"); 713 cleaned = CODE_SPACE_PATTERN.matcher(cleaned).replaceAll(FIRST_CAPTURE_GROUP); 714 result = extractFirstSentence(cleaned); 715 } 716 return result; 717 } 718 719 /** 720 * Extracts first sentence from plain text. 721 * 722 * @param text the text to process 723 * @return the first sentence extracted from the text 724 */ 725 private static String extractFirstSentence(String text) { 726 String result = ""; 727 if (text != null && !text.isEmpty()) { 728 int end = -1; 729 for (int index = 0; index < text.length(); index++) { 730 if (text.charAt(index) == '.' 731 && (index == text.length() - 1 732 || Character.isWhitespace(text.charAt(index + 1)) 733 || text.charAt(index + 1) == '<')) { 734 end = index; 735 break; 736 } 737 } 738 if (end == -1) { 739 result = text.trim(); 740 } 741 else { 742 result = text.substring(0, end + 1).trim(); 743 } 744 } 745 return result; 746 } 747 748 /** 749 * Wraps long summaries to avoid exceeding line width. 750 * 751 * @param text the text to wrap 752 * @return the wrapped text 753 */ 754 private static String wrapSummary(String text) { 755 final String result; 756 if (text == null || text.isEmpty()) { 757 result = ""; 758 } 759 else if (text.length() <= MAX_LINE_WIDTH) { 760 result = text; 761 } 762 else { 763 result = performWrapping(text); 764 } 765 return result; 766 } 767 768 /** 769 * Performs wrapping of summary text. 770 * 771 * @param text the text to wrap 772 * @return the wrapped text 773 */ 774 private static String performWrapping(String text) { 775 final int textLength = text.length(); 776 final StringBuilder result = new StringBuilder(textLength + 100); 777 int pos = 0; 778 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 779 boolean firstLine = true; 780 781 while (pos < textLength) { 782 final int end = Math.min(pos + MAX_LINE_WIDTH, textLength); 783 if (end >= textLength) { 784 if (!firstLine) { 785 result.append(indent); 786 } 787 result.append(text.substring(pos)); 788 break; 789 } 790 int breakPos = text.lastIndexOf(' ', end); 791 if (breakPos <= pos) { 792 breakPos = end; 793 } 794 if (!firstLine) { 795 result.append(indent); 796 } 797 result.append(text, pos, breakPos); 798 pos = breakPos + 1; 799 firstLine = false; 800 } 801 return result.toString(); 802 } 803 804 /** 805 * Data holder for each Check module entry. 806 */ 807 private static final class CheckInfo { 808 /** Simple name of the check. */ 809 private final String simpleName; 810 /** Documentation link. */ 811 private final String link; 812 /** Short summary text. */ 813 private final String summary; 814 /** Whether the module is a holder type. */ 815 private final boolean isHolder; 816 817 /** 818 * Constructs an info record. 819 * 820 * @param simpleName check simple name 821 * @param link documentation link 822 * @param summary module summary 823 * @param isHolder whether holder 824 */ 825 private CheckInfo(String simpleName, String link, 826 String summary, boolean isHolder) { 827 this.simpleName = simpleName; 828 this.link = link; 829 this.summary = summary; 830 this.isHolder = isHolder; 831 } 832 } 833}