001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.nio.charset.StandardCharsets;
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031import java.util.concurrent.ConcurrentHashMap;
032
033import com.puppycrawl.tools.checkstyle.api.AuditEvent;
034import com.puppycrawl.tools.checkstyle.api.AuditListener;
035import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
036import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
038
039/**
040 * Simple XML logger.
041 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
042 * we want to localize error messages or simply that file names are
043 * localized and takes care about escaping as well.
044 */
045// -@cs[AbbreviationAsWordInName] We can not change it as,
046// check's name is part of API (used in configurations).
047public class XMLLogger
048    extends AbstractAutomaticBean
049    implements AuditListener {
050
051    /** Decimal radix. */
052    private static final int BASE_10 = 10;
053
054    /** Hex radix. */
055    private static final int BASE_16 = 16;
056
057    /** Some known entities to detect. */
058    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
059                                              "quot", };
060
061    /** Close output stream in auditFinished. */
062    private final boolean closeStream;
063
064    /** The writer lock object. */
065    private final Object writerLock = new Object();
066
067    /** Holds all messages for the given file. */
068    private final Map<String, FileMessages> fileMessages =
069            new ConcurrentHashMap<>();
070
071    /**
072     * Helper writer that allows easy encoding and printing.
073     */
074    private final PrintWriter writer;
075
076    /**
077     * Creates a new {@code XMLLogger} instance.
078     * Sets the output to a defined stream.
079     *
080     * @param outputStream the stream to write logs to.
081     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
082     * @throws IllegalArgumentException if outputStreamOptions is null.
083     * @noinspection deprecation
084     * @noinspectionreason We are forced to keep AutomaticBean compatability
085     *     because of maven-checkstyle-plugin. Until #12873.
086     */
087    public XMLLogger(OutputStream outputStream,
088                     AutomaticBean.OutputStreamOptions outputStreamOptions) {
089        this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
090    }
091
092    /**
093     * Creates a new {@code XMLLogger} instance.
094     * Sets the output to a defined stream.
095     *
096     * @param outputStream the stream to write logs to.
097     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
098     * @throws IllegalArgumentException if outputStreamOptions is null.
099     */
100    public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
101        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
102        if (outputStreamOptions == null) {
103            throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
104        }
105        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
106    }
107
108    @Override
109    protected void finishLocalSetup() {
110        // No code by default
111    }
112
113    @Override
114    public void auditStarted(AuditEvent event) {
115        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
116
117        final String version = XMLLogger.class.getPackage().getImplementationVersion();
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     *
153     * @param fileName The file name, as should be printed in the opening file tag.
154     * @param messages The file messages.
155     */
156    private void writeFileMessages(String fileName, FileMessages messages) {
157        writeFileOpeningTag(fileName);
158        if (messages != null) {
159            for (AuditEvent errorEvent : messages.getErrors()) {
160                writeFileError(errorEvent);
161            }
162            for (Throwable exception : messages.getExceptions()) {
163                writeException(exception);
164            }
165        }
166        writeFileClosingTag();
167    }
168
169    /**
170     * Prints the "file" opening tag with the given filename.
171     *
172     * @param fileName The filename to output.
173     */
174    private void writeFileOpeningTag(String fileName) {
175        writer.println("<file name=\"" + encode(fileName) + "\">");
176    }
177
178    /**
179     * Prints the "file" closing tag.
180     */
181    private void writeFileClosingTag() {
182        writer.println("</file>");
183    }
184
185    @Override
186    public void addError(AuditEvent event) {
187        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
188            final String fileName = event.getFileName();
189            if (fileName == null || !fileMessages.containsKey(fileName)) {
190                synchronized (writerLock) {
191                    writeFileError(event);
192                }
193            }
194            else {
195                final FileMessages messages = fileMessages.get(fileName);
196                messages.addError(event);
197            }
198        }
199    }
200
201    /**
202     * Outputs the given event to the writer.
203     *
204     * @param event An event to print.
205     */
206    private void writeFileError(AuditEvent event) {
207        writer.print("<error" + " line=\"" + event.getLine() + "\"");
208        if (event.getColumn() > 0) {
209            writer.print(" column=\"" + event.getColumn() + "\"");
210        }
211        writer.print(" severity=\""
212                + event.getSeverityLevel().getName()
213                + "\"");
214        writer.print(" message=\""
215                + encode(event.getMessage())
216                + "\"");
217        writer.print(" source=\"");
218        if (event.getModuleId() == null) {
219            writer.print(encode(event.getSourceName()));
220        }
221        else {
222            writer.print(encode(event.getModuleId()));
223        }
224        writer.println("\"/>");
225    }
226
227    @Override
228    public void addException(AuditEvent event, Throwable throwable) {
229        final String fileName = event.getFileName();
230        if (fileName == null || !fileMessages.containsKey(fileName)) {
231            synchronized (writerLock) {
232                writeException(throwable);
233            }
234        }
235        else {
236            final FileMessages messages = fileMessages.get(fileName);
237            messages.addException(throwable);
238        }
239    }
240
241    /**
242     * Writes the exception event to the print writer.
243     *
244     * @param throwable The
245     */
246    private void writeException(Throwable throwable) {
247        writer.println("<exception>");
248        writer.println("<![CDATA[");
249
250        final StringWriter stringWriter = new StringWriter();
251        final PrintWriter printer = new PrintWriter(stringWriter);
252        throwable.printStackTrace(printer);
253        writer.println(encode(stringWriter.toString()));
254
255        writer.println("]]>");
256        writer.println("</exception>");
257    }
258
259    /**
260     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
261     *
262     * @param value the value to escape.
263     * @return the escaped value if necessary.
264     */
265    public static String encode(String value) {
266        final StringBuilder sb = new StringBuilder(256);
267        for (int i = 0; i < value.length(); i++) {
268            final char chr = value.charAt(i);
269            switch (chr) {
270                case '<':
271                    sb.append("&lt;");
272                    break;
273                case '>':
274                    sb.append("&gt;");
275                    break;
276                case '\'':
277                    sb.append("&apos;");
278                    break;
279                case '\"':
280                    sb.append("&quot;");
281                    break;
282                case '&':
283                    sb.append("&amp;");
284                    break;
285                case '\r':
286                    break;
287                case '\n':
288                    sb.append("&#10;");
289                    break;
290                default:
291                    if (Character.isISOControl(chr)) {
292                        // true escape characters need '&' before, but it also requires XML 1.1
293                        // until https://github.com/checkstyle/checkstyle/issues/5168
294                        sb.append("#x");
295                        sb.append(Integer.toHexString(chr));
296                        sb.append(';');
297                    }
298                    else {
299                        sb.append(chr);
300                    }
301                    break;
302            }
303        }
304        return sb.toString();
305    }
306
307    /**
308     * Finds whether the given argument is character or entity reference.
309     *
310     * @param ent the possible entity to look for.
311     * @return whether the given argument a character or entity reference
312     */
313    public static boolean isReference(String ent) {
314        boolean reference = false;
315
316        if (ent.charAt(0) == '&' && CommonUtil.endsWithChar(ent, ';')) {
317            if (ent.charAt(1) == '#') {
318                // prefix is "&#"
319                int prefixLength = 2;
320
321                int radix = BASE_10;
322                if (ent.charAt(2) == 'x') {
323                    prefixLength++;
324                    radix = BASE_16;
325                }
326                try {
327                    Integer.parseInt(
328                        ent.substring(prefixLength, ent.length() - 1), radix);
329                    reference = true;
330                }
331                catch (final NumberFormatException ignored) {
332                    reference = false;
333                }
334            }
335            else {
336                final String name = ent.substring(1, ent.length() - 1);
337                for (String element : ENTITIES) {
338                    if (name.equals(element)) {
339                        reference = true;
340                        break;
341                    }
342                }
343            }
344        }
345
346        return reference;
347    }
348
349    /**
350     * The registered file messages.
351     */
352    private static final class FileMessages {
353
354        /** The file error events. */
355        private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
356
357        /** The file exceptions. */
358        private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
359
360        /**
361         * Returns the file error events.
362         *
363         * @return the file error events.
364         */
365        public List<AuditEvent> getErrors() {
366            return Collections.unmodifiableList(errors);
367        }
368
369        /**
370         * Adds the given error event to the messages.
371         *
372         * @param event the error event.
373         */
374        public void addError(AuditEvent event) {
375            errors.add(event);
376        }
377
378        /**
379         * Returns the file exceptions.
380         *
381         * @return the file exceptions.
382         */
383        public List<Throwable> getExceptions() {
384            return Collections.unmodifiableList(exceptions);
385        }
386
387        /**
388         * Adds the given exception to the messages.
389         *
390         * @param throwable the file exception
391         */
392        public void addException(Throwable throwable) {
393            exceptions.add(throwable);
394        }
395
396    }
397
398}