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}