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.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      /** Holds all messages for the given file. */
64      private final Map<String, FileMessages> fileMessages =
65              new ConcurrentHashMap<>();
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     @Override
110     public void auditStarted(AuditEvent event) {
111         writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
112 
113         final String version = XMLLogger.class.getPackage().getImplementationVersion();
114 
115         writer.println("<checkstyle version=\"" + version + "\">");
116     }
117 
118     @Override
119     public void auditFinished(AuditEvent event) {
120         writer.println("</checkstyle>");
121         if (closeStream) {
122             writer.close();
123         }
124         else {
125             writer.flush();
126         }
127     }
128 
129     @Override
130     public void fileStarted(AuditEvent event) {
131         fileMessages.put(event.getFileName(), new FileMessages());
132     }
133 
134     @Override
135     public void fileFinished(AuditEvent event) {
136         final String fileName = event.getFileName();
137         final FileMessages messages = fileMessages.remove(fileName);
138         writeFileMessages(fileName, messages);
139     }
140 
141     /**
142      * Prints the file section with all file errors and exceptions.
143      *
144      * @param fileName The file name, as should be printed in the opening file tag.
145      * @param messages The file messages.
146      */
147     private void writeFileMessages(String fileName, FileMessages messages) {
148         writeFileOpeningTag(fileName);
149         if (messages != null) {
150             for (AuditEvent errorEvent : messages.getErrors()) {
151                 writeFileError(errorEvent);
152             }
153             for (Throwable exception : messages.getExceptions()) {
154                 writeException(exception);
155             }
156         }
157         writeFileClosingTag();
158     }
159 
160     /**
161      * Prints the "file" opening tag with the given filename.
162      *
163      * @param fileName The filename to output.
164      */
165     private void writeFileOpeningTag(String fileName) {
166         writer.println("<file name=\"" + encode(fileName) + "\">");
167     }
168 
169     /**
170      * Prints the "file" closing tag.
171      */
172     private void writeFileClosingTag() {
173         writer.println("</file>");
174     }
175 
176     @Override
177     public void addError(AuditEvent event) {
178         if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
179             final String fileName = event.getFileName();
180             if (fileName == null || !fileMessages.containsKey(fileName)) {
181                 writeFileError(event);
182             }
183             else {
184                 final FileMessages messages = fileMessages.get(fileName);
185                 messages.addError(event);
186             }
187         }
188     }
189 
190     /**
191      * Outputs the given event to the writer.
192      *
193      * @param event An event to print.
194      */
195     private void writeFileError(AuditEvent event) {
196         writer.print("<error" + " line=\"" + event.getLine() + "\"");
197         if (event.getColumn() > 0) {
198             writer.print(" column=\"" + event.getColumn() + "\"");
199         }
200         writer.print(" severity=\""
201                 + event.getSeverityLevel().getName()
202                 + "\"");
203         writer.print(" message=\""
204                 + encode(event.getMessage())
205                 + "\"");
206         writer.print(" source=\"");
207         if (event.getModuleId() == null) {
208             writer.print(encode(event.getSourceName()));
209         }
210         else {
211             writer.print(encode(event.getModuleId()));
212         }
213         writer.println("\"/>");
214     }
215 
216     @Override
217     public void addException(AuditEvent event, Throwable throwable) {
218         final String fileName = event.getFileName();
219         if (fileName == null || !fileMessages.containsKey(fileName)) {
220             writeException(throwable);
221         }
222         else {
223             final FileMessages messages = fileMessages.get(fileName);
224             messages.addException(throwable);
225         }
226     }
227 
228     /**
229      * Writes the exception event to the print writer.
230      *
231      * @param throwable The
232      */
233     private void writeException(Throwable throwable) {
234         writer.println("<exception>");
235         writer.println("<![CDATA[");
236 
237         final StringWriter stringWriter = new StringWriter();
238         final PrintWriter printer = new PrintWriter(stringWriter);
239         throwable.printStackTrace(printer);
240         writer.println(encode(stringWriter.toString()));
241 
242         writer.println("]]>");
243         writer.println("</exception>");
244     }
245 
246     /**
247      * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
248      *
249      * @param value the value to escape.
250      * @return the escaped value if necessary.
251      */
252     public static String encode(String value) {
253         final StringBuilder sb = new StringBuilder(256);
254         for (int i = 0; i < value.length(); i++) {
255             final char chr = value.charAt(i);
256             switch (chr) {
257                 case '<':
258                     sb.append("&lt;");
259                     break;
260                 case '>':
261                     sb.append("&gt;");
262                     break;
263                 case '\'':
264                     sb.append("&apos;");
265                     break;
266                 case '\"':
267                     sb.append("&quot;");
268                     break;
269                 case '&':
270                     sb.append("&amp;");
271                     break;
272                 case '\r':
273                     break;
274                 case '\n':
275                     sb.append("&#10;");
276                     break;
277                 default:
278                     if (Character.isISOControl(chr)) {
279                         // true escape characters need '&' before, but it also requires XML 1.1
280                         // until https://github.com/checkstyle/checkstyle/issues/5168
281                         sb.append("#x");
282                         sb.append(Integer.toHexString(chr));
283                         sb.append(';');
284                     }
285                     else {
286                         sb.append(chr);
287                     }
288                     break;
289             }
290         }
291         return sb.toString();
292     }
293 
294     /**
295      * Finds whether the given argument is character or entity reference.
296      *
297      * @param ent the possible entity to look for.
298      * @return whether the given argument a character or entity reference
299      */
300     public static boolean isReference(String ent) {
301         boolean reference = false;
302 
303         if (ent.charAt(0) == '&' && ent.endsWith(";")) {
304             if (ent.charAt(1) == '#') {
305                 // prefix is "&#"
306                 int prefixLength = 2;
307 
308                 int radix = BASE_10;
309                 if (ent.charAt(2) == 'x') {
310                     prefixLength++;
311                     radix = BASE_16;
312                 }
313                 try {
314                     Integer.parseInt(
315                         ent.substring(prefixLength, ent.length() - 1), radix);
316                     reference = true;
317                 }
318                 catch (final NumberFormatException ignored) {
319                     reference = false;
320                 }
321             }
322             else {
323                 final String name = ent.substring(1, ent.length() - 1);
324                 for (String element : ENTITIES) {
325                     if (name.equals(element)) {
326                         reference = true;
327                         break;
328                     }
329                 }
330             }
331         }
332 
333         return reference;
334     }
335 
336     /**
337      * The registered file messages.
338      */
339     private static final class FileMessages {
340 
341         /** The file error events. */
342         private final List<AuditEvent> errors = new ArrayList<>();
343 
344         /** The file exceptions. */
345         private final List<Throwable> exceptions = new ArrayList<>();
346 
347         /**
348          * Returns the file error events.
349          *
350          * @return the file error events.
351          */
352         public List<AuditEvent> getErrors() {
353             return Collections.unmodifiableList(errors);
354         }
355 
356         /**
357          * Adds the given error event to the messages.
358          *
359          * @param event the error event.
360          */
361         public void addError(AuditEvent event) {
362             errors.add(event);
363         }
364 
365         /**
366          * Returns the file exceptions.
367          *
368          * @return the file exceptions.
369          */
370         public List<Throwable> getExceptions() {
371             return Collections.unmodifiableList(exceptions);
372         }
373 
374         /**
375          * Adds the given exception to the messages.
376          *
377          * @param throwable the file exception
378          */
379         public void addException(Throwable throwable) {
380             exceptions.add(throwable);
381         }
382 
383     }
384 
385 }