001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.Locale; 031import java.util.Map; 032import java.util.ResourceBundle; 033import java.util.concurrent.ConcurrentHashMap; 034 035import com.puppycrawl.tools.checkstyle.api.AuditEvent; 036import com.puppycrawl.tools.checkstyle.api.AuditListener; 037import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 038import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 039import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 040 041/** 042 * Simple XML logger. 043 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case 044 * we want to localize error messages or simply that file names are 045 * localized and takes care about escaping as well. 046 047 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a> 048 */ 049// -@cs[AbbreviationAsWordInName] We can not change it as, 050// check's name is part of API (used in configurations). 051public class XMLLogger 052 extends AutomaticBean 053 implements AuditListener { 054 /** Decimal radix. */ 055 private static final int BASE_10 = 10; 056 057 /** Hex radix. */ 058 private static final int BASE_16 = 16; 059 060 /** Some known entities to detect. */ 061 private static final String[] ENTITIES = {"gt", "amp", "lt", "apos", 062 "quot", }; 063 064 /** Close output stream in auditFinished. */ 065 private final boolean closeStream; 066 067 /** The writer lock object. */ 068 private final Object writerLock = new Object(); 069 070 /** Holds all messages for the given file. */ 071 private final Map<String, FileMessages> fileMessages = 072 new ConcurrentHashMap<>(); 073 074 /** 075 * Helper writer that allows easy encoding and printing. 076 */ 077 private final PrintWriter writer; 078 079 /** 080 * Creates a new {@code XMLLogger} instance. 081 * Sets the output to a defined stream. 082 * @param outputStream the stream to write logs to. 083 * @param closeStream close oS in auditFinished 084 * @deprecated in order to fullfil demands of BooleanParameter IDEA check. 085 * @noinspection BooleanParameter 086 */ 087 @Deprecated 088 public XMLLogger(OutputStream outputStream, boolean closeStream) { 089 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); 090 this.closeStream = closeStream; 091 } 092 093 /** 094 * Creates a new {@code XMLLogger} instance. 095 * Sets the output to a defined stream. 096 * @param outputStream the stream to write logs to. 097 * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished() 098 */ 099 public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) { 100 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); 101 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 102 } 103 104 @Override 105 public void auditStarted(AuditEvent event) { 106 writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); 107 108 final ResourceBundle compilationProperties = 109 ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT); 110 final String version = 111 compilationProperties.getString("checkstyle.compile.version"); 112 113 writer.println("<checkstyle version=\"" + version + "\">"); 114 } 115 116 @Override 117 public void auditFinished(AuditEvent event) { 118 fileMessages.forEach(this::writeFileMessages); 119 120 writer.println("</checkstyle>"); 121 if (closeStream) { 122 writer.close(); 123 } 124 else { 125 writer.flush(); 126 } 127 } 128 129 @Override 130 public void fileStarted(AuditEvent event) { 131 fileMessages.put(event.getFileName(), new FileMessages()); 132 } 133 134 @Override 135 public void fileFinished(AuditEvent event) { 136 final String fileName = event.getFileName(); 137 final FileMessages messages = fileMessages.get(fileName); 138 139 synchronized (writerLock) { 140 writeFileMessages(fileName, messages); 141 } 142 143 fileMessages.remove(fileName); 144 } 145 146 /** 147 * Prints the file section with all file errors and exceptions. 148 * @param fileName The file name, as should be printed in the opening file tag. 149 * @param messages The file messages. 150 */ 151 private void writeFileMessages(String fileName, FileMessages messages) { 152 writeFileOpeningTag(fileName); 153 if (messages != null) { 154 for (AuditEvent errorEvent : messages.getErrors()) { 155 writeFileError(errorEvent); 156 } 157 for (Throwable exception : messages.getExceptions()) { 158 writeException(exception); 159 } 160 } 161 writeFileClosingTag(); 162 } 163 164 /** 165 * Prints the "file" opening tag with the given filename. 166 * @param fileName The filename to output. 167 */ 168 private void writeFileOpeningTag(String fileName) { 169 writer.println("<file name=\"" + encode(fileName) + "\">"); 170 } 171 172 /** 173 * Prints the "file" closing tag. 174 */ 175 private void writeFileClosingTag() { 176 writer.println("</file>"); 177 } 178 179 @Override 180 public void addError(AuditEvent event) { 181 if (event.getSeverityLevel() != SeverityLevel.IGNORE) { 182 final String fileName = event.getFileName(); 183 if (fileName == null) { 184 synchronized (writerLock) { 185 writeFileError(event); 186 } 187 } 188 else { 189 final FileMessages messages = fileMessages.computeIfAbsent( 190 fileName, name -> new FileMessages()); 191 messages.addError(event); 192 } 193 } 194 } 195 196 /** 197 * Outputs the given envet 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) { 225 synchronized (writerLock) { 226 writeException(throwable); 227 } 228 } 229 else { 230 final FileMessages messages = fileMessages.computeIfAbsent( 231 fileName, name -> new FileMessages()); 232 messages.addException(throwable); 233 } 234 } 235 236 /** 237 * Writes the exception event to the print writer. 238 * @param throwable The 239 */ 240 private void writeException(Throwable throwable) { 241 final StringWriter stringWriter = new StringWriter(); 242 final PrintWriter printer = new PrintWriter(stringWriter); 243 printer.println("<exception>"); 244 printer.println("<![CDATA["); 245 throwable.printStackTrace(printer); 246 printer.println("]]>"); 247 printer.println("</exception>"); 248 writer.println(encode(stringWriter.toString())); 249 } 250 251 /** 252 * Escape <, > & ' and " as their entities. 253 * @param value the value to escape. 254 * @return the escaped value if necessary. 255 */ 256 public static String encode(String value) { 257 final StringBuilder sb = new StringBuilder(256); 258 for (int i = 0; i < value.length(); i++) { 259 final char chr = value.charAt(i); 260 switch (chr) { 261 case '<': 262 sb.append("<"); 263 break; 264 case '>': 265 sb.append(">"); 266 break; 267 case '\'': 268 sb.append("'"); 269 break; 270 case '\"': 271 sb.append("""); 272 break; 273 case '&': 274 sb.append(encodeAmpersand(value, i)); 275 break; 276 case '\r': 277 break; 278 case '\n': 279 sb.append(" "); 280 break; 281 default: 282 sb.append(chr); 283 break; 284 } 285 } 286 return sb.toString(); 287 } 288 289 /** 290 * Finds whether the given argument is character or entity reference. 291 * @param ent the possible entity to look for. 292 * @return whether the given argument a character or entity reference 293 */ 294 public static boolean isReference(String ent) { 295 boolean reference = false; 296 297 if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) { 298 reference = false; 299 } 300 else 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 return reference; 328 } 329 330 /** 331 * Encodes ampersand in value at required position. 332 * @param value string value, which contains ampersand 333 * @param ampPosition position of ampersand in value 334 * @return encoded ampersand which should be used in xml 335 */ 336 private static String encodeAmpersand(String value, int ampPosition) { 337 final int nextSemi = value.indexOf(';', ampPosition); 338 final String result; 339 if (nextSemi == -1 340 || !isReference(value.substring(ampPosition, nextSemi + 1))) { 341 result = "&"; 342 } 343 else { 344 result = "&"; 345 } 346 return result; 347 } 348 349 /** 350 * The registered file messages. 351 */ 352 private static class FileMessages { 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 * @return the file error events. 362 */ 363 public List<AuditEvent> getErrors() { 364 return Collections.unmodifiableList(errors); 365 } 366 367 /** 368 * Adds the given error event to the messages. 369 * @param event the error event. 370 */ 371 public void addError(AuditEvent event) { 372 errors.add(event); 373 } 374 375 /** 376 * Returns the file exceptions. 377 * @return the file exceptions. 378 */ 379 public List<Throwable> getExceptions() { 380 return Collections.unmodifiableList(exceptions); 381 } 382 383 /** 384 * Adds the given exception to the messages. 385 * @param throwable the file exception 386 */ 387 public void addException(Throwable throwable) { 388 exceptions.add(throwable); 389 } 390 } 391}