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