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.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 /** Newline character. */ 058 private static final String NEWLINE = System.lineSeparator(); 059 060 /** Eight whitespace characters. All example source tags are indented 8 spaces. */ 061 private static final String INDENTATION = " "; 062 063 /** The pattern of xml code blocks. */ 064 private static final Pattern XML_PATTERN = Pattern.compile( 065 "^\\s*(<!DOCTYPE\\s+.*?>|<\\?xml\\s+.*?>|<module\\s+.*?>)\\s*", 066 Pattern.DOTALL 067 ); 068 069 /** The path of the last file. */ 070 private String lastPath = ""; 071 072 /** The line contents of the last file. */ 073 private List<String> lastLines = new ArrayList<>(); 074 075 @Override 076 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 077 final String path = (String) request.getParameter("path"); 078 final String type = (String) request.getParameter("type"); 079 080 List<String> lines = lastLines; 081 if (!path.equals(lastPath)) { 082 lines = readFile("src/xdocs-examples/" + path); 083 lastPath = path; 084 lastLines = lines; 085 } 086 087 if ("config".equals(type)) { 088 final String config = getConfigSnippet(lines); 089 090 if (config.isBlank()) { 091 final String message = String.format(Locale.ROOT, 092 "Empty config snippet from %s, check" 093 + " for xml config snippet delimiters in input file.", path 094 ); 095 throw new MacroExecutionException(message); 096 } 097 098 writeSnippet(sink, config); 099 } 100 else if ("code".equals(type)) { 101 String code = getCodeSnippet(lines); 102 // Replace tabs with spaces for FileTabCharacterCheck examples 103 if (path.contains("filetabcharacter")) { 104 code = code.replace("\t", " "); 105 } 106 107 if (code.isBlank()) { 108 final String message = String.format(Locale.ROOT, 109 "Empty code snippet from %s, check" 110 + " for code snippet delimiters in input file.", path 111 ); 112 throw new MacroExecutionException(message); 113 } 114 115 writeSnippet(sink, code); 116 } 117 else if ("raw".equals(type)) { 118 final String content = String.join(NEWLINE, lines); 119 writeSnippet(sink, content); 120 } 121 else { 122 final String message = String.format(Locale.ROOT, "Unknown example type: %s", type); 123 throw new MacroExecutionException(message); 124 } 125 } 126 127 /** 128 * Read the file at the given path and returns its contents as a list of lines. 129 * 130 * @param path the path to the file to read. 131 * @return the contents of the file as a list of lines. 132 * @throws MacroExecutionException if the file could not be read. 133 */ 134 private static List<String> readFile(String path) throws MacroExecutionException { 135 try { 136 final Path exampleFilePath = Path.of(path); 137 return Files.readAllLines(exampleFilePath); 138 } 139 catch (IOException ioException) { 140 final String message = String.format(Locale.ROOT, "Failed to read %s", path); 141 throw new MacroExecutionException(message, ioException); 142 } 143 } 144 145 /** 146 * Extract a configuration snippet from the given lines. Config delimiters use the whole 147 * line for themselves and have no indentation. We use equals() instead of contains() 148 * to be more strict because some examples contain those delimiters. 149 * 150 * @param lines the lines to extract the snippet from. 151 * @return the configuration snippet. 152 */ 153 private static String getConfigSnippet(Collection<String> lines) { 154 return lines.stream() 155 .dropWhile(line -> !XML_CONFIG_START.equals(line)) 156 .skip(1) 157 .takeWhile(line -> !XML_CONFIG_END.equals(line)) 158 .collect(Collectors.joining(NEWLINE)); 159 } 160 161 /** 162 * Extract a code snippet from the given lines. Code delimiters can be indented, so 163 * we use contains() instead of equals(). 164 * 165 * @param lines the lines to extract the snippet from. 166 * @return the code snippet. 167 */ 168 private static String getCodeSnippet(Collection<String> lines) { 169 return lines.stream() 170 .dropWhile(line -> !line.contains(CODE_SNIPPET_START)) 171 .skip(1) 172 .takeWhile(line -> !line.contains(CODE_SNIPPET_END)) 173 .collect(Collectors.joining(NEWLINE)); 174 } 175 176 /** 177 * Writes the given snippet inside a formatted source block. 178 * 179 * @param sink the sink to write to. 180 * @param snippet the snippet to write. 181 */ 182 private static void writeSnippet(Sink sink, String snippet) { 183 sink.rawText("<div class=\"wrapper\">"); 184 final boolean isXml = isXml(snippet); 185 186 final String languageClass; 187 if (isXml) { 188 languageClass = "language-xml"; 189 } 190 else { 191 languageClass = "language-java"; 192 } 193 sink.rawText("<pre class=\"prettyprint\"><code class=\"" + languageClass + "\">" + NEWLINE); 194 sink.rawText(escapeHtml(snippet).trim() + NEWLINE); 195 sink.rawText("</code></pre>"); 196 sink.rawText("</div>"); 197 } 198 199 /** 200 * Escapes HTML special characters in the snippet. 201 * 202 * @param snippet the snippet to escape. 203 * @return the escaped snippet. 204 */ 205 private static String escapeHtml(String snippet) { 206 return snippet.replace("&", "&") 207 .replace("<", "<") 208 .replace(">", ">"); 209 } 210 211 /** 212 * Determines if the given snippet is likely an XML fragment. 213 * 214 * @param snippet the code snippet to analyze. 215 * @return {@code true} if the snippet appears to be XML, otherwise {@code false}. 216 */ 217 private static boolean isXml(String snippet) { 218 return XML_PATTERN.matcher(snippet.trim()).matches(); 219 } 220}