001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.io.Reader;
024import java.io.StringReader;
025import java.io.StringWriter;
026import java.nio.file.Path;
027import java.util.HashMap;
028import java.util.Locale;
029import java.util.Map;
030
031import javax.swing.text.html.HTML.Attribute;
032
033import org.apache.maven.doxia.macro.MacroExecutionException;
034import org.apache.maven.doxia.macro.MacroRequest;
035import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
036import org.apache.maven.doxia.module.xdoc.XdocParser;
037import org.apache.maven.doxia.parser.ParseException;
038import org.apache.maven.doxia.parser.Parser;
039import org.apache.maven.doxia.sink.Sink;
040import org.codehaus.plexus.component.annotations.Component;
041import org.codehaus.plexus.util.IOUtil;
042import org.codehaus.plexus.util.xml.pull.XmlPullParser;
043
044/**
045 * Parser for Checkstyle's xdoc templates.
046 * This parser is responsible for generating xdocs({@code .xml}) from the xdoc
047 * templates({@code .xml.template}). The templates are regular xdocs with custom
048 * macros for generating dynamic content - properties, examples, etc.
049 * This parser behaves just like the {@link XdocParser} with the difference that all
050 * elements apart from the {@code macro} element are copied as is to the output.
051 * This module will be removed once
052 * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
053 *
054 * @see ExampleMacro
055 */
056@Component(role = Parser.class, hint = "xdocs-template")
057public class XdocsTemplateParser extends XdocParser {
058
059    /** User working directory. */
060    public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
061
062    /** The macro parameters. */
063    private final Map<String, Object> macroParameters = new HashMap<>();
064
065    /** The source content of the input reader. Used to pass into macros. */
066    private String sourceContent;
067
068    /** A macro name. */
069    private String macroName;
070
071    /**
072     * Creates a new {@code XdocsTemplateParser} instance.
073     */
074    public XdocsTemplateParser() {
075        // no code by default
076    }
077
078    @Override
079    public void parse(Reader source, Sink sink, String reference) throws ParseException {
080        try (StringWriter contentWriter = new StringWriter()) {
081            IOUtil.copy(source, contentWriter);
082            sourceContent = contentWriter.toString();
083            super.parse(new StringReader(sourceContent), sink, reference);
084        }
085        catch (IOException ioException) {
086            throw new ParseException("Error reading the input source", ioException);
087        }
088        finally {
089            sourceContent = null;
090        }
091    }
092
093    @Override
094    protected void handleStartTag(XmlPullParser parser, Sink sink) throws MacroExecutionException {
095        final String tagName = parser.getName();
096        if (tagName.equals(DOCUMENT_TAG.toString())) {
097            sink.body();
098            sink.rawText(parser.getText());
099        }
100        else if (tagName.equals(MACRO_TAG.toString()) && !isSecondParsing()) {
101            processMacroStart(parser);
102            setIgnorableWhitespace(true);
103        }
104        else if (tagName.equals(PARAM.toString()) && !isSecondParsing()) {
105            processParamStart(parser, sink);
106        }
107        else {
108            sink.rawText(parser.getText());
109        }
110    }
111
112    @Override
113    protected void handleEndTag(XmlPullParser parser, Sink sink) throws MacroExecutionException {
114        final String tagName = parser.getName();
115        if (!"hr".equalsIgnoreCase(tagName)) {
116            if (tagName.equals(DOCUMENT_TAG.toString())) {
117                sink.rawText(parser.getText());
118                sink.body_();
119            }
120            else if (macroName != null
121                    && tagName.equals(MACRO_TAG.toString())
122                    && !macroName.isEmpty()
123                    && !isSecondParsing()) {
124                processMacroEnd(sink);
125                setIgnorableWhitespace(false);
126            }
127            else if (!tagName.equals(PARAM.toString())) {
128                sink.rawText(parser.getText());
129            }
130        }
131    }
132
133    /**
134     * Handle the opening tag of a macro. Gather the macro name and parameters.
135     *
136     * @param parser the xml parser.
137     * @throws MacroExecutionException if the macro name is not specified.
138     */
139    private void processMacroStart(XmlPullParser parser) throws MacroExecutionException {
140        macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
141
142        if (macroName == null || macroName.isEmpty()) {
143            final String message = String.format(Locale.ROOT,
144                    "The '%s' attribute for the '%s' tag is required.",
145                    Attribute.NAME, MACRO_TAG);
146            throw new MacroExecutionException(message);
147        }
148    }
149
150    /**
151     * Handle the opening tag of a parameter. Gather the parameter name and value.
152     *
153     * @param parser the xml parser.
154     * @param sink the sink object.
155     * @throws MacroExecutionException if the parameter name or value is not specified.
156     */
157    private void processParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
158        if (macroName != null && !macroName.isEmpty()) {
159            final String paramName = parser
160                    .getAttributeValue(null, Attribute.NAME.toString());
161            final String paramValue = parser
162                    .getAttributeValue(null, Attribute.VALUE.toString());
163
164            if (paramName == null
165                    || paramValue == null
166                    || paramName.isEmpty()
167                    || paramValue.isEmpty()) {
168                final String message = String.format(Locale.ROOT,
169                        "'%s' and '%s' attributes for the '%s' tag are required"
170                                + " inside the '%s' tag.",
171                        Attribute.NAME, Attribute.VALUE, PARAM, MACRO_TAG);
172                throw new MacroExecutionException(message);
173            }
174
175            macroParameters.put(paramName, paramValue);
176        }
177        else {
178            sink.rawText(parser.getText());
179        }
180    }
181
182    /**
183     * Execute a macro. Creates a {@link MacroRequest} with the gathered
184     * {@link #macroName} and {@link #macroParameters} and executes the macro.
185     * Afterward, the macro fields are reinitialized.
186     *
187     * @param sink the sink object.
188     * @throws MacroExecutionException if a macro is not found.
189     */
190    private void processMacroEnd(Sink sink) throws MacroExecutionException {
191        final MacroRequest request = new MacroRequest(sourceContent,
192                new XdocsTemplateParser(), macroParameters,
193                Path.of(TEMP_DIR).toFile());
194
195        try {
196            executeMacro(macroName, request, sink);
197        }
198        catch (MacroNotFoundException exception) {
199            final String message = String.format(Locale.ROOT, "Macro '%s' not found.", macroName);
200            throw new MacroExecutionException(message, exception);
201        }
202
203        reinitializeMacroFields();
204    }
205
206    /**
207     * Reinitialize the macro fields.
208     */
209    private void reinitializeMacroFields() {
210        macroName = "";
211        macroParameters.clear();
212    }
213
214}