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