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