001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.SeverityLevel;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
040
041/**
042 * Simple XML logger.
043 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
044 * we want to localize error messages or simply that file names are
045 * localized and takes care about escaping as well.
046
047 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
048 */
049// -@cs[AbbreviationAsWordInName] We can not change it as,
050// check's name is part of API (used in configurations).
051public class XMLLogger
052    extends AutomaticBean
053    implements AuditListener {
054    /** Decimal radix. */
055    private static final int BASE_10 = 10;
056
057    /** Hex radix. */
058    private static final int BASE_16 = 16;
059
060    /** Some known entities to detect. */
061    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
062                                              "quot", };
063
064    /** Close output stream in auditFinished. */
065    private final boolean closeStream;
066
067    /** The writer lock object. */
068    private final Object writerLock = new Object();
069
070    /** Holds all messages for the given file. */
071    private final Map<String, FileMessages> fileMessages =
072            new ConcurrentHashMap<>();
073
074    /**
075     * Helper writer that allows easy encoding and printing.
076     */
077    private final PrintWriter writer;
078
079    /**
080     * Creates a new {@code XMLLogger} instance.
081     * Sets the output to a defined stream.
082     * @param outputStream the stream to write logs to.
083     * @param closeStream close oS in auditFinished
084     * @deprecated in order to fullfil demands of BooleanParameter IDEA check.
085     * @noinspection BooleanParameter
086     */
087    @Deprecated
088    public XMLLogger(OutputStream outputStream, boolean closeStream) {
089        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
090        this.closeStream = closeStream;
091    }
092
093    /**
094     * Creates a new {@code XMLLogger} instance.
095     * Sets the output to a defined stream.
096     * @param outputStream the stream to write logs to.
097     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
098     */
099    public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
100        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
101        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
102    }
103
104    @Override
105    public void auditStarted(AuditEvent event) {
106        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
107
108        final ResourceBundle compilationProperties =
109            ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
110        final String version =
111            compilationProperties.getString("checkstyle.compile.version");
112
113        writer.println("<checkstyle version=\"" + version + "\">");
114    }
115
116    @Override
117    public void auditFinished(AuditEvent event) {
118        fileMessages.forEach(this::writeFileMessages);
119
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.get(fileName);
138
139        synchronized (writerLock) {
140            writeFileMessages(fileName, messages);
141        }
142
143        fileMessages.remove(fileName);
144    }
145
146    /**
147     * Prints the file section with all file errors and exceptions.
148     * @param fileName The file name, as should be printed in the opening file tag.
149     * @param messages The file messages.
150     */
151    private void writeFileMessages(String fileName, FileMessages messages) {
152        writeFileOpeningTag(fileName);
153        if (messages != null) {
154            for (AuditEvent errorEvent : messages.getErrors()) {
155                writeFileError(errorEvent);
156            }
157            for (Throwable exception : messages.getExceptions()) {
158                writeException(exception);
159            }
160        }
161        writeFileClosingTag();
162    }
163
164    /**
165     * Prints the "file" opening tag with the given filename.
166     * @param fileName The filename to output.
167     */
168    private void writeFileOpeningTag(String fileName) {
169        writer.println("<file name=\"" + encode(fileName) + "\">");
170    }
171
172    /**
173     * Prints the "file" closing tag.
174     */
175    private void writeFileClosingTag() {
176        writer.println("</file>");
177    }
178
179    @Override
180    public void addError(AuditEvent event) {
181        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
182            final String fileName = event.getFileName();
183            if (fileName == null) {
184                synchronized (writerLock) {
185                    writeFileError(event);
186                }
187            }
188            else {
189                final FileMessages messages = fileMessages.computeIfAbsent(
190                        fileName, name -> new FileMessages());
191                messages.addError(event);
192            }
193        }
194    }
195
196    /**
197     * Outputs the given envet to the writer.
198     * @param event An event to print.
199     */
200    private void writeFileError(AuditEvent event) {
201        writer.print("<error" + " line=\"" + event.getLine() + "\"");
202        if (event.getColumn() > 0) {
203            writer.print(" column=\"" + event.getColumn() + "\"");
204        }
205        writer.print(" severity=\""
206                + event.getSeverityLevel().getName()
207                + "\"");
208        writer.print(" message=\""
209                + encode(event.getMessage())
210                + "\"");
211        writer.print(" source=\"");
212        if (event.getModuleId() == null) {
213            writer.print(encode(event.getSourceName()));
214        }
215        else {
216            writer.print(encode(event.getModuleId()));
217        }
218        writer.println("\"/>");
219    }
220
221    @Override
222    public void addException(AuditEvent event, Throwable throwable) {
223        final String fileName = event.getFileName();
224        if (fileName == null) {
225            synchronized (writerLock) {
226                writeException(throwable);
227            }
228        }
229        else {
230            final FileMessages messages = fileMessages.computeIfAbsent(
231                    fileName, name -> new FileMessages());
232            messages.addException(throwable);
233        }
234    }
235
236    /**
237     * Writes the exception event to the print writer.
238     * @param throwable The
239     */
240    private void writeException(Throwable throwable) {
241        final StringWriter stringWriter = new StringWriter();
242        final PrintWriter printer = new PrintWriter(stringWriter);
243        printer.println("<exception>");
244        printer.println("<![CDATA[");
245        throwable.printStackTrace(printer);
246        printer.println("]]>");
247        printer.println("</exception>");
248        writer.println(encode(stringWriter.toString()));
249    }
250
251    /**
252     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
253     * @param value the value to escape.
254     * @return the escaped value if necessary.
255     */
256    public static String encode(String value) {
257        final StringBuilder sb = new StringBuilder(256);
258        for (int i = 0; i < value.length(); i++) {
259            final char chr = value.charAt(i);
260            switch (chr) {
261                case '<':
262                    sb.append("&lt;");
263                    break;
264                case '>':
265                    sb.append("&gt;");
266                    break;
267                case '\'':
268                    sb.append("&apos;");
269                    break;
270                case '\"':
271                    sb.append("&quot;");
272                    break;
273                case '&':
274                    sb.append("&amp;");
275                    break;
276                case '\r':
277                    break;
278                case '\n':
279                    sb.append("&#10;");
280                    break;
281                default:
282                    if (Character.isISOControl(chr)) {
283                        // true escape characters need '&' before but it also requires XML 1.1
284                        // until https://github.com/checkstyle/checkstyle/issues/5168
285                        sb.append("#x");
286                        sb.append(Integer.toHexString(chr));
287                        sb.append(';');
288                    }
289                    else {
290                        sb.append(chr);
291                    }
292                    break;
293            }
294        }
295        return sb.toString();
296    }
297
298    /**
299     * Finds whether the given argument is character or entity reference.
300     * @param ent the possible entity to look for.
301     * @return whether the given argument a character or entity reference
302     */
303    public static boolean isReference(String ent) {
304        boolean reference = false;
305
306        if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
307            reference = false;
308        }
309        else if (ent.charAt(1) == '#') {
310            // prefix is "&#"
311            int prefixLength = 2;
312
313            int radix = BASE_10;
314            if (ent.charAt(2) == 'x') {
315                prefixLength++;
316                radix = BASE_16;
317            }
318            try {
319                Integer.parseInt(
320                    ent.substring(prefixLength, ent.length() - 1), radix);
321                reference = true;
322            }
323            catch (final NumberFormatException ignored) {
324                reference = false;
325            }
326        }
327        else {
328            final String name = ent.substring(1, ent.length() - 1);
329            for (String element : ENTITIES) {
330                if (name.equals(element)) {
331                    reference = true;
332                    break;
333                }
334            }
335        }
336        return reference;
337    }
338
339    /**
340     * The registered file messages.
341     */
342    private static class FileMessages {
343        /** The file error events. */
344        private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
345
346        /** The file exceptions. */
347        private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
348
349        /**
350         * Returns the file error events.
351         * @return the file error events.
352         */
353        public List<AuditEvent> getErrors() {
354            return Collections.unmodifiableList(errors);
355        }
356
357        /**
358         * Adds the given error event to the messages.
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         * @return the file exceptions.
368         */
369        public List<Throwable> getExceptions() {
370            return Collections.unmodifiableList(exceptions);
371        }
372
373        /**
374         * Adds the given exception to the messages.
375         * @param throwable the file exception
376         */
377        public void addException(Throwable throwable) {
378            exceptions.add(throwable);
379        }
380    }
381}