View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.nio.file.Path;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  
30  import org.apache.maven.doxia.macro.AbstractMacro;
31  import org.apache.maven.doxia.macro.Macro;
32  import org.apache.maven.doxia.macro.MacroExecutionException;
33  import org.apache.maven.doxia.macro.MacroRequest;
34  import org.apache.maven.doxia.module.xdoc.XdocSink;
35  import org.apache.maven.doxia.sink.Sink;
36  import org.codehaus.plexus.component.annotations.Component;
37  
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39  
40  /**
41   * A macro that inserts a table of properties for the given checkstyle module.
42   */
43  @Component(role = Macro.class, hint = "properties")
44  public class PropertiesMacro extends AbstractMacro {
45  
46      /**
47       * Constant value for cases when tokens set is empty.
48       */
49      public static final String EMPTY = "empty";
50  
51      /** The string '{}'. */
52      private static final String CURLY_BRACKET = "{}";
53  
54      /** The string 'subset of tokens'. */
55      private static final String SUBSET_OF_TOKENS = "subset of tokens";
56  
57      /** Represents the relative path to the property types XML. */
58      private static final String PROPERTY_TYPES_XML = "property_types.xml";
59  
60      /** The string '#'. */
61      private static final String HASHTAG = "#";
62  
63      /** Represents the format string for constructing URLs with two placeholders. */
64      private static final String URL_F = "%s#%s";
65  
66      /** Reflects start of a code segment. */
67      private static final String CODE_START = "<code>";
68  
69      /** Reflects end of a code segment. */
70      private static final String CODE_END = "</code>";
71  
72      /**
73       * This property is used to change the existing properties for javadoc.
74       * Tokens always present at the end of all properties.
75       */
76      private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
77  
78      /** The name of the current module being processed. */
79      private static String currentModuleName = "";
80  
81      /** The file of the current module being processed. */
82      private static Path currentModulePath = Path.of("");
83  
84      @Override
85      public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
86          // until https://github.com/checkstyle/checkstyle/issues/13426
87          if (!(sink instanceof XdocSink xdocSink)) {
88              throw new MacroExecutionException("Expected Sink to be an XdocSink.");
89          }
90  
91          final String modulePath = (String) request.getParameter("modulePath");
92  
93          configureGlobalProperties(modulePath);
94  
95          writePropertiesTable(xdocSink);
96      }
97  
98      /**
99       * 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 }