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}