001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038 039/** 040 * Simple XML logger. 041 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case 042 * we want to localize error messages or simply that file names are 043 * localized and takes care about escaping as well. 044 */ 045// -@cs[AbbreviationAsWordInName] We can not change it as, 046// check's name is part of API (used in configurations). 047public class XMLLogger 048 extends AbstractAutomaticBean 049 implements AuditListener { 050 051 /** Decimal radix. */ 052 private static final int BASE_10 = 10; 053 054 /** Hex radix. */ 055 private static final int BASE_16 = 16; 056 057 /** Some known entities to detect. */ 058 private static final String[] ENTITIES = {"gt", "amp", "lt", "apos", 059 "quot", }; 060 061 /** Close output stream in auditFinished. */ 062 private final boolean closeStream; 063 064 /** The writer lock object. */ 065 private final Object writerLock = new Object(); 066 067 /** Holds all messages for the given file. */ 068 private final Map<String, FileMessages> fileMessages = 069 new ConcurrentHashMap<>(); 070 071 /** 072 * Helper writer that allows easy encoding and printing. 073 */ 074 private final PrintWriter writer; 075 076 /** 077 * Creates a new {@code XMLLogger} instance. 078 * Sets the output to a defined stream. 079 * 080 * @param outputStream the stream to write logs to. 081 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished() 082 * @throws IllegalArgumentException if outputStreamOptions is null. 083 * @noinspection deprecation 084 * @noinspectionreason We are forced to keep AutomaticBean compatability 085 * because of maven-checkstyle-plugin. Until #12873. 086 */ 087 public XMLLogger(OutputStream outputStream, 088 AutomaticBean.OutputStreamOptions outputStreamOptions) { 089 this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name())); 090 } 091 092 /** 093 * Creates a new {@code XMLLogger} instance. 094 * Sets the output to a defined stream. 095 * 096 * @param outputStream the stream to write logs to. 097 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished() 098 * @throws IllegalArgumentException if outputStreamOptions is null. 099 */ 100 public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) { 101 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); 102 if (outputStreamOptions == null) { 103 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null"); 104 } 105 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 106 } 107 108 @Override 109 protected void finishLocalSetup() { 110 // No code by default 111 } 112 113 @Override 114 public void auditStarted(AuditEvent event) { 115 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 116 117 final String version = XMLLogger.class.getPackage().getImplementationVersion(); 118 119 writer.println("<checkstyle version=\"" + version + "\">"); 120 } 121 122 @Override 123 public void auditFinished(AuditEvent event) { 124 writer.println("</checkstyle>"); 125 if (closeStream) { 126 writer.close(); 127 } 128 else { 129 writer.flush(); 130 } 131 } 132 133 @Override 134 public void fileStarted(AuditEvent event) { 135 fileMessages.put(event.getFileName(), new FileMessages()); 136 } 137 138 @Override 139 public void fileFinished(AuditEvent event) { 140 final String fileName = event.getFileName(); 141 final FileMessages messages = fileMessages.get(fileName); 142 143 synchronized (writerLock) { 144 writeFileMessages(fileName, messages); 145 } 146 147 fileMessages.remove(fileName); 148 } 149 150 /** 151 * Prints the file section with all file errors and exceptions. 152 * 153 * @param fileName The file name, as should be printed in the opening file tag. 154 * @param messages The file messages. 155 */ 156 private void writeFileMessages(String fileName, FileMessages messages) { 157 writeFileOpeningTag(fileName); 158 if (messages != null) { 159 for (AuditEvent errorEvent : messages.getErrors()) { 160 writeFileError(errorEvent); 161 } 162 for (Throwable exception : messages.getExceptions()) { 163 writeException(exception); 164 } 165 } 166 writeFileClosingTag(); 167 } 168 169 /** 170 * Prints the "file" opening tag with the given filename. 171 * 172 * @param fileName The filename to output. 173 */ 174 private void writeFileOpeningTag(String fileName) { 175 writer.println("<file name=\"" + encode(fileName) + "\">"); 176 } 177 178 /** 179 * Prints the "file" closing tag. 180 */ 181 private void writeFileClosingTag() { 182 writer.println("</file>"); 183 } 184 185 @Override 186 public void addError(AuditEvent event) { 187 if (event.getSeverityLevel() != SeverityLevel.IGNORE) { 188 final String fileName = event.getFileName(); 189 if (fileName == null || !fileMessages.containsKey(fileName)) { 190 synchronized (writerLock) { 191 writeFileError(event); 192 } 193 } 194 else { 195 final FileMessages messages = fileMessages.get(fileName); 196 messages.addError(event); 197 } 198 } 199 } 200 201 /** 202 * Outputs the given event to the writer. 203 * 204 * @param event An event to print. 205 */ 206 private void writeFileError(AuditEvent event) { 207 writer.print("<error" + " line=\"" + event.getLine() + "\""); 208 if (event.getColumn() > 0) { 209 writer.print(" column=\"" + event.getColumn() + "\""); 210 } 211 writer.print(" severity=\"" 212 + event.getSeverityLevel().getName() 213 + "\""); 214 writer.print(" message=\"" 215 + encode(event.getMessage()) 216 + "\""); 217 writer.print(" source=\""); 218 if (event.getModuleId() == null) { 219 writer.print(encode(event.getSourceName())); 220 } 221 else { 222 writer.print(encode(event.getModuleId())); 223 } 224 writer.println("\"/>"); 225 } 226 227 @Override 228 public void addException(AuditEvent event, Throwable throwable) { 229 final String fileName = event.getFileName(); 230 if (fileName == null || !fileMessages.containsKey(fileName)) { 231 synchronized (writerLock) { 232 writeException(throwable); 233 } 234 } 235 else { 236 final FileMessages messages = fileMessages.get(fileName); 237 messages.addException(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 <, > & ' and " 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("<"); 272 break; 273 case '>': 274 sb.append(">"); 275 break; 276 case '\'': 277 sb.append("'"); 278 break; 279 case '\"': 280 sb.append("""); 281 break; 282 case '&': 283 sb.append("&"); 284 break; 285 case '\r': 286 break; 287 case '\n': 288 sb.append(" "); 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) == '&' && CommonUtil.endsWithChar(ent, ';')) { 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 = Collections.synchronizedList(new ArrayList<>()); 356 357 /** The file exceptions. */ 358 private final List<Throwable> exceptions = Collections.synchronizedList(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}