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