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.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.stream.Collectors; 030 031import org.apache.maven.doxia.macro.AbstractMacro; 032import org.apache.maven.doxia.macro.Macro; 033import org.apache.maven.doxia.macro.MacroExecutionException; 034import org.apache.maven.doxia.macro.MacroRequest; 035import org.apache.maven.doxia.sink.Sink; 036import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 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 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 { 112 final String message = String.format(Locale.ROOT, "Unknown example type: %s", type); 113 throw new MacroExecutionException(message); 114 } 115 } 116 117 /** 118 * Read the file at the given path and returns its contents as a list of lines. 119 * 120 * @param path the path to the file to read. 121 * @return the contents of the file as a list of lines. 122 * @throws MacroExecutionException if the file could not be read. 123 */ 124 private static List<String> readFile(String path) throws MacroExecutionException { 125 try { 126 final Path exampleFilePath = Path.of(path); 127 return Files.readAllLines(exampleFilePath); 128 } 129 catch (IOException ioException) { 130 final String message = String.format(Locale.ROOT, "Failed to read %s", path); 131 throw new MacroExecutionException(message, ioException); 132 } 133 } 134 135 /** 136 * Extract a configuration snippet from the given lines. Config delimiters use the whole 137 * line for themselves and have no indentation. We use equals() instead of contains() 138 * to be more strict because some examples contain those delimiters. 139 * 140 * @param lines the lines to extract the snippet from. 141 * @return the configuration snippet. 142 */ 143 private static String getConfigSnippet(Collection<String> lines) { 144 return lines.stream() 145 .dropWhile(line -> !XML_CONFIG_START.equals(line)) 146 .skip(1) 147 .takeWhile(line -> !XML_CONFIG_END.equals(line)) 148 .collect(Collectors.joining(NEWLINE)); 149 } 150 151 /** 152 * Extract a code snippet from the given lines. Code delimiters can be indented, so 153 * we use contains() instead of equals(). 154 * 155 * @param lines the lines to extract the snippet from. 156 * @return the code snippet. 157 */ 158 private static String getCodeSnippet(Collection<String> lines) { 159 return lines.stream() 160 .dropWhile(line -> !line.contains(CODE_SNIPPET_START)) 161 .skip(1) 162 .takeWhile(line -> !line.contains(CODE_SNIPPET_END)) 163 .collect(Collectors.joining(NEWLINE)); 164 } 165 166 /** 167 * Write the given snippet to the file inside a source block. 168 * 169 * @param sink the sink to write to. 170 * @param snippet the snippet to write. 171 */ 172 private static void writeSnippet(Sink sink, String snippet) { 173 sink.verbatim(SinkEventAttributeSet.BOXED); 174 final String text = NEWLINE 175 + String.join(NEWLINE, snippet.stripTrailing(), INDENTATION); 176 sink.text(text); 177 sink.verbatim_(); 178 } 179}