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