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