View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.HashMap;
30  import java.util.List;
31  import java.util.Map;
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      /** Holds all messages for the given file. */
64      private final Map<String, FileMessages> fileMessages =
65              new HashMap<>();
66  
67      /**
68       * Helper writer that allows easy encoding and printing.
69       */
70      private final PrintWriter writer;
71  
72      /**
73       * Creates a new {@code XMLLogger} instance.
74       * Sets the output to a defined stream.
75       *
76       * @param outputStream the stream to write logs to.
77       * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
78       * @throws IllegalArgumentException if outputStreamOptions is null.
79       * @noinspection deprecation
80       * @noinspectionreason We are forced to keep AutomaticBean compatability
81       *     because of maven-checkstyle-plugin. Until #12873.
82       */
83      public XMLLogger(OutputStream outputStream,
84                       AutomaticBean.OutputStreamOptions outputStreamOptions) {
85          this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
86      }
87  
88      /**
89       * Creates a new {@code XMLLogger} instance.
90       * Sets the output to a defined stream.
91       *
92       * @param outputStream the stream to write logs to.
93       * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
94       * @throws IllegalArgumentException if outputStreamOptions is null.
95       */
96      public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
97          writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
98          if (outputStreamOptions == null) {
99              throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
100         }
101         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
102     }
103 
104     @Override
105     protected void finishLocalSetup() {
106         // No code by default
107     }
108 
109     /**
110      * Returns the version string printed.
111      *
112      */
113     private void printVersionString() {
114         final String version = XMLLogger.class.getPackage().getImplementationVersion();
115         writer.println("<checkstyle version=\"" + version + "\">");
116     }
117 
118     @Override
119     public void auditStarted(AuditEvent event) {
120         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
121 
122         printVersionString();
123     }
124 
125     @Override
126     public void auditFinished(AuditEvent event) {
127         writer.println("</checkstyle>");
128         if (closeStream) {
129             writer.close();
130         }
131         else {
132             writer.flush();
133         }
134     }
135 
136     @Override
137     public void fileStarted(AuditEvent event) {
138         fileMessages.put(event.getFileName(), new FileMessages());
139     }
140 
141     @Override
142     public void fileFinished(AuditEvent event) {
143         final String fileName = event.getFileName();
144         final FileMessages messages = fileMessages.remove(fileName);
145         writeFileMessages(fileName, messages);
146     }
147 
148     /**
149      * Prints the file section with all file errors and exceptions.
150      *
151      * @param fileName The file name, as should be printed in the opening file tag.
152      * @param messages The file messages.
153      */
154     private void writeFileMessages(String fileName, FileMessages messages) {
155         writeFileOpeningTag(fileName);
156         if (messages != null) {
157             for (AuditEvent errorEvent : messages.getErrors()) {
158                 writeFileError(errorEvent);
159             }
160             for (Throwable exception : messages.getExceptions()) {
161                 writeException(exception);
162             }
163         }
164         writeFileClosingTag();
165     }
166 
167     /**
168      * Prints the "file" opening tag with the given filename.
169      *
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             final FileMessages messages = fileMessages.get(fileName);
188             if (messages != null) {
189                 messages.addError(event);
190             }
191             else {
192                 writeFileError(event);
193             }
194         }
195     }
196 
197     /**
198      * Outputs the given event to the writer.
199      *
200      * @param event An event to print.
201      */
202     private void writeFileError(AuditEvent event) {
203         writer.print("<error" + " line=\"" + event.getLine() + "\"");
204         if (event.getColumn() > 0) {
205             writer.print(" column=\"" + event.getColumn() + "\"");
206         }
207         writer.print(" severity=\""
208                 + event.getSeverityLevel().getName()
209                 + "\"");
210         writer.print(" message=\""
211                 + encode(event.getMessage())
212                 + "\"");
213         writer.print(" source=\"");
214         final String sourceValue;
215         if (event.getModuleId() == null) {
216             sourceValue = event.getSourceName();
217         }
218         else {
219             sourceValue = event.getModuleId();
220         }
221         writer.print(encode(sourceValue));
222         writer.println("\"/>");
223     }
224 
225     @Override
226     public void addException(AuditEvent event, Throwable throwable) {
227         final String fileName = event.getFileName();
228         final FileMessages messages = fileMessages.get(fileName);
229         if (messages != null) {
230             messages.addException(throwable);
231         }
232         else {
233             writeException(throwable);
234         }
235     }
236 
237     /**
238      * Writes the exception event to the print writer.
239      *
240      * @param throwable The
241      */
242     private void writeException(Throwable throwable) {
243         writer.println("<exception>");
244         writer.println("<![CDATA[");
245 
246         final StringWriter stringWriter = new StringWriter();
247         final PrintWriter printer = new PrintWriter(stringWriter);
248         throwable.printStackTrace(printer);
249         writer.println(encode(stringWriter.toString()));
250 
251         writer.println("]]>");
252         writer.println("</exception>");
253     }
254 
255     /**
256      * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
257      *
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("&lt;");
268                     break;
269                 case '>':
270                     sb.append("&gt;");
271                     break;
272                 case '\'':
273                     sb.append("&apos;");
274                     break;
275                 case '\"':
276                     sb.append("&quot;");
277                     break;
278                 case '&':
279                     sb.append("&amp;");
280                     break;
281                 case '\r':
282                     break;
283                 case '\n':
284                     sb.append("&#10;");
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      *
306      * @param ent the possible entity to look for.
307      * @return whether the given argument a character or entity reference
308      */
309     public static boolean isReference(String ent) {
310         boolean reference = false;
311 
312         if (ent.charAt(0) == '&' && ent.endsWith(";")) {
313             if (ent.charAt(1) == '#') {
314                 // prefix is "&#"
315                 int prefixLength = 2;
316 
317                 int radix = BASE_10;
318                 if (ent.charAt(2) == 'x') {
319                     prefixLength++;
320                     radix = BASE_16;
321                 }
322                 try {
323                     Integer.parseInt(
324                         ent.substring(prefixLength, ent.length() - 1), radix);
325                     reference = true;
326                 }
327                 catch (final NumberFormatException ignored) {
328                     reference = false;
329                 }
330             }
331             else {
332                 final String name = ent.substring(1, ent.length() - 1);
333                 for (String element : ENTITIES) {
334                     if (name.equals(element)) {
335                         reference = true;
336                         break;
337                     }
338                 }
339             }
340         }
341 
342         return reference;
343     }
344 
345     /**
346      * The registered file messages.
347      */
348     private static final class FileMessages {
349 
350         /** The file error events. */
351         private final List<AuditEvent> errors = new ArrayList<>();
352 
353         /** The file exceptions. */
354         private final List<Throwable> exceptions = new ArrayList<>();
355 
356         /**
357          * Returns the file error events.
358          *
359          * @return the file error events.
360          */
361         public List<AuditEvent> getErrors() {
362             return Collections.unmodifiableList(errors);
363         }
364 
365         /**
366          * Adds the given error event to the messages.
367          *
368          * @param event the error event.
369          */
370         public void addError(AuditEvent event) {
371             errors.add(event);
372         }
373 
374         /**
375          * Returns the file exceptions.
376          *
377          * @return the file exceptions.
378          */
379         public List<Throwable> getExceptions() {
380             return Collections.unmodifiableList(exceptions);
381         }
382 
383         /**
384          * Adds the given exception to the messages.
385          *
386          * @param throwable the file exception
387          */
388         public void addException(Throwable throwable) {
389             exceptions.add(throwable);
390         }
391 
392     }
393 
394 }