View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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  
38  /**
39   * Simple XML logger.
40   * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
41   * we want to localize error messages or simply that file names are
42   * localized and takes care about escaping as well.
43   */
44  // -@cs[AbbreviationAsWordInName] We can not change it as,
45  // check's name is part of API (used in configurations).
46  public class XMLLogger
47      extends AbstractAutomaticBean
48      implements AuditListener {
49  
50      /** Decimal radix. */
51      private static final int BASE_10 = 10;
52  
53      /** Hex radix. */
54      private static final int BASE_16 = 16;
55  
56      /** Some known entities to detect. */
57      private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
58                                                "quot", };
59  
60      /** Close output stream in auditFinished. */
61      private final boolean closeStream;
62  
63      /** The writer lock object. */
64      private final Object writerLock = new Object();
65  
66      /** Holds all messages for the given file. */
67      private final Map<String, FileMessages> fileMessages =
68              new ConcurrentHashMap<>();
69  
70      /**
71       * Helper writer that allows easy encoding and printing.
72       */
73      private final PrintWriter writer;
74  
75      /**
76       * Creates a new {@code XMLLogger} instance.
77       * Sets the output to a defined stream.
78       *
79       * @param outputStream the stream to write logs to.
80       * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
81       * @throws IllegalArgumentException if outputStreamOptions is null.
82       * @noinspection deprecation
83       * @noinspectionreason We are forced to keep AutomaticBean compatability
84       *     because of maven-checkstyle-plugin. Until #12873.
85       */
86      public XMLLogger(OutputStream outputStream,
87                       AutomaticBean.OutputStreamOptions outputStreamOptions) {
88          this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
89      }
90  
91      /**
92       * Creates a new {@code XMLLogger} instance.
93       * Sets the output to a defined stream.
94       *
95       * @param outputStream the stream to write logs to.
96       * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
97       * @throws IllegalArgumentException if outputStreamOptions is null.
98       */
99      public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
100         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
101         if (outputStreamOptions == null) {
102             throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
103         }
104         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
105     }
106 
107     @Override
108     protected void finishLocalSetup() {
109         // No code by default
110     }
111 
112     @Override
113     public void auditStarted(AuditEvent event) {
114         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
115 
116         final String version = XMLLogger.class.getPackage().getImplementationVersion();
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      *
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      *
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      *
203      * @param event An event to print.
204      */
205     private void writeFileError(AuditEvent event) {
206         writer.print("<error" + " line=\"" + event.getLine() + "\"");
207         if (event.getColumn() > 0) {
208             writer.print(" column=\"" + event.getColumn() + "\"");
209         }
210         writer.print(" severity=\""
211                 + event.getSeverityLevel().getName()
212                 + "\"");
213         writer.print(" message=\""
214                 + encode(event.getMessage())
215                 + "\"");
216         writer.print(" source=\"");
217         if (event.getModuleId() == null) {
218             writer.print(encode(event.getSourceName()));
219         }
220         else {
221             writer.print(encode(event.getModuleId()));
222         }
223         writer.println("\"/>");
224     }
225 
226     @Override
227     public void addException(AuditEvent event, Throwable throwable) {
228         final String fileName = event.getFileName();
229         if (fileName == null || !fileMessages.containsKey(fileName)) {
230             synchronized (writerLock) {
231                 writeException(throwable);
232             }
233         }
234         else {
235             final FileMessages messages = fileMessages.get(fileName);
236             messages.addException(throwable);
237         }
238     }
239 
240     /**
241      * Writes the exception event to the print writer.
242      *
243      * @param throwable The
244      */
245     private void writeException(Throwable throwable) {
246         writer.println("<exception>");
247         writer.println("<![CDATA[");
248 
249         final StringWriter stringWriter = new StringWriter();
250         final PrintWriter printer = new PrintWriter(stringWriter);
251         throwable.printStackTrace(printer);
252         writer.println(encode(stringWriter.toString()));
253 
254         writer.println("]]>");
255         writer.println("</exception>");
256     }
257 
258     /**
259      * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
260      *
261      * @param value the value to escape.
262      * @return the escaped value if necessary.
263      */
264     public static String encode(String value) {
265         final StringBuilder sb = new StringBuilder(256);
266         for (int i = 0; i < value.length(); i++) {
267             final char chr = value.charAt(i);
268             switch (chr) {
269                 case '<':
270                     sb.append("&lt;");
271                     break;
272                 case '>':
273                     sb.append("&gt;");
274                     break;
275                 case '\'':
276                     sb.append("&apos;");
277                     break;
278                 case '\"':
279                     sb.append("&quot;");
280                     break;
281                 case '&':
282                     sb.append("&amp;");
283                     break;
284                 case '\r':
285                     break;
286                 case '\n':
287                     sb.append("&#10;");
288                     break;
289                 default:
290                     if (Character.isISOControl(chr)) {
291                         // true escape characters need '&' before, but it also requires XML 1.1
292                         // until https://github.com/checkstyle/checkstyle/issues/5168
293                         sb.append("#x");
294                         sb.append(Integer.toHexString(chr));
295                         sb.append(';');
296                     }
297                     else {
298                         sb.append(chr);
299                     }
300                     break;
301             }
302         }
303         return sb.toString();
304     }
305 
306     /**
307      * Finds whether the given argument is character or entity reference.
308      *
309      * @param ent the possible entity to look for.
310      * @return whether the given argument a character or entity reference
311      */
312     public static boolean isReference(String ent) {
313         boolean reference = false;
314 
315         if (ent.charAt(0) == '&' && ent.endsWith(";")) {
316             if (ent.charAt(1) == '#') {
317                 // prefix is "&#"
318                 int prefixLength = 2;
319 
320                 int radix = BASE_10;
321                 if (ent.charAt(2) == 'x') {
322                     prefixLength++;
323                     radix = BASE_16;
324                 }
325                 try {
326                     Integer.parseInt(
327                         ent.substring(prefixLength, ent.length() - 1), radix);
328                     reference = true;
329                 }
330                 catch (final NumberFormatException ignored) {
331                     reference = false;
332                 }
333             }
334             else {
335                 final String name = ent.substring(1, ent.length() - 1);
336                 for (String element : ENTITIES) {
337                     if (name.equals(element)) {
338                         reference = true;
339                         break;
340                     }
341                 }
342             }
343         }
344 
345         return reference;
346     }
347 
348     /**
349      * The registered file messages.
350      */
351     private static final class FileMessages {
352 
353         /** The file error events. */
354         private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
355 
356         /** The file exceptions. */
357         private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
358 
359         /**
360          * Returns the file error events.
361          *
362          * @return the file error events.
363          */
364         public List<AuditEvent> getErrors() {
365             return Collections.unmodifiableList(errors);
366         }
367 
368         /**
369          * Adds the given error event to the messages.
370          *
371          * @param event the error event.
372          */
373         public void addError(AuditEvent event) {
374             errors.add(event);
375         }
376 
377         /**
378          * Returns the file exceptions.
379          *
380          * @return the file exceptions.
381          */
382         public List<Throwable> getExceptions() {
383             return Collections.unmodifiableList(exceptions);
384         }
385 
386         /**
387          * Adds the given exception to the messages.
388          *
389          * @param throwable the file exception
390          */
391         public void addException(Throwable throwable) {
392             exceptions.add(throwable);
393         }
394 
395     }
396 
397 }