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