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.nio.file.Path; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Set; 029 030import org.apache.maven.doxia.macro.AbstractMacro; 031import org.apache.maven.doxia.macro.Macro; 032import org.apache.maven.doxia.macro.MacroExecutionException; 033import org.apache.maven.doxia.macro.MacroRequest; 034import org.apache.maven.doxia.module.xdoc.XdocSink; 035import org.apache.maven.doxia.sink.Sink; 036import org.codehaus.plexus.component.annotations.Component; 037 038import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 039 040/** 041 * A macro that inserts a table of properties for the given checkstyle module. 042 */ 043@Component(role = Macro.class, hint = "properties") 044public class PropertiesMacro extends AbstractMacro { 045 046 /** 047 * Constant value for cases when tokens set is empty. 048 */ 049 public static final String EMPTY = "empty"; 050 051 /** The string '{}'. */ 052 private static final String CURLY_BRACKET = "{}"; 053 054 /** The string 'subset of tokens'. */ 055 private static final String SUBSET_OF_TOKENS = "subset of tokens"; 056 057 /** Represents the relative path to the property types XML. */ 058 private static final String PROPERTY_TYPES_XML = "property_types.xml"; 059 060 /** The string '#'. */ 061 private static final String HASHTAG = "#"; 062 063 /** Represents the format string for constructing URLs with two placeholders. */ 064 private static final String URL_F = "%s#%s"; 065 066 /** Reflects start of a code segment. */ 067 private static final String CODE_START = "<code>"; 068 069 /** Reflects end of a code segment. */ 070 private static final String CODE_END = "</code>"; 071 072 /** 073 * This property is used to change the existing properties for javadoc. 074 * Tokens always present at the end of all properties. 075 */ 076 private static final String TOKENS_PROPERTY = SiteUtil.TOKENS; 077 078 /** The name of the current module being processed. */ 079 private static String currentModuleName = ""; 080 081 /** The file of the current module being processed. */ 082 private static Path currentModulePath = Path.of(""); 083 084 @Override 085 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 086 // until https://github.com/checkstyle/checkstyle/issues/13426 087 if (!(sink instanceof XdocSink xdocSink)) { 088 throw new MacroExecutionException("Expected Sink to be an XdocSink."); 089 } 090 091 final String modulePath = (String) request.getParameter("modulePath"); 092 093 configureGlobalProperties(modulePath); 094 095 writePropertiesTable(xdocSink); 096 } 097 098 /** 099 * Configures the global properties for the current module. 100 * 101 * @param modulePath the path of the current module processed. 102 * @throws MacroExecutionException if the module path is invalid. 103 */ 104 private static void configureGlobalProperties(String modulePath) 105 throws MacroExecutionException { 106 final String normalizedPath = modulePath.replace('\\', '/'); 107 final Path modulePathObj = Path.of(normalizedPath); 108 currentModulePath = modulePathObj; 109 final Path fileNamePath = modulePathObj.getFileName(); 110 111 if (fileNamePath == null) { 112 throw new MacroExecutionException( 113 "Invalid modulePath '" + modulePath + "': No file name present."); 114 } 115 116 currentModuleName = CommonUtil.getFileNameWithoutExtension( 117 fileNamePath.toString()); 118 } 119 120 /** 121 * Writes the properties table for the given module. Expects that the module has been processed 122 * with the ClassAndPropertiesSettersJavadocScraper before calling this method. 123 * 124 * @param sink the sink to write to. 125 * @throws MacroExecutionException if an error occurs during writing. 126 */ 127 private static void writePropertiesTable(XdocSink sink) 128 throws MacroExecutionException { 129 sink.table(); 130 sink.setInsertNewline(false); 131 sink.tableRows(null, false); 132 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 133 writeTableHeaderRow(sink); 134 writeTablePropertiesRows(sink); 135 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_10); 136 sink.tableRows_(); 137 sink.table_(); 138 sink.setInsertNewline(true); 139 } 140 141 /** 142 * Writes the table header row with 5 columns - name, description, type, default value, since. 143 * 144 * @param sink sink to write to. 145 */ 146 private static void writeTableHeaderRow(Sink sink) { 147 sink.tableRow(); 148 writeTableHeaderCell(sink, "name"); 149 writeTableHeaderCell(sink, "description"); 150 writeTableHeaderCell(sink, "type"); 151 writeTableHeaderCell(sink, "default value"); 152 writeTableHeaderCell(sink, "since"); 153 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 154 sink.tableRow_(); 155 } 156 157 /** 158 * Writes a table header cell with the given text. 159 * 160 * @param sink sink to write to. 161 * @param text the text to write. 162 */ 163 private static void writeTableHeaderCell(Sink sink, String text) { 164 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 165 sink.tableHeaderCell(); 166 sink.text(text); 167 sink.tableHeaderCell_(); 168 } 169 170 /** 171 * Writes the rows of the table with the 5 columns - name, description, type, default value, 172 * since. Each row corresponds to a property of the module. 173 * 174 * @param sink sink to write to. 175 * @throws MacroExecutionException if an error occurs during writing. 176 */ 177 private static void writeTablePropertiesRows(Sink sink) 178 throws MacroExecutionException { 179 final Object instance = SiteUtil.getModuleInstance(currentModuleName); 180 final Class<?> clss = instance.getClass(); 181 182 final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance); 183 final Map<String, PropertyDetails> propertiesDetails = SiteUtil 184 .buildPropertyDetails(properties, currentModuleName, currentModulePath, instance); 185 186 final List<String> orderedProperties = orderProperties(properties); 187 188 for (String propertyName : orderedProperties) { 189 try { 190 final PropertyDetails details = Objects 191 .requireNonNull(propertiesDetails.get(propertyName)); 192 writePropertyRow(sink, details); 193 } 194 // -@cs[IllegalCatch] we need to get details in wrapping exception 195 catch (Exception exc) { 196 final String message = String.format(Locale.ROOT, 197 "Exception while handling moduleName: %s propertyName: %s", 198 currentModuleName, propertyName); 199 throw new MacroExecutionException(message, exc); 200 } 201 } 202 } 203 204 /** 205 * Reorder properties to always have the 'tokens' property last (if present). 206 * 207 * @param properties module properties. 208 * @return Collection of ordered properties. 209 * 210 */ 211 private static List<String> orderProperties(Set<String> properties) { 212 final List<String> orderProperties = new ArrayList<>(properties); 213 if (orderProperties.remove(TOKENS_PROPERTY)) { 214 orderProperties.add(TOKENS_PROPERTY); 215 } 216 if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) { 217 orderProperties.add(SiteUtil.JAVADOC_TOKENS); 218 } 219 return List.copyOf(orderProperties); 220 } 221 222 /** 223 * Writes a table row with 5 columns for the given property - name, description, type, 224 * default value, since. 225 * 226 * @param sink sink to write to. 227 * @param details the property details. 228 * @throws MacroExecutionException if an error occurs during writing. 229 */ 230 private static void writePropertyRow(Sink sink, PropertyDetails details) 231 throws MacroExecutionException { 232 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 233 sink.tableRow(); 234 235 writePropertyNameCell(sink, details.getName()); 236 writePropertyDescriptionCell(sink, details.getDescription()); 237 writePropertyTypeCell(sink, details); 238 writePropertyDefaultValueCell(sink, details); 239 writePropertySinceVersionCell(sink, details.getSinceVersion()); 240 241 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 242 sink.tableRow_(); 243 } 244 245 /** 246 * Writes a table cell with the given property name. 247 * 248 * @param sink sink to write to. 249 * @param propertyName the name of the property. 250 */ 251 private static void writePropertyNameCell(Sink sink, String propertyName) { 252 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 253 sink.tableCell(); 254 sink.rawText("<a id=\"" + propertyName + "\"/>"); 255 sink.link(HASHTAG + propertyName); 256 sink.text(propertyName); 257 sink.link_(); 258 sink.tableCell_(); 259 } 260 261 /** 262 * Writes a table cell with the property description. 263 * 264 * @param sink sink to write to. 265 * @param description the description. 266 */ 267 private static void writePropertyDescriptionCell(Sink sink, String description) { 268 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 269 sink.tableCell(); 270 sink.rawText(description); 271 sink.tableCell_(); 272 } 273 274 /** 275 * Writes a table cell with the property type. 276 * 277 * @param sink sink to write to. 278 * @param details the property details. 279 * @throws MacroExecutionException if link to the property_types.html file cannot be 280 * constructed. 281 */ 282 private static void writePropertyTypeCell(Sink sink, PropertyDetails details) 283 throws MacroExecutionException { 284 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 285 sink.tableCell(); 286 287 final PropertyDetails.TokenPropertyType tokenPropertyType = 288 details.getTokenPropertyType(); 289 if (tokenPropertyType == PropertyDetails.TokenPropertyType.TOKEN_SET) { 290 sink.text("set of any supported"); 291 writeLink(sink); 292 } 293 else if (tokenPropertyType == PropertyDetails.TokenPropertyType.TOKEN_SUBSET) { 294 sink.text(SUBSET_OF_TOKENS); 295 writeTokensList(sink, details.getConfigurableTokens(), 296 SiteUtil.PATH_TO_TOKEN_TYPES, true); 297 } 298 else if (tokenPropertyType == PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET) { 299 sink.text("subset of javadoc tokens"); 300 writeTokensList(sink, details.getConfigurableTokens(), 301 SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true); 302 } 303 else { 304 final String type = details.getType(); 305 306 if (type != null && type.startsWith(SUBSET_OF_TOKENS)) { 307 processLinkForTokenTypes(sink); 308 } 309 else { 310 final String relativePathToPropertyTypes = 311 SiteUtil.getLinkToDocument(currentModuleName, PROPERTY_TYPES_XML); 312 final String escapedType; 313 if (type == null) { 314 escapedType = ""; 315 } 316 else { 317 escapedType = type.replace("[", ".5B") 318 .replace("]", ".5D"); 319 } 320 321 final String url = 322 String.format(Locale.ROOT, URL_F, relativePathToPropertyTypes, escapedType); 323 324 sink.link(url); 325 sink.text(Objects.requireNonNullElse(type, "")); 326 sink.link_(); 327 } 328 } 329 sink.tableCell_(); 330 } 331 332 /** 333 * Writes a formatted link for "TokenTypes" to the given sink. 334 * 335 * @param sink The output target where the link is written. 336 * @throws MacroExecutionException If an error occurs during the link processing. 337 */ 338 private static void processLinkForTokenTypes(Sink sink) 339 throws MacroExecutionException { 340 final String link = 341 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES); 342 343 sink.text("subset of tokens "); 344 sink.link(link); 345 sink.text("TokenTypes"); 346 sink.link_(); 347 } 348 349 /** 350 * Write a link when all types of token supported. 351 * 352 * @param sink sink to write to. 353 * @throws MacroExecutionException if link cannot be constructed. 354 */ 355 private static void writeLink(Sink sink) 356 throws MacroExecutionException { 357 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_16); 358 final String link = 359 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES); 360 sink.link(link); 361 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_20); 362 sink.text(SiteUtil.TOKENS); 363 sink.link_(); 364 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 365 } 366 367 /** 368 * Write a list of tokens with links to the tokenTypesLink file. 369 * 370 * @param sink sink to write to. 371 * @param tokens the list of tokens to write. 372 * @param tokenTypesLink the link to the token types file. 373 * @param printDotAtTheEnd defines if printing period symbols is required. 374 * @throws MacroExecutionException if link to the tokenTypesLink file cannot be constructed. 375 */ 376 private static void writeTokensList(Sink sink, List<String> tokens, String tokenTypesLink, 377 boolean printDotAtTheEnd) 378 throws MacroExecutionException { 379 for (int index = 0; index < tokens.size(); index++) { 380 final String token = tokens.get(index); 381 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_16); 382 if (index != 0) { 383 sink.text(SiteUtil.COMMA_SPACE); 384 } 385 writeLinkToToken(sink, tokenTypesLink, token); 386 } 387 if (tokens.isEmpty()) { 388 sink.rawText(CODE_START); 389 sink.text(EMPTY); 390 sink.rawText(CODE_END); 391 } 392 else if (printDotAtTheEnd) { 393 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_18); 394 sink.text(SiteUtil.DOT); 395 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 396 } 397 else { 398 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 399 } 400 } 401 402 /** 403 * Writes a link to the given token. 404 * 405 * @param sink sink to write to. 406 * @param document the document to link to. 407 * @param tokenName the name of the token. 408 * @throws MacroExecutionException if link to the document file cannot be constructed. 409 */ 410 private static void writeLinkToToken(Sink sink, String document, String tokenName) 411 throws MacroExecutionException { 412 final String link = SiteUtil.getLinkToDocument(currentModuleName, document) 413 + HASHTAG + tokenName; 414 sink.link(link); 415 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_20); 416 sink.text(tokenName); 417 sink.link_(); 418 } 419 420 /** 421 * Writes a table cell with the property default value. 422 * 423 * @param sink sink to write to. 424 * @param details the property details. 425 * @throws MacroExecutionException if an error occurs during retrieval of the default value. 426 */ 427 private static void writePropertyDefaultValueCell(Sink sink, PropertyDetails details) 428 throws MacroExecutionException { 429 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 430 sink.tableCell(); 431 432 final PropertyDetails.TokenPropertyType type = details.getTokenPropertyType(); 433 if (type == PropertyDetails.TokenPropertyType.TOKEN_SET 434 && SiteUtil.TOKENS.equals(details.getName())) { 435 writeAllTokensDefaultValue(sink, details); 436 } 437 else if (type == PropertyDetails.TokenPropertyType.TOKEN_SUBSET 438 || type == PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET 439 || !details.getDefaultValueTokens().isEmpty()) { 440 writeTokenSubsetDefaultValue(sink, details); 441 } 442 else { 443 writeStandardDefaultValue(sink, details); 444 } 445 446 sink.tableCell_(); 447 } 448 449 /** 450 * Writes the default value for properties that represent all tokens. 451 * 452 * @param sink sink to write to. 453 * @param details property details. 454 * @throws MacroExecutionException if an error occurs. 455 */ 456 private static void writeAllTokensDefaultValue(Sink sink, PropertyDetails details) 457 throws MacroExecutionException { 458 final List<String> defaultTokens = details.getDefaultValueTokens(); 459 if (defaultTokens.size() == 1 460 && SiteUtil.TOKEN_TYPES.equals(defaultTokens.getFirst())) { 461 sink.text(SiteUtil.TOKEN_TYPES); 462 } 463 else { 464 writeTokensList(sink, defaultTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true); 465 } 466 } 467 468 /** 469 * Writes the default value for token subset properties. 470 * 471 * @param sink sink to write to. 472 * @param details property details. 473 * @throws MacroExecutionException if an error occurs. 474 */ 475 private static void writeTokenSubsetDefaultValue(Sink sink, PropertyDetails details) 476 throws MacroExecutionException { 477 final PropertyDetails.TokenPropertyType type = details.getTokenPropertyType(); 478 final boolean printDot = type == PropertyDetails.TokenPropertyType.TOKEN_SUBSET 479 || type == PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET; 480 final String tokenTypesLink; 481 if (type == PropertyDetails.TokenPropertyType.JAVADOC_TOKEN_SUBSET) { 482 tokenTypesLink = SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES; 483 } 484 else { 485 tokenTypesLink = SiteUtil.PATH_TO_TOKEN_TYPES; 486 } 487 writeTokensList(sink, details.getDefaultValueTokens(), tokenTypesLink, printDot); 488 } 489 490 /** 491 * Writes a standard property default value. 492 * 493 * @param sink sink to write to. 494 * @param details property details. 495 */ 496 private static void writeStandardDefaultValue(Sink sink, PropertyDetails details) { 497 final String defaultValue = 498 getDisplayDefaultValue(details.getName(), details.getDefaultValue()); 499 if (defaultValue.isEmpty()) { 500 sink.rawText("<code/>"); 501 } 502 else { 503 sink.rawText(CODE_START); 504 sink.text(defaultValue); 505 sink.rawText(CODE_END); 506 } 507 } 508 509 /** 510 * Converts a raw default value from PropertyDetails into a human-readable display 511 * string for the properties table. This handles cases where the display value differs 512 * from the raw stored metadata value. These conversions must NOT be applied during 513 * XML metadata generation - they belong here in the macro only. 514 * 515 * @param propertyName the name of the property. 516 * @param rawDefault the raw default value stored in PropertyDetails. 517 * @return the display string for the default value table cell. 518 */ 519 private static String getDisplayDefaultValue(String propertyName, String rawDefault) { 520 final String result; 521 if (SiteUtil.FILE_EXTENSIONS.equals(propertyName) 522 && (rawDefault.isEmpty() || CURLY_BRACKET.equals(rawDefault))) { 523 result = "all files"; 524 } 525 else if (SiteUtil.CHARSET.equals(propertyName)) { 526 result = "the charset property of the parent" 527 + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module"; 528 } 529 else { 530 result = rawDefault; 531 } 532 return result; 533 } 534 535 /** 536 * Writes a table cell with the property since version. 537 * 538 * @param sink sink to write to. 539 * @param sinceVersion the since version. 540 */ 541 private static void writePropertySinceVersionCell(Sink sink, String sinceVersion) { 542 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 543 sink.tableCell(); 544 sink.text(sinceVersion); 545 sink.tableCell_(); 546 } 547}