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 description of module from its Javadoc. 044 */ 045@Component(role = Macro.class, hint = "description") 046public class DescriptionMacro extends AbstractMacro { 047 048 /** New line escape character. */ 049 private static final String NEWLINE = "\n"; 050 /** A newline with 8 spaces of indentation. */ 051 private static final String INDENT_LEVEL_8 = SiteUtil.getNewlineAndIndentSpaces(8); 052 /** A newline with 10 spaces of indentation. */ 053 private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10); 054 /** A set of all html tags that need to be considered as text formatting for this macro. */ 055 private static final Set<String> HTML_TEXT_FORMAT_TAGS = Set.of("<code>", "<a", "</a>", "<b>", 056 "</b>", "<strong>", "</strong>", "<i>", "</i>", "<em>", "</em>", "<small>", "</small>", 057 "<ins>", "<sub>", "<sup>"); 058 /** "Notes:" javadoc marking. */ 059 private static final String NOTES = "Notes:"; 060 /** "Notes:" line. */ 061 private static final Pattern NOTES_LINE = Pattern.compile("\\s*" + NOTES + "$"); 062 063 @Override 064 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 065 final Path modulePath = Paths.get((String) request.getParameter("modulePath")); 066 final String moduleName = CommonUtil.getFileNameWithoutExtension(modulePath.toString()); 067 068 final Set<String> propertyNames = getPropertyNames(moduleName); 069 final Map<String, DetailNode> propertiesJavadocs = SiteUtil.getPropertiesJavadocs( 070 propertyNames, moduleName, modulePath); 071 072 final DetailNode moduleJavadoc = propertiesJavadocs.get(moduleName); 073 074 final int descriptionEndIndex = getDescriptionEndIndex(moduleJavadoc, propertyNames); 075 final String moduleDescription = JavadocMetadataScraper.constructSubTreeText( 076 moduleJavadoc, 0, descriptionEndIndex); 077 078 writeOutDescription(moduleDescription, sink); 079 080 } 081 082 /** 083 * Assigns values to each instance variable. 084 * 085 * @param moduleName name of module. 086 * @return set of property names. 087 * @throws MacroExecutionException if the module could not be retrieved. 088 */ 089 private static Set<String> getPropertyNames(String moduleName) 090 throws MacroExecutionException { 091 final Object instance = SiteUtil.getModuleInstance(moduleName); 092 final Class<?> clss = instance.getClass(); 093 094 return SiteUtil.getPropertiesForDocumentation(clss, instance); 095 } 096 097 /** 098 * Gets the end index of the description. 099 * 100 * @param moduleJavadoc javadoc of module. 101 * @param propertyNamesSet Set with property names. 102 * @return the end index. 103 */ 104 private static int getDescriptionEndIndex(DetailNode moduleJavadoc, 105 Set<String> propertyNamesSet) { 106 int descriptionEndIndex = -1; 107 108 final int notesStartingIndex = getNotesStartIndex(moduleJavadoc); 109 if (notesStartingIndex > -1) { 110 descriptionEndIndex += notesStartingIndex; 111 } 112 else if (propertyNamesSet.isEmpty()) { 113 descriptionEndIndex += getParentSectionStartIndex(moduleJavadoc); 114 } 115 else { 116 final String somePropertyName = propertyNamesSet.iterator().next(); 117 118 final Optional<DetailNode> somePropertyModuleNode = 119 SiteUtil.getPropertyJavadocNodeInModule( 120 somePropertyName, moduleJavadoc); 121 122 if (somePropertyModuleNode.isPresent()) { 123 descriptionEndIndex += JavadocMetadataScraper 124 .getParentIndexOf(somePropertyModuleNode.get()); 125 } 126 } 127 128 return descriptionEndIndex; 129 } 130 131 /** 132 * Gets the start index of the Notes section. 133 * 134 * @param moduleJavadoc javadoc of module. 135 * @return start index. 136 */ 137 public static int getNotesStartIndex(DetailNode moduleJavadoc) { 138 int notesStartIndex = -1; 139 140 for (DetailNode node : moduleJavadoc.getChildren()) { 141 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT 142 && isStartOfNotesSection(node)) { 143 144 notesStartIndex += node.getIndex(); 145 break; 146 } 147 } 148 149 return notesStartIndex; 150 } 151 152 /** 153 * Determines whether the given HTML node marks the start of the "Notes" section. 154 * 155 * @param htmlElement html element to check. 156 * @return true if the element starts the "Notes" section, false otherwise. 157 */ 158 private static boolean isStartOfNotesSection(DetailNode htmlElement) { 159 final DetailNode paragraphNode = JavadocUtil.findFirstToken( 160 htmlElement, JavadocTokenTypes.PARAGRAPH); 161 final Optional<DetailNode> liNode = getLiTagNode(htmlElement); 162 163 return paragraphNode != null && JavadocMetadataScraper.isChildNodeTextMatches( 164 paragraphNode, NOTES_LINE) 165 || liNode.isPresent() && JavadocMetadataScraper.isChildNodeTextMatches( 166 liNode.get(), NOTES_LINE); 167 } 168 169 /** 170 * Gets the node of Li HTML tag. 171 * 172 * @param htmlElement html element to get li tag from. 173 * @return Optional of li tag node. 174 */ 175 private static Optional<DetailNode> getLiTagNode(DetailNode htmlElement) { 176 return Optional.of(htmlElement) 177 .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.HTML_TAG)) 178 .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.HTML_ELEMENT)) 179 .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.LI)); 180 } 181 182 /** 183 * Gets the starting index of the "Parent is" paragraph in module's javadoc. 184 * 185 * @param moduleJavadoc javadoc of module. 186 * @return start index of parent subsection. 187 */ 188 private static int getParentSectionStartIndex(DetailNode moduleJavadoc) { 189 int parentStartIndex = 0; 190 191 for (DetailNode node : moduleJavadoc.getChildren()) { 192 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) { 193 final DetailNode paragraphNode = JavadocUtil.findFirstToken( 194 node, JavadocTokenTypes.PARAGRAPH); 195 if (paragraphNode != null && JavadocMetadataScraper.isParentText(paragraphNode)) { 196 parentStartIndex = node.getIndex(); 197 break; 198 } 199 } 200 } 201 202 return parentStartIndex; 203 } 204 205 /** 206 * Writes the description into xdoc. 207 * 208 * @param description description of the module. 209 * @param sink sink of the macro. 210 */ 211 private static void writeOutDescription(String description, Sink sink) { 212 final String[] moduleDescriptionLinesSplit = description.split(NEWLINE); 213 214 sink.rawText(moduleDescriptionLinesSplit[0]); 215 String lastHtmlTag = moduleDescriptionLinesSplit[0]; 216 217 for (int index = 1; index < moduleDescriptionLinesSplit.length; index++) { 218 final String currentLine = moduleDescriptionLinesSplit[index].trim(); 219 final String processedLine; 220 221 if (currentLine.isEmpty()) { 222 processedLine = NEWLINE; 223 } 224 else if (currentLine.startsWith("<") 225 && !startsWithTextFormattingHtmlTag(currentLine)) { 226 227 processedLine = INDENT_LEVEL_8 + currentLine; 228 lastHtmlTag = currentLine; 229 } 230 else if (lastHtmlTag.contains("<pre")) { 231 final String currentLineWithPreservedIndent = moduleDescriptionLinesSplit[index] 232 .substring(1); 233 234 processedLine = NEWLINE + currentLineWithPreservedIndent; 235 } 236 else { 237 processedLine = INDENT_LEVEL_10 + currentLine; 238 } 239 240 sink.rawText(processedLine); 241 } 242 243 } 244 245 /** 246 * Checks if given line starts with HTML text-formatting tag. 247 * 248 * @param line line to check on. 249 * @return whether given line starts with HTML text-formatting tag. 250 */ 251 private static boolean startsWithTextFormattingHtmlTag(String line) { 252 boolean result = false; 253 254 for (String tag : HTML_TEXT_FORMAT_TAGS) { 255 if (line.startsWith(tag)) { 256 result = true; 257 break; 258 } 259 } 260 261 return result; 262 } 263 264}