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