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