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.IOException;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.List;
028import java.util.Locale;
029import java.util.stream.Collectors;
030
031import org.apache.maven.doxia.macro.AbstractMacro;
032import org.apache.maven.doxia.macro.Macro;
033import org.apache.maven.doxia.macro.MacroExecutionException;
034import org.apache.maven.doxia.macro.MacroRequest;
035import org.apache.maven.doxia.sink.Sink;
036import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
037import org.codehaus.plexus.component.annotations.Component;
038
039/**
040 * A macro that inserts a snippet of code or configuration from a file.
041 */
042@Component(role = Macro.class, hint = "example")
043public class ExampleMacro extends AbstractMacro {
044
045    /** Starting delimiter for config snippets. */
046    private static final String XML_CONFIG_START = "/*xml";
047
048    /** Ending delimiter for config snippets. */
049    private static final String XML_CONFIG_END = "*/";
050
051    /** Starting delimiter for code snippets. */
052    private static final String CODE_SNIPPET_START = "// xdoc section -- start";
053
054    /** Ending delimiter for code snippets. */
055    private static final String CODE_SNIPPET_END = "// xdoc section -- end";
056
057    /** Newline character. */
058    private static final String NEWLINE = System.lineSeparator();
059
060    /** Eight whitespace characters. All example source tags are indented 8 spaces. */
061    private static final String INDENTATION = "        ";
062
063    /** The path of the last file. */
064    private String lastPath = "";
065
066    /** The line contents of the last file. */
067    private List<String> lastLines = new ArrayList<>();
068
069    @Override
070    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
071        final String path = (String) request.getParameter("path");
072        final String type = (String) request.getParameter("type");
073
074        List<String> lines = lastLines;
075        if (!path.equals(lastPath)) {
076            lines = readFile("src/xdocs-examples/" + path);
077            lastPath = path;
078            lastLines = lines;
079        }
080
081        if ("config".equals(type)) {
082            final String config = getConfigSnippet(lines);
083
084            if (config.isBlank()) {
085                final String message = String.format(Locale.ROOT,
086                        "Empty config snippet from %s, check"
087                                + " for xml config snippet delimiters in input file.", path
088                );
089                throw new MacroExecutionException(message);
090            }
091
092            writeSnippet(sink, config);
093        }
094        else if ("code".equals(type)) {
095            String code = getCodeSnippet(lines);
096            // Replace tabs with spaces for FileTabCharacterCheck examples
097            if (path.contains("filetabcharacter")) {
098                code = code.replace("\t", "  ");
099            }
100
101            if (code.isBlank()) {
102                final String message = String.format(Locale.ROOT,
103                        "Empty code snippet from %s, check"
104                                + " for code snippet delimiters in input file.", path
105                );
106                throw new MacroExecutionException(message);
107            }
108
109            writeSnippet(sink, code);
110        }
111        else {
112            final String message = String.format(Locale.ROOT, "Unknown example type: %s", type);
113            throw new MacroExecutionException(message);
114        }
115    }
116
117    /**
118     * Read the file at the given path and returns its contents as a list of lines.
119     *
120     * @param path the path to the file to read.
121     * @return the contents of the file as a list of lines.
122     * @throws MacroExecutionException if the file could not be read.
123     */
124    private static List<String> readFile(String path) throws MacroExecutionException {
125        try {
126            final Path exampleFilePath = Path.of(path);
127            return Files.readAllLines(exampleFilePath);
128        }
129        catch (IOException ioException) {
130            final String message = String.format(Locale.ROOT, "Failed to read %s", path);
131            throw new MacroExecutionException(message, ioException);
132        }
133    }
134
135    /**
136     * Extract a configuration snippet from the given lines. Config delimiters use the whole
137     * line for themselves and have no indentation. We use equals() instead of contains()
138     * to be more strict because some examples contain those delimiters.
139     *
140     * @param lines the lines to extract the snippet from.
141     * @return the configuration snippet.
142     */
143    private static String getConfigSnippet(Collection<String> lines) {
144        return lines.stream()
145                .dropWhile(line -> !XML_CONFIG_START.equals(line))
146                .skip(1)
147                .takeWhile(line -> !XML_CONFIG_END.equals(line))
148                .collect(Collectors.joining(NEWLINE));
149    }
150
151    /**
152     * Extract a code snippet from the given lines. Code delimiters can be indented, so
153     * we use contains() instead of equals().
154     *
155     * @param lines the lines to extract the snippet from.
156     * @return the code snippet.
157     */
158    private static String getCodeSnippet(Collection<String> lines) {
159        return lines.stream()
160                .dropWhile(line -> !line.contains(CODE_SNIPPET_START))
161                .skip(1)
162                .takeWhile(line -> !line.contains(CODE_SNIPPET_END))
163                .collect(Collectors.joining(NEWLINE));
164    }
165
166    /**
167     * Write the given snippet to the file inside a source block.
168     *
169     * @param sink the sink to write to.
170     * @param snippet the snippet to write.
171     */
172    private static void writeSnippet(Sink sink, String snippet) {
173        sink.verbatim(SinkEventAttributeSet.BOXED);
174        final String text = NEWLINE
175                + String.join(NEWLINE, snippet.stripTrailing(), INDENTATION);
176        sink.text(text);
177        sink.verbatim_();
178    }
179}