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