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 @Override 110 public void auditStarted(AuditEvent event) { 111 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 112 113 final String version = XMLLogger.class.getPackage().getImplementationVersion(); 114 115 writer.println("<checkstyle version=\"" + version + "\">"); 116 } 117 118 @Override 119 public void auditFinished(AuditEvent event) { 120 writer.println("</checkstyle>"); 121 if (closeStream) { 122 writer.close(); 123 } 124 else { 125 writer.flush(); 126 } 127 } 128 129 @Override 130 public void fileStarted(AuditEvent event) { 131 fileMessages.put(event.getFileName(), new FileMessages()); 132 } 133 134 @Override 135 public void fileFinished(AuditEvent event) { 136 final String fileName = event.getFileName(); 137 final FileMessages messages = fileMessages.remove(fileName); 138 writeFileMessages(fileName, messages); 139 } 140 141 /** 142 * Prints the file section with all file errors and exceptions. 143 * 144 * @param fileName The file name, as should be printed in the opening file tag. 145 * @param messages The file messages. 146 */ 147 private void writeFileMessages(String fileName, FileMessages messages) { 148 writeFileOpeningTag(fileName); 149 if (messages != null) { 150 for (AuditEvent errorEvent : messages.getErrors()) { 151 writeFileError(errorEvent); 152 } 153 for (Throwable exception : messages.getExceptions()) { 154 writeException(exception); 155 } 156 } 157 writeFileClosingTag(); 158 } 159 160 /** 161 * Prints the "file" opening tag with the given filename. 162 * 163 * @param fileName The filename to output. 164 */ 165 private void writeFileOpeningTag(String fileName) { 166 writer.println("<file name=\"" + encode(fileName) + "\">"); 167 } 168 169 /** 170 * Prints the "file" closing tag. 171 */ 172 private void writeFileClosingTag() { 173 writer.println("</file>"); 174 } 175 176 @Override 177 public void addError(AuditEvent event) { 178 if (event.getSeverityLevel() != SeverityLevel.IGNORE) { 179 final String fileName = event.getFileName(); 180 if (fileName == null || !fileMessages.containsKey(fileName)) { 181 writeFileError(event); 182 } 183 else { 184 final FileMessages messages = fileMessages.get(fileName); 185 messages.addError(event); 186 } 187 } 188 } 189 190 /** 191 * Outputs the given event to the writer. 192 * 193 * @param event An event to print. 194 */ 195 private void writeFileError(AuditEvent event) { 196 writer.print("<error" + " line=\"" + event.getLine() + "\""); 197 if (event.getColumn() > 0) { 198 writer.print(" column=\"" + event.getColumn() + "\""); 199 } 200 writer.print(" severity=\"" 201 + event.getSeverityLevel().getName() 202 + "\""); 203 writer.print(" message=\"" 204 + encode(event.getMessage()) 205 + "\""); 206 writer.print(" source=\""); 207 if (event.getModuleId() == null) { 208 writer.print(encode(event.getSourceName())); 209 } 210 else { 211 writer.print(encode(event.getModuleId())); 212 } 213 writer.println("\"/>"); 214 } 215 216 @Override 217 public void addException(AuditEvent event, Throwable throwable) { 218 final String fileName = event.getFileName(); 219 if (fileName == null || !fileMessages.containsKey(fileName)) { 220 writeException(throwable); 221 } 222 else { 223 final FileMessages messages = fileMessages.get(fileName); 224 messages.addException(throwable); 225 } 226 } 227 228 /** 229 * Writes the exception event to the print writer. 230 * 231 * @param throwable The 232 */ 233 private void writeException(Throwable throwable) { 234 writer.println("<exception>"); 235 writer.println("<![CDATA["); 236 237 final StringWriter stringWriter = new StringWriter(); 238 final PrintWriter printer = new PrintWriter(stringWriter); 239 throwable.printStackTrace(printer); 240 writer.println(encode(stringWriter.toString())); 241 242 writer.println("]]>"); 243 writer.println("</exception>"); 244 } 245 246 /** 247 * Escape <, > & ' and " as their entities. 248 * 249 * @param value the value to escape. 250 * @return the escaped value if necessary. 251 */ 252 public static String encode(String value) { 253 final StringBuilder sb = new StringBuilder(256); 254 for (int i = 0; i < value.length(); i++) { 255 final char chr = value.charAt(i); 256 switch (chr) { 257 case '<': 258 sb.append("<"); 259 break; 260 case '>': 261 sb.append(">"); 262 break; 263 case '\'': 264 sb.append("'"); 265 break; 266 case '\"': 267 sb.append("""); 268 break; 269 case '&': 270 sb.append("&"); 271 break; 272 case '\r': 273 break; 274 case '\n': 275 sb.append(" "); 276 break; 277 default: 278 if (Character.isISOControl(chr)) { 279 // true escape characters need '&' before, but it also requires XML 1.1 280 // until https://github.com/checkstyle/checkstyle/issues/5168 281 sb.append("#x"); 282 sb.append(Integer.toHexString(chr)); 283 sb.append(';'); 284 } 285 else { 286 sb.append(chr); 287 } 288 break; 289 } 290 } 291 return sb.toString(); 292 } 293 294 /** 295 * Finds whether the given argument is character or entity reference. 296 * 297 * @param ent the possible entity to look for. 298 * @return whether the given argument a character or entity reference 299 */ 300 public static boolean isReference(String ent) { 301 boolean reference = false; 302 303 if (ent.charAt(0) == '&' && ent.endsWith(";")) { 304 if (ent.charAt(1) == '#') { 305 // prefix is "&#" 306 int prefixLength = 2; 307 308 int radix = BASE_10; 309 if (ent.charAt(2) == 'x') { 310 prefixLength++; 311 radix = BASE_16; 312 } 313 try { 314 Integer.parseInt( 315 ent.substring(prefixLength, ent.length() - 1), radix); 316 reference = true; 317 } 318 catch (final NumberFormatException ignored) { 319 reference = false; 320 } 321 } 322 else { 323 final String name = ent.substring(1, ent.length() - 1); 324 for (String element : ENTITIES) { 325 if (name.equals(element)) { 326 reference = true; 327 break; 328 } 329 } 330 } 331 } 332 333 return reference; 334 } 335 336 /** 337 * The registered file messages. 338 */ 339 private static final class FileMessages { 340 341 /** The file error events. */ 342 private final List<AuditEvent> errors = new ArrayList<>(); 343 344 /** The file exceptions. */ 345 private final List<Throwable> exceptions = new ArrayList<>(); 346 347 /** 348 * Returns the file error events. 349 * 350 * @return the file error events. 351 */ 352 public List<AuditEvent> getErrors() { 353 return Collections.unmodifiableList(errors); 354 } 355 356 /** 357 * Adds the given error event to the messages. 358 * 359 * @param event the error event. 360 */ 361 public void addError(AuditEvent event) { 362 errors.add(event); 363 } 364 365 /** 366 * Returns the file exceptions. 367 * 368 * @return the file exceptions. 369 */ 370 public List<Throwable> getExceptions() { 371 return Collections.unmodifiableList(exceptions); 372 } 373 374 /** 375 * Adds the given exception to the messages. 376 * 377 * @param throwable the file exception 378 */ 379 public void addException(Throwable throwable) { 380 exceptions.add(throwable); 381 } 382 383 } 384 385}