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