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