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