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 os the stream to write logs to.
066     * @param closeStream close oS in auditFinished
067     */
068    public XMLLogger(OutputStream os, boolean closeStream) {
069        setOutputStream(os);
070        this.closeStream = closeStream;
071    }
072
073    /**
074     * Sets the OutputStream.
075     * @param oS the OutputStream to use
076     **/
077    private void setOutputStream(OutputStream oS) {
078        final OutputStreamWriter osw = new OutputStreamWriter(oS, StandardCharsets.UTF_8);
079        writer = new PrintWriter(osw);
080    }
081
082    @Override
083    public void auditStarted(AuditEvent evt) {
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 evt) {
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 evt) {
107        writer.println("<file name=\"" + encode(evt.getFileName()) + "\">");
108    }
109
110    @Override
111    public void fileFinished(AuditEvent evt) {
112        writer.println("</file>");
113    }
114
115    @Override
116    public void addError(AuditEvent evt) {
117        if (evt.getSeverityLevel() != SeverityLevel.IGNORE) {
118            writer.print("<error" + " line=\"" + evt.getLine() + "\"");
119            if (evt.getColumn() > 0) {
120                writer.print(" column=\"" + evt.getColumn() + "\"");
121            }
122            writer.print(" severity=\""
123                + evt.getSeverityLevel().getName()
124                + "\"");
125            writer.print(" message=\""
126                + encode(evt.getMessage())
127                + "\"");
128            writer.println(" source=\""
129                + encode(evt.getSourceName())
130                + "\"/>");
131        }
132    }
133
134    @Override
135    public void addException(AuditEvent evt, Throwable throwable) {
136        final StringWriter sw = new StringWriter();
137        final PrintWriter pw = new PrintWriter(sw);
138        pw.println("<exception>");
139        pw.println("<![CDATA[");
140        throwable.printStackTrace(pw);
141        pw.println("]]>");
142        pw.println("</exception>");
143        pw.flush();
144        writer.println(encode(sw.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 c = value.charAt(i);
156            switch (c) {
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                    final int nextSemi = value.indexOf(';', i);
171                    if (nextSemi < 0
172                        || !isReference(value.substring(i, nextSemi + 1))) {
173                        sb.append("&amp;");
174                    }
175                    else {
176                        sb.append('&');
177                    }
178                    break;
179                default:
180                    sb.append(c);
181                    break;
182            }
183        }
184        return sb.toString();
185    }
186
187    /**
188     * @param ent the possible entity to look for.
189     * @return whether the given argument a character or entity reference
190     */
191    public static boolean isReference(String ent) {
192        boolean reference = false;
193
194        if (ent.charAt(0) != '&' || !CommonUtils.endsWithChar(ent, ';')) {
195            reference = false;
196        }
197        else if (ent.charAt(1) == '#') {
198            // prefix is "&#"
199            int prefixLength = 2;
200
201            int radix = BASE_10;
202            if (ent.charAt(2) == 'x') {
203                prefixLength++;
204                radix = BASE_16;
205            }
206            try {
207                Integer.parseInt(
208                    ent.substring(prefixLength, ent.length() - 1), radix);
209                reference = true;
210            }
211            catch (final NumberFormatException ignored) {
212                reference = false;
213            }
214        }
215        else {
216            final String name = ent.substring(1, ent.length() - 1);
217            for (String element : ENTITIES) {
218                if (name.equals(element)) {
219                    reference = true;
220                    break;
221                }
222            }
223        }
224        return reference;
225    }
226}