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