001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.lang.reflect.Field;
023import java.util.Set;
024import java.util.regex.Pattern;
025
026import org.apache.maven.doxia.macro.MacroExecutionException;
027import org.apache.maven.doxia.sink.Sink;
028
029import com.puppycrawl.tools.checkstyle.PropertyType;
030import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
031import com.puppycrawl.tools.checkstyle.api.DetailNode;
032import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
033import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraperUtil;
034import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
035
036/**
037 * Utility class for parsing javadocs of modules.
038 */
039public final class ModuleJavadocParsingUtil {
040    /** New line escape character. */
041    public static final String NEWLINE = System.lineSeparator();
042    /** A newline with 4 spaces of indentation. */
043    public static final String INDENT_LEVEL_4 = SiteUtil.getNewlineAndIndentSpaces(4);
044    /** A newline with 6 spaces of indentation. */
045    public static final String INDENT_LEVEL_6 = SiteUtil.getNewlineAndIndentSpaces(6);
046    /** A newline with 8 spaces of indentation. */
047    public static final String INDENT_LEVEL_8 = SiteUtil.getNewlineAndIndentSpaces(8);
048    /** A newline with 10 spaces of indentation. */
049    public static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
050    /** A newline with 12 spaces of indentation. */
051    public static final String INDENT_LEVEL_12 = SiteUtil.getNewlineAndIndentSpaces(12);
052    /** A newline with 14 spaces of indentation. */
053    public static final String INDENT_LEVEL_14 = SiteUtil.getNewlineAndIndentSpaces(14);
054    /** A newline with 16 spaces of indentation. */
055    public static final String INDENT_LEVEL_16 = SiteUtil.getNewlineAndIndentSpaces(16);
056    /** A newline with 18 spaces of indentation. */
057    public static final String INDENT_LEVEL_18 = SiteUtil.getNewlineAndIndentSpaces(18);
058    /** A newline with 20 spaces of indentation. */
059    public static final String INDENT_LEVEL_20 = SiteUtil.getNewlineAndIndentSpaces(20);
060    /** A set of all html tags that need to be considered as text formatting for this macro. */
061    public static final Set<String> HTML_TEXT_FORMAT_TAGS = Set.of("<code>", "<a", "</a>", "<b>",
062        "</b>", "<strong>", "</strong>", "<i>", "</i>", "<em>", "</em>", "<small>", "</small>",
063        "<ins>", "<sub>", "<sup>");
064    /** "Notes:" javadoc marking. */
065    public static final String NOTES = "Notes:";
066    /** "Notes:" line. */
067    public static final Pattern NOTES_LINE = Pattern.compile("\\s*" + NOTES + "$");
068    /** "Notes:" line with new line accounted. */
069    public static final Pattern NOTES_LINE_WITH_NEWLINE = Pattern.compile("\r?\n\\s?" + NOTES);
070
071    /**
072     * Private utility constructor.
073     */
074    private ModuleJavadocParsingUtil() {
075    }
076
077    /**
078     * Gets properties of the specified module.
079     *
080     * @param moduleName name of module.
081     * @return set of properties name if present, otherwise null.
082     * @throws MacroExecutionException if the module could not be retrieved.
083     */
084    public static Set<String> getPropertyNames(String moduleName)
085            throws MacroExecutionException {
086        final Object instance = SiteUtil.getModuleInstance(moduleName);
087        final Class<?> clss = instance.getClass();
088
089        return SiteUtil.getPropertiesForDocumentation(clss, instance);
090    }
091
092    /**
093     * Determines whether the given HTML node marks the start of the "Notes" section.
094     *
095     * @param htmlElement html element to check.
096     * @return true if the element starts the "Notes" section, false otherwise.
097     */
098    private static boolean isStartOfNotesSection(DetailNode htmlElement) {
099        boolean result = false;
100        if (htmlElement != null) {
101            final DetailNode htmlContentNode = JavadocUtil.findFirstToken(
102                htmlElement, JavadocCommentsTokenTypes.HTML_CONTENT);
103
104            result = htmlContentNode != null && JavadocMetadataScraperUtil.isChildNodeTextMatches(
105                htmlContentNode, NOTES_LINE);
106        }
107        return result;
108    }
109
110    /**
111     * Writes the given javadoc chunk into xdoc.
112     *
113     * @param javadocPortion javadoc text.
114     * @param sink sink of the macro.
115     */
116    public static void writeOutJavadocPortion(String javadocPortion, Sink sink) {
117        final String[] javadocPortionLinesSplit = javadocPortion.split(NEWLINE
118            .replace("\r", ""), -1);
119
120        sink.rawText(javadocPortionLinesSplit[0]);
121        String lastHtmlTag = javadocPortionLinesSplit[0];
122
123        for (int index = 1; index < javadocPortionLinesSplit.length; index++) {
124            final String currentLine = javadocPortionLinesSplit[index].trim();
125            final String processedLine;
126
127            if (currentLine.isEmpty()) {
128                processedLine = NEWLINE;
129            }
130            else if (currentLine.startsWith("<")
131                && !startsWithTextFormattingHtmlTag(currentLine)) {
132
133                processedLine = INDENT_LEVEL_8 + currentLine;
134                lastHtmlTag = currentLine;
135            }
136            else if (lastHtmlTag.contains("<pre")) {
137                final String currentLineWithPreservedIndent = javadocPortionLinesSplit[index]
138                    .substring(1);
139
140                processedLine = NEWLINE + currentLineWithPreservedIndent;
141            }
142            else {
143                processedLine = INDENT_LEVEL_10 + currentLine;
144            }
145
146            sink.rawText(processedLine);
147        }
148
149    }
150
151    /**
152     * Checks if given line starts with HTML text-formatting tag.
153     *
154     * @param line line to check on.
155     * @return whether given line starts with HTML text-formatting tag.
156     */
157    public static boolean startsWithTextFormattingHtmlTag(String line) {
158        boolean result = false;
159
160        for (String tag : HTML_TEXT_FORMAT_TAGS) {
161            if (line.startsWith(tag)) {
162                result = true;
163                break;
164            }
165        }
166
167        return result;
168    }
169
170    /**
171     * Gets the description of module from module javadoc.
172     *
173     * @param moduleJavadoc module javadoc.
174     * @return module description.
175     */
176    public static String getModuleDescription(DetailNode moduleJavadoc) {
177        final DetailNode descriptionEndNode = getDescriptionEndNode(moduleJavadoc);
178        String result = "";
179        if (descriptionEndNode != null) {
180            result = JavadocMetadataScraperUtil.constructSubTreeText(moduleJavadoc,
181                    descriptionEndNode);
182        }
183        return result;
184    }
185
186    /**
187     * Gets the {@code @since} version of module from module javadoc.
188     *
189     * @param moduleJavadoc module javadoc
190     * @return module {@code @since} version. For instance, {@code 8.0}
191     */
192    public static String getModuleSinceVersion(DetailNode moduleJavadoc) {
193        final DetailNode sinceTagNode = getModuleSinceVersionTagStartNode(moduleJavadoc);
194        String result = "";
195
196        if (sinceTagNode == null) {
197            result = "";
198        }
199        else if (sinceTagNode.getFirstChild() != null) {
200            result = JavadocMetadataScraperUtil.constructSubTreeText(sinceTagNode,
201                    sinceTagNode.getFirstChild()).replace("@since ", "");
202        }
203        return result;
204    }
205
206    /**
207     * Gets the end node of the description.
208     *
209     * @param moduleJavadoc javadoc of module.
210     * @return the end index.
211     */
212    public static DetailNode getDescriptionEndNode(DetailNode moduleJavadoc) {
213        final DetailNode descriptionEndNode;
214
215        final DetailNode notesStartingNode =
216            getNotesSectionStartNode(moduleJavadoc);
217
218        if (notesStartingNode != null) {
219            descriptionEndNode = notesStartingNode.getPreviousSibling();
220        }
221        else {
222            descriptionEndNode = getNodeBeforeJavadocTags(moduleJavadoc);
223        }
224
225        return descriptionEndNode;
226    }
227
228    /**
229     * Gets the start node of the Notes section.
230     *
231     * @param moduleJavadoc javadoc of module.
232     * @return start node.
233     */
234    public static DetailNode getNotesSectionStartNode(DetailNode moduleJavadoc) {
235        DetailNode notesStartNode = null;
236        if (moduleJavadoc != null) {
237            DetailNode node = moduleJavadoc.getFirstChild();
238
239            while (node != null) {
240                if (isNotesSectionNode(node)) {
241                    notesStartNode = node;
242                    break;
243                }
244                node = node.getNextSibling();
245            }
246        }
247
248        return notesStartNode;
249    }
250
251    /**
252     * Checks if the given node is the start of the Notes section.
253     *
254     * @param node the node to check.
255     * @return true if the node is the start of the Notes section, false otherwise.
256     */
257    private static boolean isNotesSectionNode(DetailNode node) {
258        boolean result = false;
259        if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
260            if (JavadocUtil.isTag(node, "ul")) {
261                final DetailNode htmlContentNode = JavadocUtil.findFirstToken(
262                    node, JavadocCommentsTokenTypes.HTML_CONTENT);
263                result = htmlContentNode != null
264                        && isStartOfNotesSection(htmlContentNode.getFirstChild());
265            }
266            else {
267                result = (JavadocUtil.isTag(node, "p")
268                            || JavadocUtil.isTag(node, "li"))
269                            && isStartOfNotesSection(node);
270            }
271        }
272        return result;
273    }
274
275    /**
276     * Gets the node representing the start of the {@code @since} version tag
277     * in the module's Javadoc.
278     *
279     * @param moduleJavadoc the root Javadoc node of the module
280     * @return the {@code @since} tag start node, or {@code null} if not found
281     */
282    public static DetailNode getModuleSinceVersionTagStartNode(DetailNode moduleJavadoc) {
283        DetailNode result = null;
284
285        if (moduleJavadoc != null) {
286            result = JavadocUtil.getAllNodesOfType(
287                    moduleJavadoc, JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG).stream()
288                .filter(javadocTag -> {
289                    final DetailNode firstChild = javadocTag.getFirstChild();
290                    return firstChild != null
291                            && firstChild.getType()
292                                == JavadocCommentsTokenTypes.SINCE_BLOCK_TAG;
293                })
294                .findFirst()
295                .orElse(null);
296        }
297        return result;
298    }
299
300    /**
301     * Gets the node of module's javadoc whose next sibling is a node that defines a javadoc tag.
302     *
303     * @param moduleJavadoc the root Javadoc node of the module
304     * @return the node that precedes node defining javadoc tag if present,
305     *     otherwise just the last node of module's javadoc.
306     */
307    public static DetailNode getNodeBeforeJavadocTags(DetailNode moduleJavadoc) {
308        DetailNode nodeBeforeJavadocTags = null;
309
310        if (moduleJavadoc != null) {
311            nodeBeforeJavadocTags = moduleJavadoc.getFirstChild();
312
313            if (nodeBeforeJavadocTags != null) {
314                while (nodeBeforeJavadocTags.getNextSibling() != null
315                        && nodeBeforeJavadocTags.getNextSibling().getType()
316                            != JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
317
318                    nodeBeforeJavadocTags = nodeBeforeJavadocTags.getNextSibling();
319                }
320            }
321        }
322
323        return nodeBeforeJavadocTags;
324    }
325
326    /**
327     * Gets the Notes section of module from module javadoc.
328     *
329     * @param moduleJavadoc module javadoc.
330     * @return Notes section of module.
331     */
332    public static String getModuleNotes(DetailNode moduleJavadoc) {
333        final String result;
334
335        final DetailNode notesStartNode = getNotesSectionStartNode(moduleJavadoc);
336
337        if (notesStartNode == null) {
338            result = "";
339        }
340        else {
341            final DetailNode notesEndNode = getNodeBeforeJavadocTags(moduleJavadoc);
342
343            if (notesEndNode == null) {
344                result = "";
345            }
346            else {
347                final String unprocessedNotes = JavadocMetadataScraperUtil.constructSubTreeText(
348                            notesStartNode, notesEndNode);
349                result = NOTES_LINE_WITH_NEWLINE.matcher(unprocessedNotes).replaceAll("");
350            }
351        }
352
353        return result;
354    }
355
356    /**
357     * Checks whether property is to contain tokens.
358     *
359     * @param propertyField property field.
360     * @return true if property is to contain tokens, false otherwise.
361     */
362    public static boolean isPropertySpecialTokenProp(Field propertyField) {
363        boolean result = false;
364
365        if (propertyField != null) {
366            final XdocsPropertyType fieldXdocAnnotation =
367                propertyField.getAnnotation(XdocsPropertyType.class);
368
369            result = fieldXdocAnnotation != null
370                && fieldXdocAnnotation.value() == PropertyType.TOKEN_ARRAY;
371        }
372
373        return result;
374    }
375
376}