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;
021
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.nio.charset.StandardCharsets;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033import com.puppycrawl.tools.checkstyle.api.AuditEvent;
034import com.puppycrawl.tools.checkstyle.api.AuditListener;
035import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
036import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
037
038/**
039 * Simple XML logger.
040 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
041 * we want to localize error messages or simply that file names are
042 * localized and takes care about escaping as well.
043 */
044// -@cs[AbbreviationAsWordInName] We can not change it as,
045// check's name is part of API (used in configurations).
046public final class XMLLogger
047 extends AbstractAutomaticBean
048 implements AuditListener {
049
050 /** Decimal radix. */
051 private static final int BASE_10 = 10;
052
053 /** Hex radix. */
054 private static final int BASE_16 = 16;
055
056 /** Initial capacity for StringBuilder to "source" attribute. */
057 private static final int SOURCE_BUILDER_CAPACITY = 128;
058
059 /** Some known entities to detect. */
060 private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
061 "quot", };
062
063 /** Close output stream in auditFinished. */
064 private final boolean closeStream;
065
066 /** Holds all messages for the given file. */
067 private final Map<String, FileMessages> fileMessages =
068 new HashMap<>();
069
070 /**
071 * Helper writer that allows easy encoding and printing.
072 */
073 private final PrintWriter writer;
074
075 /**
076 * Creates a new {@code XMLLogger} instance.
077 * Sets the output to a defined stream.
078 *
079 * @param outputStream the stream to write logs to.
080 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
081 * @throws IllegalArgumentException if outputStreamOptions is null.
082 * @noinspection deprecation
083 * @noinspectionreason We are forced to keep AutomaticBean compatability
084 * because of maven-checkstyle-plugin. Until #12873.
085 */
086 public XMLLogger(OutputStream outputStream,
087 AutomaticBean.OutputStreamOptions outputStreamOptions) {
088 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
089 }
090
091 /**
092 * Creates a new {@code XMLLogger} instance.
093 * Sets the output to a defined stream.
094 *
095 * @param outputStream the stream to write logs to.
096 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
097 * @throws IllegalArgumentException if outputStreamOptions is null.
098 */
099 public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
100 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
101 if (outputStreamOptions == null) {
102 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
103 }
104 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
105 }
106
107 @Override
108 protected void finishLocalSetup() {
109 // No code by default
110 }
111
112 /**
113 * Returns the version string printed.
114 *
115 */
116 private void printVersionString() {
117 final String version = XMLLogger.class.getPackage().getImplementationVersion();
118 writer.println("<checkstyle version=\"" + version + "\">");
119 }
120
121 @Override
122 public void auditStarted(AuditEvent event) {
123 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
124
125 printVersionString();
126 }
127
128 @Override
129 public void auditFinished(AuditEvent event) {
130 writer.println("</checkstyle>");
131 if (closeStream) {
132 writer.close();
133 }
134 else {
135 writer.flush();
136 }
137 }
138
139 @Override
140 public void fileStarted(AuditEvent event) {
141 fileMessages.put(event.getFileName(), new FileMessages());
142 }
143
144 @Override
145 public void fileFinished(AuditEvent event) {
146 final String fileName = event.getFileName();
147 final FileMessages messages = fileMessages.remove(fileName);
148 writeFileMessages(fileName, messages);
149 }
150
151 /**
152 * Prints the file section with all file errors and exceptions.
153 *
154 * @param fileName The file name, as should be printed in the opening file tag.
155 * @param messages The file messages.
156 */
157 private void writeFileMessages(String fileName, FileMessages messages) {
158 writeFileOpeningTag(fileName);
159 if (messages != null) {
160 for (AuditEvent errorEvent : messages.getErrors()) {
161 writeFileError(errorEvent);
162 }
163 for (Throwable exception : messages.getExceptions()) {
164 writeException(exception);
165 }
166 }
167 writeFileClosingTag();
168 }
169
170 /**
171 * Prints the "file" opening tag with the given filename.
172 *
173 * @param fileName The filename to output.
174 */
175 private void writeFileOpeningTag(String fileName) {
176 writer.println("<file name=\"" + encode(fileName) + "\">");
177 }
178
179 /**
180 * Prints the "file" closing tag.
181 */
182 private void writeFileClosingTag() {
183 writer.println("</file>");
184 }
185
186 @Override
187 public void addError(AuditEvent event) {
188 if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
189 final String fileName = event.getFileName();
190 final FileMessages messages = fileMessages.get(fileName);
191 if (messages != null) {
192 messages.addError(event);
193 }
194 else {
195 writeFileError(event);
196 }
197 }
198 }
199
200 /**
201 * Outputs the given event to the writer.
202 *
203 * @param event An event to print.
204 */
205 private void writeFileError(AuditEvent event) {
206 writer.print("<error" + " line=\"" + event.getLine() + "\"");
207 if (event.getColumn() > 0) {
208 writer.print(" column=\"" + event.getColumn() + "\"");
209 }
210 writer.print(" severity=\""
211 + event.getSeverityLevel().getName()
212 + "\"");
213 writer.print(" message=\""
214 + encode(event.getMessage())
215 + "\"");
216 writer.print(" source=\"");
217 final StringBuilder sourceValueBuilder = new StringBuilder(SOURCE_BUILDER_CAPACITY);
218 sourceValueBuilder.append(event.getSourceName());
219 final String moduleId = event.getModuleId();
220 if (moduleId != null && !moduleId.isBlank()) {
221 sourceValueBuilder
222 .append('#')
223 .append(moduleId);
224 }
225 writer.print(encode(sourceValueBuilder.toString()));
226 writer.println("\"/>");
227 }
228
229 @Override
230 public void addException(AuditEvent event, Throwable throwable) {
231 final String fileName = event.getFileName();
232 final FileMessages messages = fileMessages.get(fileName);
233 if (messages != null) {
234 messages.addException(throwable);
235 }
236 else {
237 writeException(throwable);
238 }
239 }
240
241 /**
242 * Writes the exception event to the print writer.
243 *
244 * @param throwable The
245 */
246 private void writeException(Throwable throwable) {
247 writer.println("<exception>");
248 writer.println("<![CDATA[");
249
250 final StringWriter stringWriter = new StringWriter();
251 final PrintWriter printer = new PrintWriter(stringWriter);
252 throwable.printStackTrace(printer);
253 writer.println(encode(stringWriter.toString()));
254
255 writer.println("]]>");
256 writer.println("</exception>");
257 }
258
259 /**
260 * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
261 *
262 * @param value the value to escape.
263 * @return the escaped value if necessary.
264 */
265 public static String encode(String value) {
266 final StringBuilder sb = new StringBuilder(256);
267 for (int i = 0; i < value.length(); i++) {
268 final char chr = value.charAt(i);
269 switch (chr) {
270 case '<':
271 sb.append("&lt;");
272 break;
273 case '>':
274 sb.append("&gt;");
275 break;
276 case '\'':
277 sb.append("&apos;");
278 break;
279 case '\"':
280 sb.append("&quot;");
281 break;
282 case '&':
283 sb.append("&amp;");
284 break;
285 case '\r':
286 break;
287 case '\n':
288 sb.append("&#10;");
289 break;
290 default:
291 if (Character.isISOControl(chr)) {
292 // true escape characters need '&' before, but it also requires XML 1.1
293 // until https://github.com/checkstyle/checkstyle/issues/5168
294 sb.append("#x");
295 sb.append(Integer.toHexString(chr));
296 sb.append(';');
297 }
298 else {
299 sb.append(chr);
300 }
301 break;
302 }
303 }
304 return sb.toString();
305 }
306
307 /**
308 * Finds whether the given argument is character or entity reference.
309 *
310 * @param ent the possible entity to look for.
311 * @return whether the given argument a character or entity reference
312 */
313 public static boolean isReference(String ent) {
314 boolean reference = false;
315
316 if (ent.charAt(0) == '&' && ent.endsWith(";")) {
317 if (ent.charAt(1) == '#') {
318 // prefix is "&#"
319 int prefixLength = 2;
320
321 int radix = BASE_10;
322 if (ent.charAt(2) == 'x') {
323 prefixLength++;
324 radix = BASE_16;
325 }
326 try {
327 Integer.parseInt(
328 ent.substring(prefixLength, ent.length() - 1), radix);
329 reference = true;
330 }
331 catch (final NumberFormatException ignored) {
332 reference = false;
333 }
334 }
335 else {
336 final String name = ent.substring(1, ent.length() - 1);
337 for (String element : ENTITIES) {
338 if (name.equals(element)) {
339 reference = true;
340 break;
341 }
342 }
343 }
344 }
345
346 return reference;
347 }
348
349 /**
350 * The registered file messages.
351 */
352 private static final class FileMessages {
353
354 /** The file error events. */
355 private final List<AuditEvent> errors = new ArrayList<>();
356
357 /** The file exceptions. */
358 private final List<Throwable> exceptions = new ArrayList<>();
359
360 /**
361 * Returns the file error events.
362 *
363 * @return the file error events.
364 */
365 public List<AuditEvent> getErrors() {
366 return Collections.unmodifiableList(errors);
367 }
368
369 /**
370 * Adds the given error event to the messages.
371 *
372 * @param event the error event.
373 */
374 public void addError(AuditEvent event) {
375 errors.add(event);
376 }
377
378 /**
379 * Returns the file exceptions.
380 *
381 * @return the file exceptions.
382 */
383 public List<Throwable> getExceptions() {
384 return Collections.unmodifiableList(exceptions);
385 }
386
387 /**
388 * Adds the given exception to the messages.
389 *
390 * @param throwable the file exception
391 */
392 public void addException(Throwable throwable) {
393 exceptions.add(throwable);
394 }
395
396 }
397
398}