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