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 * &lt;macro name="allCheckSummaries"/&gt;
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 * &lt;macro name="allCheckSummaries"&gt;
061 *   &lt;param name="package" value="annotation"/&gt;
062 * &lt;/macro&gt;
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 '&amp;' 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("&amp;");
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}