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