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