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 HTML anchor tags and captures their inner text. 070 * Used to strip <a> elements while keeping their display text. 071 */ 072 private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>"); 073 074 /** 075 * Matches common HTML tags such as paragraph, div, span, strong, and em. 076 * Used to remove formatting tags from the Javadoc HTML content. 077 */ 078 private static final Pattern TAG_PATTERN = 079 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>"); 080 081 /** 082 * Matches one or more whitespace characters. 083 * Used to normalize spacing in sanitized text. 084 */ 085 private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+"); 086 087 /** 088 * Matches '&' characters that are not part of a valid HTML entity. 089 */ 090 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)"); 091 092 /** Pattern to match trailing spaces before closing code tags. */ 093 private static final Pattern CODE_SPACE_PATTERN = Pattern.compile("\\s+(</code>)"); 094 095 /** Path component for source directory. */ 096 private static final String SRC = "src"; 097 098 /** Path component for checks directory. */ 099 private static final String CHECKS = "checks"; 100 101 /** Root path for Java check files. */ 102 private static final Path JAVA_CHECKS_ROOT = Path.of( 103 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS); 104 105 /** Root path for site check XML files. */ 106 private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS); 107 108 /** Maximum line width considering indentation. */ 109 private static final int MAX_LINE_WIDTH = 86; 110 111 /** XML file extension. */ 112 private static final String XML_EXTENSION = ".xml"; 113 114 /** HTML file extension. */ 115 private static final String HTML_EXTENSION = ".html"; 116 117 /** TD opening tag. */ 118 private static final String TD_TAG = "<td>"; 119 120 /** TD closing tag. */ 121 private static final String TD_CLOSE_TAG = "</td>"; 122 123 /** Package name for miscellaneous checks. */ 124 private static final String MISC_PACKAGE = "misc"; 125 126 /** Package name for annotation checks. */ 127 private static final String ANNOTATION_PACKAGE = "annotation"; 128 129 /** HTML table closing tag. */ 130 private static final String TABLE_CLOSE_TAG = "</table>"; 131 132 /** HTML div closing tag. */ 133 private static final String DIV_CLOSE_TAG = "</div>"; 134 135 /** HTML section closing tag. */ 136 private static final String SECTION_CLOSE_TAG = "</section>"; 137 138 /** HTML div wrapper opening tag. */ 139 private static final String DIV_WRAPPER_TAG = "<div class=\"wrapper\">"; 140 141 /** HTML table opening tag. */ 142 private static final String TABLE_OPEN_TAG = "<table>"; 143 144 /** HTML anchor separator. */ 145 private static final String ANCHOR_SEPARATOR = "#"; 146 147 /** Regex replacement for first capture group. */ 148 private static final String FIRST_CAPTURE_GROUP = "$1"; 149 150 @Override 151 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 152 final String packageFilter = (String) request.getParameter("package"); 153 154 final Map<String, String> xmlHrefMap = buildXmlHtmlMap(); 155 final Map<String, CheckInfo> infos = new TreeMap<>(); 156 157 processCheckFiles(infos, xmlHrefMap, packageFilter); 158 159 final StringBuilder normalRows = new StringBuilder(4096); 160 final StringBuilder holderRows = new StringBuilder(512); 161 162 buildTableRows(infos, normalRows, holderRows); 163 164 sink.rawText(normalRows.toString()); 165 if (packageFilter == null && !holderRows.isEmpty()) { 166 appendHolderSection(sink, holderRows); 167 } 168 else if (packageFilter != null && !holderRows.isEmpty()) { 169 appendFilteredHolderSection(sink, holderRows, packageFilter); 170 } 171 } 172 173 /** 174 * Scans Java sources and populates info map with modules having Javadoc. 175 * 176 * @param infos map of collected module info 177 * @param xmlHrefMap map of XML-to-HTML hrefs 178 * @param packageFilter optional package to filter by, null for all 179 * @throws MacroExecutionException if file walk fails 180 */ 181 private static void processCheckFiles(Map<String, CheckInfo> infos, 182 Map<String, String> xmlHrefMap, 183 String packageFilter) 184 throws MacroExecutionException { 185 try { 186 final List<Path> checkFiles = new ArrayList<>(); 187 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() { 188 @Override 189 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 190 if (isCheckOrHolderFile(file)) { 191 checkFiles.add(file); 192 } 193 return FileVisitResult.CONTINUE; 194 } 195 }); 196 197 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter)); 198 } 199 catch (IOException | IllegalStateException exception) { 200 throw new MacroExecutionException("Failed to discover checks", exception); 201 } 202 } 203 204 /** 205 * Checks if a path is a Check or Holder Java file. 206 * 207 * @param path the path to check 208 * @return true if the path is a Check or Holder file, false otherwise 209 */ 210 private static boolean isCheckOrHolderFile(Path path) { 211 final Path fileName = path.getFileName(); 212 return fileName != null 213 && (fileName.toString().endsWith("Check.java") 214 || fileName.toString().endsWith("Holder.java")) 215 && Files.isRegularFile(path); 216 } 217 218 /** 219 * Checks if a module is a holder type. 220 * 221 * @param moduleName the module name 222 * @return true if the module is a holder, false otherwise 223 */ 224 private static boolean isHolder(String moduleName) { 225 return moduleName.endsWith("Holder"); 226 } 227 228 /** 229 * Processes a single check class file and extracts metadata. 230 * 231 * @param path the check class file 232 * @param infos map of results 233 * @param xmlHrefMap map of XML hrefs 234 * @param packageFilter optional package to filter by, null for all 235 * @throws IllegalArgumentException if macro execution fails 236 */ 237 private static void processCheckFile(Path path, Map<String, CheckInfo> infos, 238 Map<String, String> xmlHrefMap, 239 String packageFilter) { 240 try { 241 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString()); 242 final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path); 243 if (javadoc != null) { 244 final String description = getDescriptionIfPresent(javadoc); 245 if (description != null) { 246 final boolean isHolderModule = isHolder(moduleName); 247 final String packageName = determinePackageName(path, isHolderModule); 248 249 if (packageFilter == null || packageFilter.equals(packageName)) { 250 final String simpleName = determineSimpleName(moduleName, isHolderModule); 251 final String summary = createSummary(description); 252 final String href = resolveHref(xmlHrefMap, packageName, simpleName, 253 packageFilter); 254 addCheckInfo(infos, simpleName, href, summary, isHolderModule); 255 } 256 } 257 } 258 259 } 260 catch (MacroExecutionException exceptionThrown) { 261 throw new IllegalArgumentException(exceptionThrown); 262 } 263 } 264 265 /** 266 * Determines the simple name of a check module. 267 * 268 * @param moduleName the full module name 269 * @param isHolder whether the module is a holder 270 * @return the simple name 271 */ 272 private static String determineSimpleName(String moduleName, boolean isHolder) { 273 final String simpleName; 274 if (isHolder) { 275 simpleName = moduleName; 276 } 277 else { 278 simpleName = moduleName.substring(0, moduleName.length() - "Check".length()); 279 } 280 return simpleName; 281 } 282 283 /** 284 * Determines the package name for a check, applying remapping rules. 285 * 286 * @param path the check class file 287 * @param isHolder whether the module is a holder 288 * @return the package name 289 */ 290 private static String determinePackageName(Path path, boolean isHolder) { 291 String packageName = extractCategory(path); 292 293 // Apply remapping for indentation -> misc 294 if ("indentation".equals(packageName)) { 295 packageName = MISC_PACKAGE; 296 } 297 if (isHolder) { 298 packageName = ANNOTATION_PACKAGE; 299 } 300 return packageName; 301 } 302 303 /** 304 * Returns the module description if present and non-empty. 305 * 306 * @param javadoc the parsed Javadoc node 307 * @return the description text, or {@code null} if not present 308 */ 309 @Nullable 310 private static String getDescriptionIfPresent(DetailNode javadoc) { 311 String result = null; 312 final String desc = getModuleDescriptionSafe(javadoc); 313 if (desc != null && !desc.isEmpty()) { 314 result = desc; 315 } 316 return result; 317 } 318 319 /** 320 * Produces a concise, sanitized summary from the full Javadoc description. 321 * 322 * @param description full Javadoc text 323 * @return sanitized first sentence of the description 324 */ 325 private static String createSummary(String description) { 326 return sanitizeAndFirstSentence(description); 327 } 328 329 /** 330 * Extracts category name from the given Java source path. 331 * 332 * @param path source path of the class 333 * @return category name string 334 */ 335 private static String extractCategory(Path path) { 336 return extractCategoryFromJavaPath(path); 337 } 338 339 /** 340 * Adds a new {@link CheckInfo} record to the provided map. 341 * 342 * @param infos map to update 343 * @param simpleName simple class name 344 * @param href documentation href 345 * @param summary short summary of the check 346 * @param isHolder true if the check is a holder module 347 */ 348 private static void addCheckInfo(Map<String, CheckInfo> infos, 349 String simpleName, 350 String href, 351 String summary, 352 boolean isHolder) { 353 infos.put(simpleName, new CheckInfo(simpleName, href, summary, isHolder)); 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 (info.isHolder) { 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 * 705 * @param html the HTML string to process 706 * @return the sanitized first sentence 707 */ 708 private static String sanitizeAndFirstSentence(String html) { 709 final String result; 710 if (html == null || html.isEmpty()) { 711 result = ""; 712 } 713 else { 714 String cleaned = LINK_PATTERN.matcher(html).replaceAll(FIRST_CAPTURE_GROUP); 715 cleaned = TAG_PATTERN.matcher(cleaned).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 final String result; 761 if (text == null || text.isEmpty()) { 762 result = ""; 763 } 764 else if (text.length() <= MAX_LINE_WIDTH) { 765 result = text; 766 } 767 else { 768 result = performWrapping(text); 769 } 770 return result; 771 } 772 773 /** 774 * Performs wrapping of summary text. 775 * 776 * @param text the text to wrap 777 * @return the wrapped text 778 */ 779 private static String performWrapping(String text) { 780 final int textLength = text.length(); 781 final StringBuilder result = new StringBuilder(textLength + 100); 782 int pos = 0; 783 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 784 boolean firstLine = true; 785 786 while (pos < textLength) { 787 final int end = Math.min(pos + MAX_LINE_WIDTH, textLength); 788 if (end >= textLength) { 789 if (!firstLine) { 790 result.append(indent); 791 } 792 result.append(text.substring(pos)); 793 break; 794 } 795 int breakPos = text.lastIndexOf(' ', end); 796 if (breakPos <= pos) { 797 breakPos = end; 798 } 799 if (!firstLine) { 800 result.append(indent); 801 } 802 result.append(text, pos, breakPos); 803 pos = breakPos + 1; 804 firstLine = false; 805 } 806 return result.toString(); 807 } 808 809 /** 810 * Data holder for each Check module entry. 811 */ 812 private static final class CheckInfo { 813 /** Simple name of the check. */ 814 private final String simpleName; 815 /** Documentation link. */ 816 private final String link; 817 /** Short summary text. */ 818 private final String summary; 819 /** Whether the module is a holder type. */ 820 private final boolean isHolder; 821 822 /** 823 * Constructs an info record. 824 * 825 * @param simpleName check simple name 826 * @param link documentation link 827 * @param summary module summary 828 * @param isHolder whether holder 829 */ 830 private CheckInfo(String simpleName, String link, 831 String summary, boolean isHolder) { 832 this.simpleName = simpleName; 833 this.link = link; 834 this.summary = summary; 835 this.isHolder = isHolder; 836 } 837 } 838}