View Javadoc
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      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      private final Object writerLock = new Object();
70  
71      /** Holds all messages for the given file. */
72      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      public XMLLogger(OutputStream outputStream, boolean closeStream) {
90          writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
91          this.closeStream = closeStream;
92      }
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     public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
101         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
102         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
103     }
104 
105     @Override
106     protected void finishLocalSetup() throws CheckstyleException {
107         // No code by default
108     }
109 
110     @Override
111     public void auditStarted(AuditEvent event) {
112         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
113 
114         final ResourceBundle compilationProperties =
115             ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
116         final String version =
117             compilationProperties.getString("checkstyle.compile.version");
118 
119         writer.println("<checkstyle version=\"" + version + "\">");
120     }
121 
122     @Override
123     public void auditFinished(AuditEvent event) {
124         writer.println("</checkstyle>");
125         if (closeStream) {
126             writer.close();
127         }
128         else {
129             writer.flush();
130         }
131     }
132 
133     @Override
134     public void fileStarted(AuditEvent event) {
135         fileMessages.put(event.getFileName(), new FileMessages());
136     }
137 
138     @Override
139     public void fileFinished(AuditEvent event) {
140         final String fileName = event.getFileName();
141         final FileMessages messages = fileMessages.get(fileName);
142 
143         synchronized (writerLock) {
144             writeFileMessages(fileName, messages);
145         }
146 
147         fileMessages.remove(fileName);
148     }
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         writeFileOpeningTag(fileName);
157         if (messages != null) {
158             for (AuditEvent errorEvent : messages.getErrors()) {
159                 writeFileError(errorEvent);
160             }
161             for (Throwable exception : messages.getExceptions()) {
162                 writeException(exception);
163             }
164         }
165         writeFileClosingTag();
166     }
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         writer.println("<file name=\"" + encode(fileName) + "\">");
174     }
175 
176     /**
177      * Prints the "file" closing tag.
178      */
179     private void writeFileClosingTag() {
180         writer.println("</file>");
181     }
182 
183     @Override
184     public void addError(AuditEvent event) {
185         if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
186             final String fileName = event.getFileName();
187             if (fileName == null || !fileMessages.containsKey(fileName)) {
188                 synchronized (writerLock) {
189                     writeFileError(event);
190                 }
191             }
192             else {
193                 final FileMessages messages = fileMessages.get(fileName);
194                 messages.addError(event);
195             }
196         }
197     }
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         writer.print("<error" + " line=\"" + event.getLine() + "\"");
205         if (event.getColumn() > 0) {
206             writer.print(" column=\"" + event.getColumn() + "\"");
207         }
208         writer.print(" severity=\""
209                 + event.getSeverityLevel().getName()
210                 + "\"");
211         writer.print(" message=\""
212                 + encode(event.getMessage())
213                 + "\"");
214         writer.print(" source=\"");
215         if (event.getModuleId() == null) {
216             writer.print(encode(event.getSourceName()));
217         }
218         else {
219             writer.print(encode(event.getModuleId()));
220         }
221         writer.println("\"/>");
222     }
223 
224     @Override
225     public void addException(AuditEvent event, Throwable throwable) {
226         final String fileName = event.getFileName();
227         if (fileName == null || !fileMessages.containsKey(fileName)) {
228             synchronized (writerLock) {
229                 writeException(throwable);
230             }
231         }
232         else {
233             final FileMessages messages = fileMessages.get(fileName);
234             messages.addException(throwable);
235         }
236     }
237 
238     /**
239      * Writes the exception event to the print writer.
240      * @param throwable The
241      */
242     private void writeException(Throwable throwable) {
243         final StringWriter stringWriter = new StringWriter();
244         final PrintWriter printer = new PrintWriter(stringWriter);
245         printer.println("<exception>");
246         printer.println("<![CDATA[");
247         throwable.printStackTrace(printer);
248         printer.println("]]>");
249         printer.println("</exception>");
250         writer.println(encode(stringWriter.toString()));
251     }
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         final StringBuilder sb = new StringBuilder(256);
260         for (int i = 0; i < value.length(); i++) {
261             final char chr = value.charAt(i);
262             switch (chr) {
263                 case '<':
264                     sb.append("&lt;");
265                     break;
266                 case '>':
267                     sb.append("&gt;");
268                     break;
269                 case '\'':
270                     sb.append("&apos;");
271                     break;
272                 case '\"':
273                     sb.append("&quot;");
274                     break;
275                 case '&':
276                     sb.append("&amp;");
277                     break;
278                 case '\r':
279                     break;
280                 case '\n':
281                     sb.append("&#10;");
282                     break;
283                 default:
284                     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                         sb.append("#x");
288                         sb.append(Integer.toHexString(chr));
289                         sb.append(';');
290                     }
291                     else {
292                         sb.append(chr);
293                     }
294                     break;
295             }
296         }
297         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         boolean reference = false;
307 
308         if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
309             reference = false;
310         }
311         else if (ent.charAt(1) == '#') {
312             // prefix is "&#"
313             int prefixLength = 2;
314 
315             int radix = BASE_10;
316             if (ent.charAt(2) == 'x') {
317                 prefixLength++;
318                 radix = BASE_16;
319             }
320             try {
321                 Integer.parseInt(
322                     ent.substring(prefixLength, ent.length() - 1), radix);
323                 reference = true;
324             }
325             catch (final NumberFormatException ignored) {
326                 reference = false;
327             }
328         }
329         else {
330             final String name = ent.substring(1, ent.length() - 1);
331             for (String element : ENTITIES) {
332                 if (name.equals(element)) {
333                     reference = true;
334                     break;
335                 }
336             }
337         }
338         return reference;
339     }
340 
341     /**
342      * The registered file messages.
343      */
344     private static class FileMessages {
345         /** The file error events. */
346         private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
347 
348         /** The file exceptions. */
349         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             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             errors.add(event);
365         }
366 
367         /**
368          * Returns the file exceptions.
369          * @return the file exceptions.
370          */
371         public List<Throwable> getExceptions() {
372             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             exceptions.add(throwable);
381         }
382     }
383 }