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