View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.Reader;
25  import java.io.StringReader;
26  import java.io.StringWriter;
27  import java.util.HashMap;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import javax.swing.text.html.HTML.Attribute;
32  
33  import org.apache.maven.doxia.macro.MacroExecutionException;
34  import org.apache.maven.doxia.macro.MacroRequest;
35  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
36  import org.apache.maven.doxia.module.xdoc.XdocParser;
37  import org.apache.maven.doxia.parser.ParseException;
38  import org.apache.maven.doxia.parser.Parser;
39  import org.apache.maven.doxia.sink.Sink;
40  import org.codehaus.plexus.component.annotations.Component;
41  import org.codehaus.plexus.util.IOUtil;
42  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
43  
44  /**
45   * Parser for Checkstyle's xdoc templates.
46   * This parser is responsible for generating xdocs({@code .xml}) from the xdoc
47   * templates({@code .xml.template}). The templates are regular xdocs with custom
48   * macros for generating dynamic content - properties, examples, etc.
49   * This parser behaves just like the {@link XdocParser} with the difference that all
50   * elements apart from the {@code macro} element are copied as is to the output.
51   * This module will be removed once
52   * <a href="https://github.com/checkstyle/checkstyle/issues/13426">#13426</a> is resolved.
53   *
54   * @see ExampleMacro
55   */
56  @Component(role = Parser.class, hint = "xdocs-template")
57  public class XdocsTemplateParser extends XdocParser {
58  
59      /** User working directory. */
60      public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
61  
62      /** The macro parameters. */
63      private final Map<String, Object> macroParameters = new HashMap<>();
64  
65      /** The source content of the input reader. Used to pass into macros. */
66      private String sourceContent;
67  
68      /** A macro name. */
69      private String macroName;
70  
71      @Override
72      public void parse(Reader source, Sink sink, String reference) throws ParseException {
73          try (StringWriter contentWriter = new StringWriter()) {
74              IOUtil.copy(source, contentWriter);
75              sourceContent = contentWriter.toString();
76              super.parse(new StringReader(sourceContent), sink, reference);
77          }
78          catch (IOException ioException) {
79              throw new ParseException("Error reading the input source", ioException);
80          }
81          finally {
82              sourceContent = null;
83          }
84      }
85  
86      @Override
87      protected void handleStartTag(XmlPullParser parser, Sink sink) throws MacroExecutionException {
88          final String tagName = parser.getName();
89          if (tagName.equals(DOCUMENT_TAG.toString())) {
90              sink.body();
91              sink.rawText(parser.getText());
92          }
93          else if (tagName.equals(MACRO_TAG.toString()) && !isSecondParsing()) {
94              processMacroStart(parser);
95              setIgnorableWhitespace(true);
96          }
97          else if (tagName.equals(PARAM.toString()) && !isSecondParsing()) {
98              processParamStart(parser, sink);
99          }
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 }