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