1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.Map;
31  import java.util.concurrent.ConcurrentHashMap;
32  
33  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
34  import com.puppycrawl.tools.checkstyle.api.AuditListener;
35  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
36  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  
39  /**
40   * Simple XML logger.
41   * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
42   * we want to localize error messages or simply that file names are
43   * localized and takes care about escaping as well.
44  
45   */
46  // -@cs[AbbreviationAsWordInName] We can not change it as,
47  // check's name is part of API (used in configurations).
48  public class XMLLogger
49      extends AutomaticBean
50      implements AuditListener {
51  
52      /** Decimal radix. */
53      private static final int BASE_10 = 10;
54  
55      /** Hex radix. */
56      private static final int BASE_16 = 16;
57  
58      /** Some known entities to detect. */
59      private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
60                                                "quot", };
61  
62      /** Close output stream in auditFinished. */
63      private final boolean closeStream;
64  
65      /** The writer lock object. */
66      private final Object writerLock = new Object();
67  
68      /** Holds all messages for the given file. */
69      private final Map<String, FileMessages> fileMessages =
70              new ConcurrentHashMap<>();
71  
72      /**
73       * Helper writer that allows easy encoding and printing.
74       */
75      private final PrintWriter writer;
76  
77      /**
78       * Creates a new {@code XMLLogger} instance.
79       * Sets the output to a defined stream.
80       * @param outputStream the stream to write logs to.
81       * @param closeStream close oS in auditFinished
82       * @deprecated in order to fulfill demands of BooleanParameter IDEA check.
83       * @noinspection BooleanParameter
84       */
85      @Deprecated
86      public XMLLogger(OutputStream outputStream, boolean closeStream) {
87          writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
88          this.closeStream = closeStream;
89      }
90  
91      /**
92       * Creates a new {@code XMLLogger} instance.
93       * Sets the output to a defined stream.
94       * @param outputStream the stream to write logs to.
95       * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
96       */
97      public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
98          writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
99          if (outputStreamOptions == null) {
100             throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
101         }
102         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
103     }
104 
105     @Override
106     protected void finishLocalSetup() {
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 String version = XMLLogger.class.getPackage().getImplementationVersion();
115 
116         writer.println("<checkstyle version=\"" + version + "\">");
117     }
118 
119     @Override
120     public void auditFinished(AuditEvent event) {
121         writer.println("</checkstyle>");
122         if (closeStream) {
123             writer.close();
124         }
125         else {
126             writer.flush();
127         }
128     }
129 
130     @Override
131     public void fileStarted(AuditEvent event) {
132         fileMessages.put(event.getFileName(), new FileMessages());
133     }
134 
135     @Override
136     public void fileFinished(AuditEvent event) {
137         final String fileName = event.getFileName();
138         final FileMessages messages = fileMessages.get(fileName);
139 
140         synchronized (writerLock) {
141             writeFileMessages(fileName, messages);
142         }
143 
144         fileMessages.remove(fileName);
145     }
146 
147     /**
148      * Prints the file section with all file errors and exceptions.
149      * @param fileName The file name, as should be printed in the opening file tag.
150      * @param messages The file messages.
151      */
152     private void writeFileMessages(String fileName, FileMessages messages) {
153         writeFileOpeningTag(fileName);
154         if (messages != null) {
155             for (AuditEvent errorEvent : messages.getErrors()) {
156                 writeFileError(errorEvent);
157             }
158             for (Throwable exception : messages.getExceptions()) {
159                 writeException(exception);
160             }
161         }
162         writeFileClosingTag();
163     }
164 
165     /**
166      * Prints the "file" opening tag with the given filename.
167      * @param fileName The filename to output.
168      */
169     private void writeFileOpeningTag(String fileName) {
170         writer.println("<file name=\"" + encode(fileName) + "\">");
171     }
172 
173     /**
174      * Prints the "file" closing tag.
175      */
176     private void writeFileClosingTag() {
177         writer.println("</file>");
178     }
179 
180     @Override
181     public void addError(AuditEvent event) {
182         if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
183             final String fileName = event.getFileName();
184             if (fileName == null || !fileMessages.containsKey(fileName)) {
185                 synchronized (writerLock) {
186                     writeFileError(event);
187                 }
188             }
189             else {
190                 final FileMessages messages = fileMessages.get(fileName);
191                 messages.addError(event);
192             }
193         }
194     }
195 
196     /**
197      * Outputs the given event 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 || !fileMessages.containsKey(fileName)) {
225             synchronized (writerLock) {
226                 writeException(throwable);
227             }
228         }
229         else {
230             final FileMessages messages = fileMessages.get(fileName);
231             messages.addException(throwable);
232         }
233     }
234 
235     /**
236      * Writes the exception event to the print writer.
237      * @param throwable The
238      */
239     private void writeException(Throwable throwable) {
240         writer.println("<exception>");
241         writer.println("<![CDATA[");
242 
243         final StringWriter stringWriter = new StringWriter();
244         final PrintWriter printer = new PrintWriter(stringWriter);
245         throwable.printStackTrace(printer);
246         writer.println(encode(stringWriter.toString()));
247 
248         writer.println("]]>");
249         writer.println("</exception>");
250     }
251 
252     /**
253      * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
254      * @param value the value to escape.
255      * @return the escaped value if necessary.
256      */
257     public static String encode(String value) {
258         final StringBuilder sb = new StringBuilder(256);
259         for (int i = 0; i < value.length(); i++) {
260             final char chr = value.charAt(i);
261             switch (chr) {
262                 case '<':
263                     sb.append("&lt;");
264                     break;
265                 case '>':
266                     sb.append("&gt;");
267                     break;
268                 case '\'':
269                     sb.append("&apos;");
270                     break;
271                 case '\"':
272                     sb.append("&quot;");
273                     break;
274                 case '&':
275                     sb.append("&amp;");
276                     break;
277                 case '\r':
278                     break;
279                 case '\n':
280                     sb.append("&#10;");
281                     break;
282                 default:
283                     if (Character.isISOControl(chr)) {
284                         // true escape characters need '&' before but it also requires XML 1.1
285                         // until https://github.com/checkstyle/checkstyle/issues/5168
286                         sb.append("#x");
287                         sb.append(Integer.toHexString(chr));
288                         sb.append(';');
289                     }
290                     else {
291                         sb.append(chr);
292                     }
293                     break;
294             }
295         }
296         return sb.toString();
297     }
298 
299     /**
300      * Finds whether the given argument is character or entity reference.
301      * @param ent the possible entity to look for.
302      * @return whether the given argument a character or entity reference
303      */
304     public static boolean isReference(String ent) {
305         boolean reference = false;
306 
307         if (ent.charAt(0) != '&' || !CommonUtil.endsWithChar(ent, ';')) {
308             reference = false;
309         }
310         else if (ent.charAt(1) == '#') {
311             // prefix is "&#"
312             int prefixLength = 2;
313 
314             int radix = BASE_10;
315             if (ent.charAt(2) == 'x') {
316                 prefixLength++;
317                 radix = BASE_16;
318             }
319             try {
320                 Integer.parseInt(
321                     ent.substring(prefixLength, ent.length() - 1), radix);
322                 reference = true;
323             }
324             catch (final NumberFormatException ignored) {
325                 reference = false;
326             }
327         }
328         else {
329             final String name = ent.substring(1, ent.length() - 1);
330             for (String element : ENTITIES) {
331                 if (name.equals(element)) {
332                     reference = true;
333                     break;
334                 }
335             }
336         }
337         return reference;
338     }
339 
340     /**
341      * The registered file messages.
342      */
343     private static class FileMessages {
344 
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     }
384 
385 }