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