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