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