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}