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.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 (tagName.equals(DOCUMENT_TAG.toString())) {
109            sink.rawText(parser.getText());
110            sink.body_();
111        }
112        else if (macroName != null
113                && tagName.equals(MACRO_TAG.toString())
114                && !macroName.isEmpty()
115                && !isSecondParsing()) {
116            processMacroEnd(sink);
117            setIgnorableWhitespace(false);
118        }
119        else if (!tagName.equals(PARAM.toString())) {
120            sink.rawText(parser.getText());
121        }
122    }
123
124    /**
125     * Handle the opening tag of a macro. Gather the macro name and parameters.
126     *
127     * @param parser the xml parser.
128     * @throws MacroExecutionException if the macro name is not specified.
129     */
130    private void processMacroStart(XmlPullParser parser) throws MacroExecutionException {
131        macroName = parser.getAttributeValue(null, Attribute.NAME.toString());
132
133        if (macroName == null || macroName.isEmpty()) {
134            final String message = String.format(Locale.ROOT,
135                    "The '%s' attribute for the '%s' tag is required.",
136                    Attribute.NAME, MACRO_TAG);
137            throw new MacroExecutionException(message);
138        }
139    }
140
141    /**
142     * Handle the opening tag of a parameter. Gather the parameter name and value.
143     *
144     * @param parser the xml parser.
145     * @param sink the sink object.
146     * @throws MacroExecutionException if the parameter name or value is not specified.
147     */
148    private void processParamStart(XmlPullParser parser, Sink sink) throws MacroExecutionException {
149        if (macroName != null && !macroName.isEmpty()) {
150            final String paramName = parser
151                    .getAttributeValue(null, Attribute.NAME.toString());
152            final String paramValue = parser
153                    .getAttributeValue(null, Attribute.VALUE.toString());
154
155            if (paramName == null
156                    || paramValue == null
157                    || paramName.isEmpty()
158                    || paramValue.isEmpty()) {
159                final String message = String.format(Locale.ROOT,
160                        "'%s' and '%s' attributes for the '%s' tag are required"
161                                + " inside the '%s' tag.",
162                        Attribute.NAME, Attribute.VALUE, PARAM, MACRO_TAG);
163                throw new MacroExecutionException(message);
164            }
165
166            macroParameters.put(paramName, paramValue);
167        }
168        else {
169            sink.rawText(parser.getText());
170        }
171    }
172
173    /**
174     * Execute a macro. Creates a {@link MacroRequest} with the gathered
175     * {@link #macroName} and {@link #macroParameters} and executes the macro.
176     * Afterward, the macro fields are reinitialized.
177     *
178     * @param sink the sink object.
179     * @throws MacroExecutionException if a macro is not found.
180     */
181    private void processMacroEnd(Sink sink) throws MacroExecutionException {
182        final MacroRequest request = new MacroRequest(sourceContent,
183                new XdocsTemplateParser(), macroParameters,
184                new File(TEMP_DIR));
185
186        try {
187            executeMacro(macroName, request, sink);
188        }
189        catch (MacroNotFoundException exception) {
190            final String message = String.format(Locale.ROOT, "Macro '%s' not found.", macroName);
191            throw new MacroExecutionException(message, exception);
192        }
193
194        reinitializeMacroFields();
195    }
196
197    /**
198     * Reinitialize the macro fields.
199     */
200    private void reinitializeMacroFields() {
201        macroName = "";
202        macroParameters.clear();
203    }
204}