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}