001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.Locale;
031import java.util.Map;
032import java.util.ResourceBundle;
033import java.util.concurrent.ConcurrentHashMap;
034
035import com.puppycrawl.tools.checkstyle.api.AuditEvent;
036import com.puppycrawl.tools.checkstyle.api.AuditListener;
037import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
038import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
039import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
041
042/**
043 * Simple XML logger.
044 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
045 * we want to localize error messages or simply that file names are
046 * localized and takes care about escaping as well.
047
048 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
049 */
050// -@cs[AbbreviationAsWordInName] We can not change it as,
051// check's name is part of API (used in configurations).
052public class XMLLogger
053    extends AutomaticBean
054    implements AuditListener {
055
056    /** Decimal radix. */
057    private static final int BASE_10 = 10;
058
059    /** Hex radix. */
060    private static final int BASE_16 = 16;
061
062    /** Some known entities to detect. */
063    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
064                                              "quot", };
065
066    /** Close output stream in auditFinished. */
067    private final boolean closeStream;
068
069    /** The writer lock object. */
070    private final Object writerLock = new Object();
071
072    /** Holds all messages for the given file. */
073    private final Map<String, FileMessages> fileMessages =
074            new ConcurrentHashMap<>();
075
076    /**
077     * Helper writer that allows easy encoding and printing.
078     */
079    private final PrintWriter writer;
080
081    /**
082     * Creates a new {@code XMLLogger} instance.
083     * Sets the output to a defined stream.
084     * @param outputStream the stream to write logs to.
085     * @param closeStream close oS in auditFinished
086     * @deprecated in order to fulfill demands of BooleanParameter IDEA check.
087     * @noinspection BooleanParameter
088     */
089    @Deprecated
090    public XMLLogger(OutputStream outputStream, boolean closeStream) {
091        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
092        this.closeStream = closeStream;
093    }
094
095    /**
096     * Creates a new {@code XMLLogger} instance.
097     * Sets the output to a defined stream.
098     * @param outputStream the stream to write logs to.
099     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
100     */
101    public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
102        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
103        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
104    }
105
106    @Override
107    protected void finishLocalSetup() throws CheckstyleException {
108        // No code by default
109    }
110
111    @Override
112    public void auditStarted(AuditEvent event) {
113        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
114
115        final ResourceBundle compilationProperties =
116            ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
117        final String version =
118            compilationProperties.getString("checkstyle.compile.version");
119
120        writer.println("<checkstyle version=\"" + version + "\">");
121    }
122
123    @Override
124    public void auditFinished(AuditEvent event) {
125        writer.println("</checkstyle>");
126        if (closeStream) {
127            writer.close();
128        }
129        else {
130            writer.flush();
131        }
132    }
133
134    @Override
135    public void fileStarted(AuditEvent event) {
136        fileMessages.put(event.getFileName(), new FileMessages());
137    }
138
139    @Override
140    public void fileFinished(AuditEvent event) {
141        final String fileName = event.getFileName();
142        final FileMessages messages = fileMessages.get(fileName);
143
144        synchronized (writerLock) {
145            writeFileMessages(fileName, messages);
146        }
147
148        fileMessages.remove(fileName);
149    }
150
151    /**
152     * Prints the file section with all file errors and exceptions.
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     * @param fileName The filename to output.
172     */
173    private void writeFileOpeningTag(String fileName) {
174        writer.println("<file name=\"" + encode(fileName) + "\">");
175    }
176
177    /**
178     * Prints the "file" closing tag.
179     */
180    private void writeFileClosingTag() {
181        writer.println("</file>");
182    }
183
184    @Override
185    public void addError(AuditEvent event) {
186        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
187            final String fileName = event.getFileName();
188            if (fileName == null || !fileMessages.containsKey(fileName)) {
189                synchronized (writerLock) {
190                    writeFileError(event);
191                }
192            }
193            else {
194                final FileMessages messages = fileMessages.get(fileName);
195                messages.addError(event);
196            }
197        }
198    }
199
200    /**
201     * Outputs the given event to the writer.
202     * @param event An event to print.
203     */
204    private void writeFileError(AuditEvent event) {
205        writer.print("<error" + " line=\"" + event.getLine() + "\"");
206        if (event.getColumn() > 0) {
207            writer.print(" column=\"" + event.getColumn() + "\"");
208        }
209        writer.print(" severity=\""
210                + event.getSeverityLevel().getName()
211                + "\"");
212        writer.print(" message=\""
213                + encode(event.getMessage())
214                + "\"");
215        writer.print(" source=\"");
216        if (event.getModuleId() == null) {
217            writer.print(encode(event.getSourceName()));
218        }
219        else {
220            writer.print(encode(event.getModuleId()));
221        }
222        writer.println("\"/>");
223    }
224
225    @Override
226    public void addException(AuditEvent event, Throwable throwable) {
227        final String fileName = event.getFileName();
228        if (fileName == null || !fileMessages.containsKey(fileName)) {
229            synchronized (writerLock) {
230                writeException(throwable);
231            }
232        }
233        else {
234            final FileMessages messages = fileMessages.get(fileName);
235            messages.addException(throwable);
236        }
237    }
238
239    /**
240     * Writes the exception event to the print writer.
241     * @param throwable The
242     */
243    private void writeException(Throwable throwable) {
244        writer.println("<exception>");
245        writer.println("<![CDATA[");
246
247        final StringWriter stringWriter = new StringWriter();
248        final PrintWriter printer = new PrintWriter(stringWriter);
249        throwable.printStackTrace(printer);
250        writer.println(encode(stringWriter.toString()));
251
252        writer.println("]]>");
253        writer.println("</exception>");
254    }
255
256    /**
257     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
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     * @param ent the possible entity to look for.
306     * @return whether the given argument a character or entity reference
307     */
308    public static boolean isReference(String ent) {
309        boolean reference = false;
310
311        if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
312            reference = false;
313        }
314        else if (ent.charAt(1) == '#') {
315            // prefix is "&#"
316            int prefixLength = 2;
317
318            int radix = BASE_10;
319            if (ent.charAt(2) == 'x') {
320                prefixLength++;
321                radix = BASE_16;
322            }
323            try {
324                Integer.parseInt(
325                    ent.substring(prefixLength, ent.length() - 1), radix);
326                reference = true;
327            }
328            catch (final NumberFormatException ignored) {
329                reference = false;
330            }
331        }
332        else {
333            final String name = ent.substring(1, ent.length() - 1);
334            for (String element : ENTITIES) {
335                if (name.equals(element)) {
336                    reference = true;
337                    break;
338                }
339            }
340        }
341        return reference;
342    }
343
344    /**
345     * The registered file messages.
346     */
347    private static class FileMessages {
348
349        /** The file error events. */
350        private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
351
352        /** The file exceptions. */
353        private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
354
355        /**
356         * Returns the file error events.
357         * @return the file error events.
358         */
359        public List<AuditEvent> getErrors() {
360            return Collections.unmodifiableList(errors);
361        }
362
363        /**
364         * Adds the given error event to the messages.
365         * @param event the error event.
366         */
367        public void addError(AuditEvent event) {
368            errors.add(event);
369        }
370
371        /**
372         * Returns the file exceptions.
373         * @return the file exceptions.
374         */
375        public List<Throwable> getExceptions() {
376            return Collections.unmodifiableList(exceptions);
377        }
378
379        /**
380         * Adds the given exception to the messages.
381         * @param throwable the file exception
382         */
383        public void addException(Throwable throwable) {
384            exceptions.add(throwable);
385        }
386
387    }
388
389}