001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.io.File;
023import java.lang.reflect.Field;
024import java.util.Arrays;
025import java.util.Collections;
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;
032import java.util.stream.Collectors;
033
034import org.apache.maven.doxia.macro.AbstractMacro;
035import org.apache.maven.doxia.macro.Macro;
036import org.apache.maven.doxia.macro.MacroExecutionException;
037import org.apache.maven.doxia.macro.MacroRequest;
038import org.apache.maven.doxia.module.xdoc.XdocSink;
039import org.apache.maven.doxia.sink.Sink;
040import org.codehaus.plexus.component.annotations.Component;
041
042import com.puppycrawl.tools.checkstyle.PropertyType;
043import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
044import com.puppycrawl.tools.checkstyle.api.DetailNode;
045import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
046import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
047import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
048import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
049
050/**
051 * A macro that inserts a table of properties for the given checkstyle module.
052 */
053@Component(role = Macro.class, hint = "properties")
054public class PropertiesMacro extends AbstractMacro {
055
056    /**
057     * Constant value for cases when tokens set is empty.
058     */
059    public static final String EMPTY = "empty";
060
061    /** Set of properties not inherited from the base token configuration. */
062    public static final Set<String> NON_BASE_TOKEN_PROPERTIES = Collections.unmodifiableSet(
063            Arrays.stream(new String[] {
064                "AtclauseOrder - target",
065                "DescendantToken - limitedTokens",
066                "IllegalType - memberModifiers",
067                "MagicNumber - constantWaiverParentToken",
068                "MultipleStringLiterals - ignoreOccurrenceContext",
069            }).collect(Collectors.toSet()));
070
071    /** The precompiled pattern for a comma followed by a space. */
072    private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
073
074    /** The precompiled pattern for a Check. */
075    private static final Pattern CHECK_PATTERN = Pattern.compile("Check$");
076
077    /** The string '{}'. */
078    private static final String CURLY_BRACKET = "{}";
079
080    /** Represents the relative path to the property types XML. */
081    private static final String PROPERTY_TYPES_XML = "property_types.xml";
082
083    /** Represents the format string for constructing URLs with two placeholders. */
084    private static final String URL_F = "%s#%s";
085
086    /** Reflects start of a code segment. */
087    private static final String CODE_START = "<code>";
088
089    /** Reflects end of a code segment. */
090    private static final String CODE_END = "</code>";
091
092    /** A newline with 10 spaces of indentation. */
093    private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
094    /** A newline with 12 spaces of indentation. */
095    private static final String INDENT_LEVEL_12 = SiteUtil.getNewlineAndIndentSpaces(12);
096    /** A newline with 14 spaces of indentation. */
097    private static final String INDENT_LEVEL_14 = SiteUtil.getNewlineAndIndentSpaces(14);
098    /** A newline with 16 spaces of indentation. */
099    private static final String INDENT_LEVEL_16 = SiteUtil.getNewlineAndIndentSpaces(16);
100    /** A newline with 18 spaces of indentation. */
101    private static final String INDENT_LEVEL_18 = SiteUtil.getNewlineAndIndentSpaces(18);
102    /** A newline with 20 spaces of indentation. */
103    private static final String INDENT_LEVEL_20 = SiteUtil.getNewlineAndIndentSpaces(20);
104
105    /**
106     * This property is used to change the existing properties for javadoc.
107     * Tokens always present at the end of all properties.
108     */
109    private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
110
111    /** The name of the current module being processed. */
112    private static String currentModuleName = "";
113
114    /** The file of the current module being processed. */
115    private static File currentModuleFile = new File("");
116
117    @Override
118    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
119        // until https://github.com/checkstyle/checkstyle/issues/13426
120        if (!(sink instanceof XdocSink)) {
121            throw new MacroExecutionException("Expected Sink to be an XdocSink.");
122        }
123
124        final String modulePath = (String) request.getParameter("modulePath");
125
126        configureGlobalProperties(modulePath);
127
128        writePropertiesTable((XdocSink) sink);
129    }
130
131    /**
132     * Configures the global properties for the current module.
133     *
134     * @param modulePath the path of the current module processed.
135     */
136    private static void configureGlobalProperties(String modulePath) {
137        final File moduleFile = new File(modulePath);
138        currentModuleFile = moduleFile;
139        currentModuleName = CommonUtil.getFileNameWithoutExtension(moduleFile.getName());
140    }
141
142    /**
143     * Writes the properties table for the given module. Expects that the module has been processed
144     * with the ClassAndPropertiesSettersJavadocScraper before calling this method.
145     *
146     * @param sink the sink to write to.
147     * @throws MacroExecutionException if an error occurs during writing.
148     */
149    private static void writePropertiesTable(XdocSink sink)
150            throws MacroExecutionException {
151        sink.table();
152        sink.setInsertNewline(false);
153        sink.tableRows(null, false);
154        sink.rawText(INDENT_LEVEL_12);
155        writeTableHeaderRow(sink);
156        writeTablePropertiesRows(sink);
157        sink.rawText(INDENT_LEVEL_10);
158        sink.tableRows_();
159        sink.table_();
160        sink.setInsertNewline(true);
161    }
162
163    /**
164     * Writes the table header row with 5 columns - name, description, type, default value, since.
165     *
166     * @param sink sink to write to.
167     */
168    private static void writeTableHeaderRow(Sink sink) {
169        sink.tableRow();
170        writeTableHeaderCell(sink, "name");
171        writeTableHeaderCell(sink, "description");
172        writeTableHeaderCell(sink, "type");
173        writeTableHeaderCell(sink, "default value");
174        writeTableHeaderCell(sink, "since");
175        sink.rawText(INDENT_LEVEL_12);
176        sink.tableRow_();
177    }
178
179    /**
180     * Writes a table header cell with the given text.
181     *
182     * @param sink sink to write to.
183     * @param text the text to write.
184     */
185    private static void writeTableHeaderCell(Sink sink, String text) {
186        sink.rawText(INDENT_LEVEL_14);
187        sink.tableHeaderCell();
188        sink.text(text);
189        sink.tableHeaderCell_();
190    }
191
192    /**
193     * Writes the rows of the table with the 5 columns - name, description, type, default value,
194     * since. Each row corresponds to a property of the module.
195     *
196     * @param sink sink to write to.
197     * @throws MacroExecutionException if an error occurs during writing.
198     */
199    private static void writeTablePropertiesRows(Sink sink)
200            throws MacroExecutionException {
201        final Object instance = SiteUtil.getModuleInstance(currentModuleName);
202        final Class<?> clss = instance.getClass();
203
204        final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance);
205        final Map<String, DetailNode> propertiesJavadocs = SiteUtil
206                .getPropertiesJavadocs(properties, currentModuleName, currentModuleFile);
207
208        final List<String> orderedProperties = orderProperties(properties);
209
210        for (String property : orderedProperties) {
211            try {
212                final DetailNode propertyJavadoc = propertiesJavadocs.get(property);
213                final DetailNode currentModuleJavadoc = propertiesJavadocs.get(currentModuleName);
214                writePropertyRow(sink, property, propertyJavadoc, instance, currentModuleJavadoc);
215            }
216            // -@cs[IllegalCatch] we need to get details in wrapping exception
217            catch (Exception exc) {
218                final String message = String.format(Locale.ROOT,
219                        "Exception while handling moduleName: %s propertyName: %s",
220                        currentModuleName, property);
221                throw new MacroExecutionException(message, exc);
222            }
223        }
224    }
225
226    /**
227     * Reorder properties to always have the 'tokens' property last (if present).
228     *
229     * @param properties module properties.
230     * @return Collection of ordered properties.
231     *
232     */
233    private static List<String> orderProperties(Set<String> properties) {
234
235        final List<String> orderProperties = new LinkedList<>(properties);
236
237        if (orderProperties.remove(TOKENS_PROPERTY)) {
238            orderProperties.add(TOKENS_PROPERTY);
239        }
240        if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) {
241            orderProperties.add(SiteUtil.JAVADOC_TOKENS);
242        }
243        return List.copyOf(orderProperties);
244
245    }
246
247    /**
248     * Writes a table row with 5 columns for the given property - name, description, type,
249     * default value, since.
250     *
251     * @param sink sink to write to.
252     * @param propertyName the name of the property.
253     * @param propertyJavadoc the Javadoc of the property.
254     * @param instance the instance of the module.
255     * @param moduleJavadoc the Javadoc of the module.
256     * @throws MacroExecutionException if an error occurs during writing.
257     */
258    private static void writePropertyRow(Sink sink, String propertyName,
259                                         DetailNode propertyJavadoc, Object instance,
260                                            DetailNode moduleJavadoc)
261            throws MacroExecutionException {
262        final Field field = SiteUtil.getField(instance.getClass(), propertyName);
263
264        sink.rawText(INDENT_LEVEL_12);
265        sink.tableRow();
266
267        writePropertyNameCell(sink, propertyName);
268        writePropertyDescriptionCell(sink, propertyName, propertyJavadoc);
269        writePropertyTypeCell(sink, propertyName, field, instance);
270        writePropertyDefaultValueCell(sink, propertyName, field, instance);
271        writePropertySinceVersionCell(
272                sink, propertyName, moduleJavadoc, propertyJavadoc);
273
274        sink.rawText(INDENT_LEVEL_12);
275        sink.tableRow_();
276    }
277
278    /**
279     * Writes a table cell with the given property name.
280     *
281     * @param sink sink to write to.
282     * @param propertyName the name of the property.
283     */
284    private static void writePropertyNameCell(Sink sink, String propertyName) {
285        sink.rawText(INDENT_LEVEL_14);
286        sink.tableCell();
287        sink.text(propertyName);
288        sink.tableCell_();
289    }
290
291    /**
292     * Writes a table cell with the property description.
293     *
294     * @param sink sink to write to.
295     * @param propertyName the name of the property.
296     * @param propertyJavadoc the Javadoc of the property containing the description.
297     * @throws MacroExecutionException if an error occurs during retrieval of the description.
298     */
299    private static void writePropertyDescriptionCell(Sink sink, String propertyName,
300                                                     DetailNode propertyJavadoc)
301            throws MacroExecutionException {
302        sink.rawText(INDENT_LEVEL_14);
303        sink.tableCell();
304        final String description = SiteUtil
305                .getPropertyDescription(propertyName, propertyJavadoc, currentModuleName);
306
307        sink.rawText(description);
308        sink.tableCell_();
309    }
310
311    /**
312     * Writes a table cell with the property type.
313     *
314     * @param sink sink to write to.
315     * @param propertyName the name of the property.
316     * @param field the field of the property.
317     * @param instance the instance of the module.
318     * @throws MacroExecutionException if link to the property_types.html file cannot be
319     *                                 constructed.
320     */
321    private static void writePropertyTypeCell(Sink sink, String propertyName,
322                                              Field field, Object instance)
323            throws MacroExecutionException {
324        sink.rawText(INDENT_LEVEL_14);
325        sink.tableCell();
326
327        if (SiteUtil.TOKENS.equals(propertyName)) {
328            final AbstractCheck check = (AbstractCheck) instance;
329            if (check.getRequiredTokens().length == 0
330                    && Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds())) {
331                sink.text("set of any supported");
332                writeLink(sink);
333            }
334            else {
335                final List<String> configurableTokens = SiteUtil
336                        .getDifference(check.getAcceptableTokens(),
337                                check.getRequiredTokens())
338                        .stream()
339                        .map(TokenUtil::getTokenName)
340                        .collect(Collectors.toUnmodifiableList());
341                sink.text("subset of tokens");
342
343                writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
344            }
345        }
346        else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
347            final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
348            final List<String> configurableTokens = SiteUtil
349                    .getDifference(check.getAcceptableJavadocTokens(),
350                            check.getRequiredJavadocTokens())
351                    .stream()
352                    .map(JavadocUtil::getTokenName)
353                    .collect(Collectors.toUnmodifiableList());
354            sink.text("subset of javadoc tokens");
355            writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
356        }
357        else {
358            final String type = SiteUtil.getType(field, propertyName, currentModuleName, instance);
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(INDENT_LEVEL_16);
406        final String link =
407                SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
408        sink.link(link);
409        sink.rawText(INDENT_LEVEL_20);
410        sink.text(SiteUtil.TOKENS);
411        sink.link_();
412        sink.rawText(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(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(INDENT_LEVEL_18);
442            sink.text(SiteUtil.DOT);
443            sink.rawText(INDENT_LEVEL_14);
444        }
445        else {
446            sink.rawText(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                        + "#" + tokenName;
462        sink.link(link);
463        sink.rawText(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(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                        .collect(Collectors.toUnmodifiableList());
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                    .collect(Collectors.toUnmodifiableList());
507            writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
508        }
509        else {
510            final String defaultValue = getDefaultValue(propertyName, field, instance);
511            final String checkName = CHECK_PATTERN
512                    .matcher(instance.getClass().getSimpleName()).replaceAll("");
513
514            final boolean isSpecialTokenProp = NON_BASE_TOKEN_PROPERTIES.stream()
515                    .anyMatch(tokenProp -> tokenProp.equals(checkName + " - " + propertyName));
516
517            if (isSpecialTokenProp && !CURLY_BRACKET.equals(defaultValue)) {
518                final List<String> defaultValuesList =
519                        Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue));
520                writeTokensList(sink, defaultValuesList, SiteUtil.PATH_TO_TOKEN_TYPES, false);
521            }
522            else {
523                sink.rawText(CODE_START);
524                sink.text(defaultValue);
525                sink.rawText(CODE_END);
526            }
527        }
528
529        sink.tableCell_();
530    }
531
532    /**
533     * Get the default value of the property.
534     *
535     * @param propertyName the name of the property.
536     * @param field the field of the property.
537     * @param instance the instance of the module.
538     * @return the default value of the property.
539     * @throws MacroExecutionException if an error occurs during retrieval of the default value.
540     */
541    private static String getDefaultValue(String propertyName, Field field, Object instance)
542            throws MacroExecutionException {
543        final String result;
544
545        if (field != null) {
546            result = SiteUtil.getDefaultValue(
547                    propertyName, field, instance, currentModuleName);
548        }
549        else {
550            final Class<?> fieldClass = SiteUtil.getPropertyClass(propertyName, instance);
551
552            if (fieldClass.isArray()) {
553                result = CURLY_BRACKET;
554            }
555            else {
556                result = "null";
557            }
558        }
559        return result;
560    }
561
562    /**
563     * Writes a table cell with the property since version.
564     *
565     * @param sink sink to write to.
566     * @param propertyName the name of the property.
567     * @param moduleJavadoc the Javadoc of the module.
568     * @param propertyJavadoc the Javadoc of the property containing the since version.
569     * @throws MacroExecutionException if an error occurs during retrieval of the since version.
570     */
571    private static void writePropertySinceVersionCell(Sink sink, String propertyName,
572                                                      DetailNode moduleJavadoc,
573                                                      DetailNode propertyJavadoc)
574            throws MacroExecutionException {
575        sink.rawText(INDENT_LEVEL_14);
576        sink.tableCell();
577        final String sinceVersion = SiteUtil.getSinceVersion(
578                currentModuleName, moduleJavadoc, propertyName, propertyJavadoc);
579        sink.text(sinceVersion);
580        sink.tableCell_();
581    }
582}