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}