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.Locale;
028import java.util.ResourceBundle;
029
030import com.puppycrawl.tools.checkstyle.api.AuditEvent;
031import com.puppycrawl.tools.checkstyle.api.AuditListener;
032import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
033import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
034import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
035
036/**
037 * Simple XML logger.
038 * It outputs everything in UTF-8 (default XML encoding is UTF-8) in case
039 * we want to localize error messages or simply that file names are
040 * localized and takes care about escaping as well.
041
042 * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
043 */
044// -@cs[AbbreviationAsWordInName] We can not change it as,
045// check's name is part of API (used in configurations).
046public class XMLLogger
047    extends AutomaticBean
048    implements AuditListener {
049    /** Decimal radix. */
050    private static final int BASE_10 = 10;
051
052    /** Hex radix. */
053    private static final int BASE_16 = 16;
054
055    /** Some known entities to detect. */
056    private static final String[] ENTITIES = {"gt", "amp", "lt", "apos",
057                                              "quot", };
058
059    /** Close output stream in auditFinished. */
060    private final boolean closeStream;
061
062    /** Helper writer that allows easy encoding and printing. */
063    private PrintWriter writer;
064
065    /**
066     * Creates a new {@code XMLLogger} instance.
067     * Sets the output to a defined stream.
068     * @param outputStream the stream to write logs to.
069     * @param closeStream close oS in auditFinished
070     */
071    public XMLLogger(OutputStream outputStream, boolean closeStream) {
072        setOutputStream(outputStream);
073        this.closeStream = closeStream;
074    }
075
076    /**
077     * Sets the OutputStream.
078     * @param outputStream the OutputStream to use
079     **/
080    private void setOutputStream(OutputStream outputStream) {
081        final OutputStreamWriter osw = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
082        writer = new PrintWriter(osw);
083    }
084
085    @Override
086    public void auditStarted(AuditEvent event) {
087        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
088
089        final ResourceBundle compilationProperties =
090            ResourceBundle.getBundle("checkstylecompilation", Locale.ROOT);
091        final String version =
092            compilationProperties.getString("checkstyle.compile.version");
093
094        writer.println("<checkstyle version=\"" + version + "\">");
095    }
096
097    @Override
098    public void auditFinished(AuditEvent event) {
099        writer.println("</checkstyle>");
100        if (closeStream) {
101            writer.close();
102        }
103        else {
104            writer.flush();
105        }
106    }
107
108    @Override
109    public void fileStarted(AuditEvent event) {
110        writer.println("<file name=\"" + encode(event.getFileName()) + "\">");
111    }
112
113    @Override
114    public void fileFinished(AuditEvent event) {
115        writer.println("</file>");
116    }
117
118    @Override
119    public void addError(AuditEvent event) {
120        if (event.getSeverityLevel() != SeverityLevel.IGNORE) {
121            writer.print("<error" + " line=\"" + event.getLine() + "\"");
122            if (event.getColumn() > 0) {
123                writer.print(" column=\"" + event.getColumn() + "\"");
124            }
125            writer.print(" severity=\""
126                + event.getSeverityLevel().getName()
127                + "\"");
128            writer.print(" message=\""
129                + encode(event.getMessage())
130                + "\"");
131            writer.println(" source=\""
132                + encode(event.getSourceName())
133                + "\"/>");
134        }
135    }
136
137    @Override
138    public void addException(AuditEvent event, Throwable throwable) {
139        final StringWriter stringWriter = new StringWriter();
140        final PrintWriter printer = new PrintWriter(stringWriter);
141        printer.println("<exception>");
142        printer.println("<![CDATA[");
143        throwable.printStackTrace(printer);
144        printer.println("]]>");
145        printer.println("</exception>");
146        printer.flush();
147        writer.println(encode(stringWriter.toString()));
148    }
149
150    /**
151     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
152     * @param value the value to escape.
153     * @return the escaped value if necessary.
154     */
155    public static String encode(String value) {
156        final StringBuilder sb = new StringBuilder();
157        for (int i = 0; i < value.length(); i++) {
158            final char chr = value.charAt(i);
159            switch (chr) {
160                case '<':
161                    sb.append("&lt;");
162                    break;
163                case '>':
164                    sb.append("&gt;");
165                    break;
166                case '\'':
167                    sb.append("&apos;");
168                    break;
169                case '\"':
170                    sb.append("&quot;");
171                    break;
172                case '&':
173                    sb.append(encodeAmpersand(value, i));
174                    break;
175                case '\r':
176                    break;
177                case '\n':
178                    sb.append("&#10;");
179                    break;
180                default:
181                    sb.append(chr);
182                    break;
183            }
184        }
185        return sb.toString();
186    }
187
188    /**
189     * Finds whether the given argument is character or entity reference.
190     * @param ent the possible entity to look for.
191     * @return whether the given argument a character or entity reference
192     */
193    public static boolean isReference(String ent) {
194        boolean reference = false;
195
196        if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
197            reference = false;
198        }
199        else if (ent.charAt(1) == '#') {
200            // prefix is "&#"
201            int prefixLength = 2;
202
203            int radix = BASE_10;
204            if (ent.charAt(2) == 'x') {
205                prefixLength++;
206                radix = BASE_16;
207            }
208            try {
209                Integer.parseInt(
210                    ent.substring(prefixLength, ent.length() - 1), radix);
211                reference = true;
212            }
213            catch (final NumberFormatException ignored) {
214                reference = false;
215            }
216        }
217        else {
218            final String name = ent.substring(1, ent.length() - 1);
219            for (String element : ENTITIES) {
220                if (name.equals(element)) {
221                    reference = true;
222                    break;
223                }
224            }
225        }
226        return reference;
227    }
228
229    /**
230     * Encodes ampersand in value at required position.
231     * @param value string value, which contains ampersand
232     * @param ampPosition position of ampersand in value
233     * @return encoded ampersand which should be used in xml
234     */
235    private static String encodeAmpersand(String value, int ampPosition) {
236        final int nextSemi = value.indexOf(';', ampPosition);
237        final String result;
238        if (nextSemi < 0
239            || !isReference(value.substring(ampPosition, nextSemi + 1))) {
240            result = "&amp;";
241        }
242        else {
243            result = "&";
244        }
245        return result;
246    }
247}