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