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