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.nio.file.Path;
023import java.nio.file.Paths;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.regex.Pattern;
028
029import org.apache.maven.doxia.macro.AbstractMacro;
030import org.apache.maven.doxia.macro.Macro;
031import org.apache.maven.doxia.macro.MacroExecutionException;
032import org.apache.maven.doxia.macro.MacroRequest;
033import org.apache.maven.doxia.sink.Sink;
034import org.codehaus.plexus.component.annotations.Component;
035
036import com.puppycrawl.tools.checkstyle.api.DetailNode;
037import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
038import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraper;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
040import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
041
042/**
043 * A macro that inserts a notes subsection of module from its Javadoc.
044 */
045@Component(role = Macro.class, hint = "notes")
046public class NotesMacro extends AbstractMacro {
047
048    /** "Notes:" javadoc marking. */
049    public static final String NOTES = "Notes:";
050    /** New line escape character. */
051    private static final String NEWLINE = "\n";
052    /** A newline with 8 spaces of indentation. */
053    private static final String INDENT_LEVEL_8 = SiteUtil.getNewlineAndIndentSpaces(8);
054    /** A newline with 10 spaces of indentation. */
055    private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
056    /** A set of all html tags that need to be considered as text formatting for this macro. */
057    private static final Set<String> HTML_TEXT_FORMAT_TAGS = Set.of("<code>", "<a", "</a>", "<b>",
058        "</b>", "<strong>", "</strong>", "<i>", "</i>", "<em>", "</em>", "<small>", "</small>",
059        "<ins>", "<sub>", "<sup>");
060
061    @Override
062    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
063        final Path modulePath = Paths.get((String) request.getParameter("modulePath"));
064        final String moduleName = CommonUtil.getFileNameWithoutExtension(modulePath.toString());
065
066        final Set<String> propertyNames = getPropertyNames(moduleName);
067        final Map<String, DetailNode> propertiesJavadocs = SiteUtil.getPropertiesJavadocs(
068            propertyNames, moduleName, modulePath);
069
070        final DetailNode moduleJavadoc = propertiesJavadocs.get(moduleName);
071
072        final int notesStartIndex = getNotesStartIndex(moduleJavadoc) - 1;
073        final int notesEndIndex = getNotesEndIndex(moduleJavadoc, propertyNames);
074
075        final Pattern notesLine = Pattern.compile("\r?\n\\s?" + NOTES);
076        final String moduleNotes = notesLine.matcher(JavadocMetadataScraper.constructSubTreeText(
077            moduleJavadoc, notesStartIndex, notesEndIndex)).replaceAll("");
078
079        writeOutNotes(moduleNotes, sink);
080
081    }
082
083    /**
084     * Assigns values to each instance variable.
085     *
086     * @param moduleName name of module.
087     * @return set of property names.
088     * @throws MacroExecutionException if the module could not be retrieved.
089     */
090    private static Set<String> getPropertyNames(String moduleName)
091            throws MacroExecutionException {
092        final Object instance = SiteUtil.getModuleInstance(moduleName);
093        final Class<?> clss = instance.getClass();
094
095        return SiteUtil.getPropertiesForDocumentation(clss, instance);
096    }
097
098    /**
099     * Gets the start index of the Notes section.
100     *
101     * @param moduleJavadoc javadoc of module.
102     * @return start index.
103     */
104    private static int getNotesStartIndex(DetailNode moduleJavadoc) {
105        int notesStartIndex = 0;
106
107        for (DetailNode node : moduleJavadoc.getChildren()) {
108            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
109                final DetailNode paragraphNode = JavadocUtil.findFirstToken(
110                    node, JavadocTokenTypes.PARAGRAPH);
111                if (paragraphNode != null && JavadocMetadataScraper.isChildNodeTextMatches(
112                    paragraphNode, Pattern.compile("\\s*" + NOTES + "$"))) {
113
114                    notesStartIndex = node.getIndex();
115                    break;
116                }
117            }
118        }
119
120        return notesStartIndex;
121    }
122
123    /**
124     * Gets the end index of the Notes.
125     *
126     * @param moduleJavadoc javadoc of module.
127     * @param propertyNamesSet Set with property names.
128     * @return the end index.
129     */
130    private static int getNotesEndIndex(DetailNode moduleJavadoc,
131                                              Set<String> propertyNamesSet) {
132        int notesEndIndex = -1;
133
134        if (propertyNamesSet.isEmpty()) {
135            notesEndIndex += getParentSectionStartIndex(moduleJavadoc);
136        }
137        else {
138            final String somePropertyName = propertyNamesSet.iterator().next();
139
140            final Optional<DetailNode> somePropertyModuleNode =
141                SiteUtil.getPropertyJavadocNodeInModule(
142                    somePropertyName, moduleJavadoc);
143
144            if (somePropertyModuleNode.isPresent()) {
145                notesEndIndex += JavadocMetadataScraper
146                    .getParentIndexOf(somePropertyModuleNode.get());
147            }
148        }
149
150        return notesEndIndex;
151    }
152
153    /**
154     * Gets the start index of the parent subsection in module's JavaDoc.
155     *
156     * @param moduleJavadoc javadoc of module.
157     * @return start index of parent subsection.
158     */
159    private static int getParentSectionStartIndex(DetailNode moduleJavadoc) {
160        int parentStartIndex = 0;
161
162        for (DetailNode node : moduleJavadoc.getChildren()) {
163            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
164                final DetailNode paragraphNode = JavadocUtil.findFirstToken(
165                    node, JavadocTokenTypes.PARAGRAPH);
166                if (paragraphNode != null && JavadocMetadataScraper.isParentText(paragraphNode)) {
167                    parentStartIndex = node.getIndex();
168                    break;
169                }
170            }
171        }
172
173        return parentStartIndex;
174    }
175
176    /**
177     * Writes the notes into xdoc.
178     *
179     * @param notes notes of the module.
180     * @param sink sink of the macro.
181     */
182    private static void writeOutNotes(String notes, Sink sink) {
183        final String[] moduleNotesLinesSplit = notes.split(NEWLINE);
184
185        sink.rawText(moduleNotesLinesSplit[0]);
186        String previousProcessedLine = moduleNotesLinesSplit[0];
187        for (int index = 1; index < moduleNotesLinesSplit.length; index++) {
188            final String currentLine = moduleNotesLinesSplit[index].trim();
189            final String processedLine;
190
191            if (currentLine.isEmpty()) {
192                processedLine = NEWLINE;
193            }
194            else if (currentLine.startsWith("<")
195                && !startsWithTextFormattingHtmlTag(currentLine)) {
196
197                processedLine = INDENT_LEVEL_8 + currentLine;
198            }
199            else if (index > 1
200                && (previousProcessedLine.contains("<pre")
201                    || !previousProcessedLine.startsWith(INDENT_LEVEL_8))) {
202
203                final String currentLineWithPreservedIndent = moduleNotesLinesSplit[index]
204                    .substring(1);
205                processedLine = NEWLINE + currentLineWithPreservedIndent;
206            }
207            else {
208                processedLine = INDENT_LEVEL_10 + currentLine;
209            }
210
211            sink.rawText(processedLine);
212
213            previousProcessedLine = processedLine;
214        }
215
216    }
217
218    /**
219     * Checks if given line starts with HTML text-formatting tag.
220     *
221     * @param line line to check on.
222     * @return whether given line starts with HTML text-formatting tag.
223     */
224    private static boolean startsWithTextFormattingHtmlTag(String line) {
225        boolean result = false;
226
227        for (String tag : HTML_TEXT_FORMAT_TAGS) {
228            if (line.startsWith(tag)) {
229                result = true;
230                break;
231            }
232        }
233
234        return result;
235    }
236
237}