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.io.File;
023import java.io.IOException;
024import java.io.Reader;
025import java.io.StringReader;
026import java.io.StringWriter;
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    @Override
072    public void parse(Reader source, Sink sink, String reference) throws ParseException {
073        try (StringWriter contentWriter = new StringWriter()) {
074            IOUtil.copy(source, contentWriter);
075            sourceContent = contentWriter.toString();
076            super.parse(new StringReader(sourceContent), sink, reference);
077        }
078        catch (IOException ioException) {
079            throw new ParseException("Error reading the input source", ioException);
080        }
081        finally {
082            sourceContent = null;
083        }
084    }
085
086    @Override
087    protected void handleStartTag(XmlPullParser parser, Sink sink) throws MacroExecutionException {
088        final String tagName = parser.getName();
089        if (tagName.equals(DOCUMENT_TAG.toString())) {
090            sink.body();
091            sink.rawText(parser.getText());
092        }
093        else if (tagName.equals(MACRO_TAG.toString()) && !isSecondParsing()) {
094            processMacroStart(parser);
095            setIgnorableWhitespace(true);
096        }
097        else if (tagName.equals(PARAM.toString()) && !isSecondParsing()) {
098            processParamStart(parser, sink);
099        }
100        else {
101            sink.rawText(parser.getText());
102        }
103    }
104
105    @Override
106    protected void handleEndTag(XmlPullParser parser, Sink sink) throws MacroExecutionException {
107        final String tagName = parser.getName();
108        if (!"hr".equalsIgnoreCase(tagName)) {
109            if (tagName.equals(DOCUMENT_TAG.toString())) {
110                sink.rawText(parser.getText());
111                sink.body_();
112            }
113            else if (macroName != null
114                    && tagName.equals(MACRO_TAG.toString())
115                    && !macroName.isEmpty()
116                    && !isSecondParsing()) {
117                processMacroEnd(sink);
118                setIgnorableWhitespace(false);
119            }
120            else if (!tagName.equals(PARAM.toString())) {
121                sink.rawText(parser.getText());
122            }
123        }
124    }
125
126    /**
127     * Handle the opening tag of a macro. Gather the macro name and parameters.
128     *
129     * @param parser the xml parser.
130     * @throws MacroExecutionException if the macro name is not specified.
131     */
132    private void processMacroStart(XmlPullParser parser) throws MacroExecutionException {
133        macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
134
135        if (macroName == null || macroName.isEmpty()) {
136            final String message = String.format(Locale.ROOT,
137                    "The '%s' attribute for the '%s' tag is required.",
138                    Attribute.NAME, MACRO_TAG);
139            throw new MacroExecutionException(message);
140        }
141    }
142
143    /**
144     * Handle the opening tag of a parameter. Gather the parameter name and value.
145     *
146     * @param parser the xml parser.
147     * @param sink the sink object.
148     * @throws MacroExecutionException if the parameter name or value is not specified.
149     */
150    private void processParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
151        if (macroName != null && !macroName.isEmpty()) {
152            final String paramName = parser
153                    .getAttributeValue(null, Attribute.NAME.toString());
154            final String paramValue = parser
155                    .getAttributeValue(null, Attribute.VALUE.toString());
156
157            if (paramName == null
158                    || paramValue == null
159                    || paramName.isEmpty()
160                    || paramValue.isEmpty()) {
161                final String message = String.format(Locale.ROOT,
162                        "'%s' and '%s' attributes for the '%s' tag are required"
163                                + " inside the '%s' tag.",
164                        Attribute.NAME, Attribute.VALUE, PARAM, MACRO_TAG);
165                throw new MacroExecutionException(message);
166            }
167
168            macroParameters.put(paramName, paramValue);
169        }
170        else {
171            sink.rawText(parser.getText());
172        }
173    }
174
175    /**
176     * Execute a macro. Creates a {@link MacroRequest} with the gathered
177     * {@link #macroName} and {@link #macroParameters} and executes the macro.
178     * Afterward, the macro fields are reinitialized.
179     *
180     * @param sink the sink object.
181     * @throws MacroExecutionException if a macro is not found.
182     */
183    private void processMacroEnd(Sink sink) throws MacroExecutionException {
184        final MacroRequest request = new MacroRequest(sourceContent,
185                new XdocsTemplateParser(), macroParameters,
186                new File(TEMP_DIR));
187
188        try {
189            executeMacro(macroName, request, sink);
190        }
191        catch (MacroNotFoundException exception) {
192            final String message = String.format(Locale.ROOT, "Macro '%s' not found.", macroName);
193            throw new MacroExecutionException(message, exception);
194        }
195
196        reinitializeMacroFields();
197    }
198
199    /**
200     * Reinitialize the macro fields.
201     */
202    private void reinitializeMacroFields() {
203        macroName = "";
204        macroParameters.clear();
205    }
206}