Coverage Report - com.puppycrawl.tools.checkstyle.XMLLogger
 
Classes in this File Line Coverage Branch Coverage Complexity
XMLLogger
100%
131/131
100%
48/48
2.35
XMLLogger$1
N/A
N/A
2.35
XMLLogger$FileMessages
100%
9/9
N/A
2.35
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle;
 21  
 
 22  
 import java.io.OutputStream;
 23  
 import java.io.OutputStreamWriter;
 24  
 import java.io.PrintWriter;
 25  
 import java.io.StringWriter;
 26  
 import java.nio.charset.StandardCharsets;
 27  
 import java.util.ArrayList;
 28  
 import java.util.Collections;
 29  
 import java.util.List;
 30  
 import java.util.Locale;
 31  
 import java.util.Map;
 32  
 import java.util.ResourceBundle;
 33  
 import java.util.concurrent.ConcurrentHashMap;
 34  
 
 35  
 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
 36  
 import com.puppycrawl.tools.checkstyle.api.AuditListener;
 37  
 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
 38  
 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
 39  
 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
 40  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 41  
 
 42  
 /**
 43  
  * Simple XML logger.
 44  
  * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
 45  
  * we want to localize error messages or simply that file names are
 46  
  * localized and takes care about escaping as well.
 47  
 
 48  
  * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
 49  
  */
 50  
 // -@cs[AbbreviationAsWordInName] We can not change it as,
 51  
 // check's name is part of API (used in configurations).
 52  
 public class XMLLogger
 53  
     extends AutomaticBean
 54  
     implements AuditListener {
 55  
     /** Decimal radix. */
 56  
     private static final int BASE_10 = 10;
 57  
 
 58  
     /** Hex radix. */
 59  
     private static final int BASE_16 = 16;
 60  
 
 61  
     /** Some known entities to detect. */
 62  4
     private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
 63  
                                               "quot", };
 64  
 
 65  
     /** Close output stream in auditFinished. */
 66  
     private final boolean closeStream;
 67  
 
 68  
     /** The writer lock object. */
 69  24
     private final Object writerLock = new Object();
 70  
 
 71  
     /** Holds all messages for the given file. */
 72  24
     private final Map<String, FileMessages> fileMessages =
 73  
             new ConcurrentHashMap<>();
 74  
 
 75  
     /**
 76  
      * Helper writer that allows easy encoding and printing.
 77  
      */
 78  
     private final PrintWriter writer;
 79  
 
 80  
     /**
 81  
      * Creates a new {@code XMLLogger} instance.
 82  
      * Sets the output to a defined stream.
 83  
      * @param outputStream the stream to write logs to.
 84  
      * @param closeStream close oS in auditFinished
 85  
      * @deprecated in order to fulfill demands of BooleanParameter IDEA check.
 86  
      * @noinspection BooleanParameter
 87  
      */
 88  
     @Deprecated
 89  17
     public XMLLogger(OutputStream outputStream, boolean closeStream) {
 90  17
         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
 91  17
         this.closeStream = closeStream;
 92  17
     }
 93  
 
 94  
     /**
 95  
      * Creates a new {@code XMLLogger} instance.
 96  
      * Sets the output to a defined stream.
 97  
      * @param outputStream the stream to write logs to.
 98  
      * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
 99  
      */
 100  7
     public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
 101  7
         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
 102  7
         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
 103  7
     }
 104  
 
 105  
     @Override
 106  
     protected void finishLocalSetup() throws CheckstyleException {
 107  
         // No code by default
 108  1
     }
 109  
 
 110  
     @Override
 111  
     public void auditStarted(AuditEvent event) {
 112  20
         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
 113  
 
 114  20
         final ResourceBundle compilationProperties =
 115  20
             ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
 116  20
         final String version =
 117  20
             compilationProperties.getString("checkstyle.compile.version");
 118  
 
 119  20
         writer.println("<checkstyle version=\"" + version + "\">");
 120  20
     }
 121  
 
 122  
     @Override
 123  
     public void auditFinished(AuditEvent event) {
 124  20
         writer.println("</checkstyle>");
 125  20
         if (closeStream) {
 126  17
             writer.close();
 127  
         }
 128  
         else {
 129  3
             writer.flush();
 130  
         }
 131  20
     }
 132  
 
 133  
     @Override
 134  
     public void fileStarted(AuditEvent event) {
 135  14
         fileMessages.put(event.getFileName(), new FileMessages());
 136  14
     }
 137  
 
 138  
     @Override
 139  
     public void fileFinished(AuditEvent event) {
 140  16
         final String fileName = event.getFileName();
 141  16
         final FileMessages messages = fileMessages.get(fileName);
 142  
 
 143  16
         synchronized (writerLock) {
 144  16
             writeFileMessages(fileName, messages);
 145  16
         }
 146  
 
 147  16
         fileMessages.remove(fileName);
 148  16
     }
 149  
 
 150  
     /**
 151  
      * Prints the file section with all file errors and exceptions.
 152  
      * @param fileName The file name, as should be printed in the opening file tag.
 153  
      * @param messages The file messages.
 154  
      */
 155  
     private void writeFileMessages(String fileName, FileMessages messages) {
 156  16
         writeFileOpeningTag(fileName);
 157  16
         if (messages != null) {
 158  14
             for (AuditEvent errorEvent : messages.getErrors()) {
 159  7
                 writeFileError(errorEvent);
 160  7
             }
 161  14
             for (Throwable exception : messages.getExceptions()) {
 162  2
                 writeException(exception);
 163  2
             }
 164  
         }
 165  16
         writeFileClosingTag();
 166  16
     }
 167  
 
 168  
     /**
 169  
      * Prints the "file" opening tag with the given filename.
 170  
      * @param fileName The filename to output.
 171  
      */
 172  
     private void writeFileOpeningTag(String fileName) {
 173  16
         writer.println("<file name=\"" + encode(fileName) + "\">");
 174  16
     }
 175  
 
 176  
     /**
 177  
      * Prints the "file" closing tag.
 178  
      */
 179  
     private void writeFileClosingTag() {
 180  16
         writer.println("</file>");
 181  16
     }
 182  
 
 183  
     @Override
 184  
     public void addError(AuditEvent event) {
 185  10
         if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
 186  9
             final String fileName = event.getFileName();
 187  9
             if (fileName == null || !fileMessages.containsKey(fileName)) {
 188  2
                 synchronized (writerLock) {
 189  2
                     writeFileError(event);
 190  2
                 }
 191  
             }
 192  
             else {
 193  7
                 final FileMessages messages = fileMessages.get(fileName);
 194  7
                 messages.addError(event);
 195  
             }
 196  
         }
 197  10
     }
 198  
 
 199  
     /**
 200  
      * Outputs the given event to the writer.
 201  
      * @param event An event to print.
 202  
      */
 203  
     private void writeFileError(AuditEvent event) {
 204  9
         writer.print("<error" + " line=\"" + event.getLine() + "\"");
 205  9
         if (event.getColumn() > 0) {
 206  4
             writer.print(" column=\"" + event.getColumn() + "\"");
 207  
         }
 208  18
         writer.print(" severity=\""
 209  9
                 + event.getSeverityLevel().getName()
 210  
                 + "\"");
 211  18
         writer.print(" message=\""
 212  9
                 + encode(event.getMessage())
 213  
                 + "\"");
 214  9
         writer.print(" source=\"");
 215  9
         if (event.getModuleId() == null) {
 216  8
             writer.print(encode(event.getSourceName()));
 217  
         }
 218  
         else {
 219  1
             writer.print(encode(event.getModuleId()));
 220  
         }
 221  9
         writer.println("\"/>");
 222  9
     }
 223  
 
 224  
     @Override
 225  
     public void addException(AuditEvent event, Throwable throwable) {
 226  5
         final String fileName = event.getFileName();
 227  5
         if (fileName == null || !fileMessages.containsKey(fileName)) {
 228  3
             synchronized (writerLock) {
 229  3
                 writeException(throwable);
 230  3
             }
 231  
         }
 232  
         else {
 233  2
             final FileMessages messages = fileMessages.get(fileName);
 234  2
             messages.addException(throwable);
 235  
         }
 236  5
     }
 237  
 
 238  
     /**
 239  
      * Writes the exception event to the print writer.
 240  
      * @param throwable The
 241  
      */
 242  
     private void writeException(Throwable throwable) {
 243  5
         final StringWriter stringWriter = new StringWriter();
 244  5
         final PrintWriter printer = new PrintWriter(stringWriter);
 245  5
         printer.println("<exception>");
 246  5
         printer.println("<![CDATA[");
 247  5
         throwable.printStackTrace(printer);
 248  5
         printer.println("]]>");
 249  5
         printer.println("</exception>");
 250  5
         writer.println(encode(stringWriter.toString()));
 251  5
     }
 252  
 
 253  
     /**
 254  
      * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
 255  
      * @param value the value to escape.
 256  
      * @return the escaped value if necessary.
 257  
      */
 258  
     public static String encode(String value) {
 259  51
         final StringBuilder sb = new StringBuilder(256);
 260  1577
         for (int i = 0; i < value.length(); i++) {
 261  1526
             final char chr = value.charAt(i);
 262  1526
             switch (chr) {
 263  
                 case '<':
 264  16
                     sb.append("&lt;");
 265  16
                     break;
 266  
                 case '>':
 267  16
                     sb.append("&gt;");
 268  16
                     break;
 269  
                 case '\'':
 270  5
                     sb.append("&apos;");
 271  5
                     break;
 272  
                 case '\"':
 273  1
                     sb.append("&quot;");
 274  1
                     break;
 275  
                 case '&':
 276  5
                     sb.append("&amp;");
 277  5
                     break;
 278  
                 case '\r':
 279  5
                     break;
 280  
                 case '\n':
 281  25
                     sb.append("&#10;");
 282  25
                     break;
 283  
                 default:
 284  1453
                     if (Character.isISOControl(chr)) {
 285  
                         // true escape characters need '&' before but it also requires XML 1.1
 286  
                         // until https://github.com/checkstyle/checkstyle/issues/5168
 287  2
                         sb.append("#x");
 288  2
                         sb.append(Integer.toHexString(chr));
 289  2
                         sb.append(';');
 290  
                     }
 291  
                     else {
 292  1451
                         sb.append(chr);
 293  
                     }
 294  
                     break;
 295  
             }
 296  
         }
 297  51
         return sb.toString();
 298  
     }
 299  
 
 300  
     /**
 301  
      * Finds whether the given argument is character or entity reference.
 302  
      * @param ent the possible entity to look for.
 303  
      * @return whether the given argument a character or entity reference
 304  
      */
 305  
     public static boolean isReference(String ent) {
 306  15
         boolean reference = false;
 307  
 
 308  15
         if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
 309  2
             reference = false;
 310  
         }
 311  13
         else if (ent.charAt(1) == '#') {
 312  
             // prefix is "&#"
 313  7
             int prefixLength = 2;
 314  
 
 315  7
             int radix = BASE_10;
 316  7
             if (ent.charAt(2) == 'x') {
 317  3
                 prefixLength++;
 318  3
                 radix = BASE_16;
 319  
             }
 320  
             try {
 321  14
                 Integer.parseInt(
 322  7
                     ent.substring(prefixLength, ent.length() - 1), radix);
 323  2
                 reference = true;
 324  
             }
 325  5
             catch (final NumberFormatException ignored) {
 326  5
                 reference = false;
 327  2
             }
 328  7
         }
 329  
         else {
 330  6
             final String name = ent.substring(1, ent.length() - 1);
 331  21
             for (String element : ENTITIES) {
 332  20
                 if (name.equals(element)) {
 333  5
                     reference = true;
 334  5
                     break;
 335  
                 }
 336  
             }
 337  
         }
 338  15
         return reference;
 339  
     }
 340  
 
 341  
     /**
 342  
      * The registered file messages.
 343  
      */
 344  28
     private static class FileMessages {
 345  
         /** The file error events. */
 346  14
         private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
 347  
 
 348  
         /** The file exceptions. */
 349  14
         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  14
             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  7
             errors.add(event);
 365  7
         }
 366  
 
 367  
         /**
 368  
          * Returns the file exceptions.
 369  
          * @return the file exceptions.
 370  
          */
 371  
         public List<Throwable> getExceptions() {
 372  14
             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  2
             exceptions.add(throwable);
 381  2
         }
 382  
     }
 383  
 }