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.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 */
046// -@cs[AbbreviationAsWordInName] We can not change it as,
047// check's name is part of API (used in configurations).
048public class XMLLogger
049    extends AutomaticBean
050    implements AuditListener {
051
052    /** Decimal radix. */
053    private static final int BASE_10 = 10;
054
055    /** Hex radix. */
056    private static final int BASE_16 = 16;
057
058    /** Some known entities to detect. */
059    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
060                                              "quot", };
061
062    /** Close output stream in auditFinished. */
063    private final boolean closeStream;
064
065    /** The writer lock object. */
066    private final Object writerLock = new Object();
067
068    /** Holds all messages for the given file. */
069    private final Map<String, FileMessages> fileMessages =
070            new ConcurrentHashMap<>();
071
072    /**
073     * Helper writer that allows easy encoding and printing.
074     */
075    private final PrintWriter writer;
076
077    /**
078     * Creates a new {@code XMLLogger} instance.
079     * Sets the output to a defined stream.
080     * @param outputStream the stream to write logs to.
081     * @param closeStream close oS in auditFinished
082     * @deprecated in order to fulfill demands of BooleanParameter IDEA check.
083     * @noinspection BooleanParameter
084     */
085    @Deprecated
086    public XMLLogger(OutputStream outputStream, boolean closeStream) {
087        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
088        this.closeStream = closeStream;
089    }
090
091    /**
092     * Creates a new {@code XMLLogger} instance.
093     * Sets the output to a defined stream.
094     * @param outputStream the stream to write logs to.
095     * @param outputStreamOptions if {@code CLOSE} stream should be closed in auditFinished()
096     */
097    public XMLLogger(OutputStream outputStream, OutputStreamOptions outputStreamOptions) {
098        writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
099        if (outputStreamOptions == null) {
100            throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
101        }
102        closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
103    }
104
105    @Override
106    protected void finishLocalSetup() {
107        // No code by default
108    }
109
110    @Override
111    public void auditStarted(AuditEvent event) {
112        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
113
114        final String version = XMLLogger.class.getPackage().getImplementationVersion();
115
116        writer.println("<checkstyle version=\"" + version + "\">");
117    }
118
119    @Override
120    public void auditFinished(AuditEvent event) {
121        writer.println("</checkstyle>");
122        if (closeStream) {
123            writer.close();
124        }
125        else {
126            writer.flush();
127        }
128    }
129
130    @Override
131    public void fileStarted(AuditEvent event) {
132        fileMessages.put(event.getFileName(), new FileMessages());
133    }
134
135    @Override
136    public void fileFinished(AuditEvent event) {
137        final String fileName = event.getFileName();
138        final FileMessages messages = fileMessages.get(fileName);
139
140        synchronized (writerLock) {
141            writeFileMessages(fileName, messages);
142        }
143
144        fileMessages.remove(fileName);
145    }
146
147    /**
148     * Prints the file section with all file errors and exceptions.
149     * @param fileName The file name, as should be printed in the opening file tag.
150     * @param messages The file messages.
151     */
152    private void writeFileMessages(String fileName, FileMessages messages) {
153        writeFileOpeningTag(fileName);
154        if (messages != null) {
155            for (AuditEvent errorEvent : messages.getErrors()) {
156                writeFileError(errorEvent);
157            }
158            for (Throwable exception : messages.getExceptions()) {
159                writeException(exception);
160            }
161        }
162        writeFileClosingTag();
163    }
164
165    /**
166     * Prints the "file" opening tag with the given filename.
167     * @param fileName The filename to output.
168     */
169    private void writeFileOpeningTag(String fileName) {
170        writer.println("<file name=\"" + encode(fileName) + "\">");
171    }
172
173    /**
174     * Prints the "file" closing tag.
175     */
176    private void writeFileClosingTag() {
177        writer.println("</file>");
178    }
179
180    @Override
181    public void addError(AuditEvent event) {
182        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
183            final String fileName = event.getFileName();
184            if (fileName == null || !fileMessages.containsKey(fileName)) {
185                synchronized (writerLock) {
186                    writeFileError(event);
187                }
188            }
189            else {
190                final FileMessages messages = fileMessages.get(fileName);
191                messages.addError(event);
192            }
193        }
194    }
195
196    /**
197     * Outputs the given event 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 || !fileMessages.containsKey(fileName)) {
225            synchronized (writerLock) {
226                writeException(throwable);
227            }
228        }
229        else {
230            final FileMessages messages = fileMessages.get(fileName);
231            messages.addException(throwable);
232        }
233    }
234
235    /**
236     * Writes the exception event to the print writer.
237     * @param throwable The
238     */
239    private void writeException(Throwable throwable) {
240        writer.println("<exception>");
241        writer.println("<![CDATA[");
242
243        final StringWriter stringWriter = new StringWriter();
244        final PrintWriter printer = new PrintWriter(stringWriter);
245        throwable.printStackTrace(printer);
246        writer.println(encode(stringWriter.toString()));
247
248        writer.println("]]>");
249        writer.println("</exception>");
250    }
251
252    /**
253     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
254     * @param value the value to escape.
255     * @return the escaped value if necessary.
256     */
257    public static String encode(String value) {
258        final StringBuilder sb = new StringBuilder(256);
259        for (int i = 0; i < value.length(); i++) {
260            final char chr = value.charAt(i);
261            switch (chr) {
262                case '<':
263                    sb.append("&lt;");
264                    break;
265                case '>':
266                    sb.append("&gt;");
267                    break;
268                case '\'':
269                    sb.append("&apos;");
270                    break;
271                case '\"':
272                    sb.append("&quot;");
273                    break;
274                case '&':
275                    sb.append("&amp;");
276                    break;
277                case '\r':
278                    break;
279                case '\n':
280                    sb.append("&#10;");
281                    break;
282                default:
283                    if (Character.isISOControl(chr)) {
284                        // true escape characters need '&' before but it also requires XML 1.1
285                        // until https://github.com/checkstyle/checkstyle/issues/5168
286                        sb.append("#x");
287                        sb.append(Integer.toHexString(chr));
288                        sb.append(';');
289                    }
290                    else {
291                        sb.append(chr);
292                    }
293                    break;
294            }
295        }
296        return sb.toString();
297    }
298
299    /**
300     * Finds whether the given argument is character or entity reference.
301     * @param ent the possible entity to look for.
302     * @return whether the given argument a character or entity reference
303     */
304    public static boolean isReference(String ent) {
305        boolean reference = false;
306
307        if (ent.charAt(0) != '&' || !CommonUtil.endsWithChar(ent, ';')) {
308            reference = false;
309        }
310        else if (ent.charAt(1) == '#') {
311            // prefix is "&#"
312            int prefixLength = 2;
313
314            int radix = BASE_10;
315            if (ent.charAt(2) == 'x') {
316                prefixLength++;
317                radix = BASE_16;
318            }
319            try {
320                Integer.parseInt(
321                    ent.substring(prefixLength, ent.length() - 1), radix);
322                reference = true;
323            }
324            catch (final NumberFormatException ignored) {
325                reference = false;
326            }
327        }
328        else {
329            final String name = ent.substring(1, ent.length() - 1);
330            for (String element : ENTITIES) {
331                if (name.equals(element)) {
332                    reference = true;
333                    break;
334                }
335            }
336        }
337        return reference;
338    }
339
340    /**
341     * The registered file messages.
342     */
343    private static class FileMessages {
344
345        /** The file error events. */
346        private final List<AuditEvent> errors = Collections.synchronizedList(new ArrayList<>());
347
348        /** The file exceptions. */
349        private final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
350
351        /**
352         * Returns the file error events.
353         * @return the file error events.
354         */
355        public List<AuditEvent> getErrors() {
356            return Collections.unmodifiableList(errors);
357        }
358
359        /**
360         * Adds the given error event to the messages.
361         * @param event the error event.
362         */
363        public void addError(AuditEvent event) {
364            errors.add(event);
365        }
366
367        /**
368         * Returns the file exceptions.
369         * @return the file exceptions.
370         */
371        public List<Throwable> getExceptions() {
372            return Collections.unmodifiableList(exceptions);
373        }
374
375        /**
376         * Adds the given exception to the messages.
377         * @param throwable the file exception
378         */
379        public void addException(Throwable throwable) {
380            exceptions.add(throwable);
381        }
382
383    }
384
385}