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.nio.file.Files; 024import java.nio.file.Path; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.List; 028import java.util.Locale; 029import java.util.regex.Pattern; 030import java.util.stream.Collectors; 031 032import org.apache.maven.doxia.macro.AbstractMacro; 033import org.apache.maven.doxia.macro.Macro; 034import org.apache.maven.doxia.macro.MacroExecutionException; 035import org.apache.maven.doxia.macro.MacroRequest; 036import org.apache.maven.doxia.sink.Sink; 037import org.codehaus.plexus.component.annotations.Component; 038 039/** 040 * A macro that inserts a snippet of code or configuration from a file. 041 */ 042@Component(role = Macro.class, hint = "example") 043public class ExampleMacro extends AbstractMacro { 044 045 /** Starting delimiter for config snippets. */ 046 private static final String XML_CONFIG_START = "/*xml"; 047 048 /** Ending delimiter for config snippets. */ 049 private static final String XML_CONFIG_END = "*/"; 050 051 /** Starting delimiter for code snippets. */ 052 private static final String CODE_SNIPPET_START = "// xdoc section -- start"; 053 054 /** Ending delimiter for code snippets. */ 055 private static final String CODE_SNIPPET_END = "// xdoc section -- end"; 056 057 /** The pattern of xml code blocks. */ 058 private static final Pattern XML_PATTERN = Pattern.compile( 059 "^\\s*(<!DOCTYPE\\s+.*?>|<\\?xml\\s+.*?>|<module\\s+.*?>)\\s*", 060 Pattern.DOTALL 061 ); 062 063 /** The path of the last file. */ 064 private String lastPath = ""; 065 066 /** The line contents of the last file. */ 067 private List<String> lastLines = new ArrayList<>(); 068 069 /** 070 * Creates a new {@code ExampleMacro} instance. 071 */ 072 public ExampleMacro() { 073 // no code by default 074 } 075 076 @Override 077 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 078 final String path = (String) request.getParameter("path"); 079 final String type = (String) request.getParameter("type"); 080 081 List<String> lines = lastLines; 082 if (!path.equals(lastPath)) { 083 lines = readFile("src/xdocs-examples/" + path); 084 lastPath = path; 085 lastLines = lines; 086 } 087 088 if ("config".equals(type)) { 089 final String config = getConfigSnippet(lines); 090 091 if (config.isBlank()) { 092 final String message = String.format(Locale.ROOT, 093 "Empty config snippet from %s, check" 094 + " for xml config snippet delimiters in input file.", path 095 ); 096 throw new MacroExecutionException(message); 097 } 098 099 writeSnippet(sink, config); 100 } 101 else if ("code".equals(type)) { 102 String code = getCodeSnippet(lines); 103 // Replace tabs with spaces for FileTabCharacterCheck examples 104 if (path.contains("filetabcharacter")) { 105 code = code.replace("\t", " "); 106 } 107 108 if (code.isBlank()) { 109 final String message = String.format(Locale.ROOT, 110 "Empty code snippet from %s, check" 111 + " for code snippet delimiters in input file.", path 112 ); 113 throw new MacroExecutionException(message); 114 } 115 116 writeSnippet(sink, code); 117 } 118 else if ("raw".equals(type)) { 119 final String content = String.join(ModuleJavadocParsingUtil.NEWLINE, lines); 120 writeSnippet(sink, content); 121 } 122 else { 123 final String message = String.format(Locale.ROOT, "Unknown example type: %s", type); 124 throw new MacroExecutionException(message); 125 } 126 } 127 128 /** 129 * Read the file at the given path and returns its contents as a list of lines. 130 * 131 * @param path the path to the file to read. 132 * @return the contents of the file as a list of lines. 133 * @throws MacroExecutionException if the file could not be read. 134 */ 135 private static List<String> readFile(String path) throws MacroExecutionException { 136 try { 137 final Path exampleFilePath = Path.of(path); 138 return Files.readAllLines(exampleFilePath); 139 } 140 catch (IOException ioException) { 141 final String message = String.format(Locale.ROOT, "Failed to read %s", path); 142 throw new MacroExecutionException(message, ioException); 143 } 144 } 145 146 /** 147 * Extract a configuration snippet from the given lines. Config delimiters use the whole 148 * line for themselves and have no indentation. We use equals() instead of contains() 149 * to be more strict because some examples contain those delimiters. If the delimiters 150 * are not found, returns the entire file content. 151 * 152 * @param lines the lines to extract the snippet from. 153 * @return the configuration snippet. 154 */ 155 private static String getConfigSnippet(Collection<String> lines) { 156 final String snippet = lines.stream() 157 .dropWhile(line -> !XML_CONFIG_START.equals(line)) 158 .skip(1) 159 .takeWhile(line -> !XML_CONFIG_END.equals(line)) 160 .collect(Collectors.joining(ModuleJavadocParsingUtil.NEWLINE)); 161 162 // If no snippet was found (markers not present), return the entire file content 163 final String result; 164 if (snippet.isBlank()) { 165 result = String.join(ModuleJavadocParsingUtil.NEWLINE, lines); 166 } 167 else { 168 result = snippet; 169 } 170 171 return result; 172 } 173 174 /** 175 * Extract a code snippet from the given lines. Code delimiters can be indented, so 176 * we use contains() instead of equals(). If the delimiters are not found, returns 177 * the file content excluding the XML config block (if present). 178 * 179 * @param lines the lines to extract the snippet from. 180 * @return the code snippet. 181 */ 182 private static String getCodeSnippet(Collection<String> lines) { 183 final String snippet = lines.stream() 184 .dropWhile(line -> !line.contains(CODE_SNIPPET_START)) 185 .skip(1) 186 .takeWhile(line -> !line.contains(CODE_SNIPPET_END)) 187 .collect(Collectors.joining(ModuleJavadocParsingUtil.NEWLINE)); 188 189 // If no snippet was found (markers not present), return the file content 190 // excluding the XML config block (if present) 191 final String result; 192 if (snippet.isBlank()) { 193 final List<String> linesList = new ArrayList<>(lines); 194 final int configEndIndex = linesList.indexOf(XML_CONFIG_END); 195 if (configEndIndex >= 0) { 196 // XML config block is present, return content after it 197 result = String.join(ModuleJavadocParsingUtil.NEWLINE, 198 linesList.stream() 199 .skip(configEndIndex + 1) 200 .toList()); 201 } 202 else { 203 // No XML config block, return entire file 204 result = String.join(ModuleJavadocParsingUtil.NEWLINE, linesList); 205 } 206 } 207 else { 208 result = snippet; 209 } 210 211 return result; 212 } 213 214 /** 215 * Writes the given snippet inside a formatted source block. 216 * 217 * @param sink the sink to write to. 218 * @param snippet the snippet to write. 219 */ 220 private static void writeSnippet(Sink sink, String snippet) { 221 sink.rawText("<div class=\"wrapper\">"); 222 final boolean isXml = isXml(snippet); 223 224 final String languageClass; 225 if (isXml) { 226 languageClass = "language-xml"; 227 } 228 else { 229 languageClass = "language-java"; 230 } 231 sink.rawText("<pre class=\"prettyprint\"><code class=\"" + languageClass + "\">" 232 + ModuleJavadocParsingUtil.NEWLINE); 233 sink.rawText(escapeHtml(snippet).trim() + ModuleJavadocParsingUtil.NEWLINE); 234 sink.rawText("</code></pre>"); 235 sink.rawText("</div>"); 236 } 237 238 /** 239 * Escapes HTML special characters in the snippet. 240 * 241 * @param snippet the snippet to escape. 242 * @return the escaped snippet. 243 */ 244 private static String escapeHtml(String snippet) { 245 return snippet.replace("&", "&") 246 .replace("<", "<") 247 .replace(">", ">"); 248 } 249 250 /** 251 * Determines if the given snippet is likely an XML fragment. 252 * 253 * @param snippet the code snippet to analyze. 254 * @return {@code true} if the snippet appears to be XML, otherwise {@code false}. 255 */ 256 private static boolean isXml(String snippet) { 257 return XML_PATTERN.matcher(snippet.trim()).matches(); 258 } 259 260}