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 mCloseStream;
053
054    /** helper writer that allows easy encoding and printing */
055    private PrintWriter mWriter;
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 aOS the stream to write logs to.
065     * @param aCloseStream close aOS in auditFinished
066     */
067    public XMLLogger(OutputStream aOS, boolean aCloseStream)
068    {
069        setOutputStream(aOS);
070        mCloseStream = aCloseStream;
071    }
072
073    /**
074     * sets the OutputStream
075     * @param aOS the OutputStream to use
076     **/
077    private void setOutputStream(OutputStream aOS)
078    {
079        try {
080            final OutputStreamWriter osw = new OutputStreamWriter(aOS, "UTF-8");
081            mWriter = new PrintWriter(osw);
082        }
083        catch (final UnsupportedEncodingException e) {
084            // unlikely to happen...
085            throw new ExceptionInInitializerError(e);
086        }
087    }
088
089    /** {@inheritDoc} */
090    public void auditStarted(AuditEvent aEvt)
091    {
092        mWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
093
094        final ResourceBundle compilationProperties =
095            ResourceBundle.getBundle("checkstylecompilation");
096        final String version =
097            compilationProperties.getString("checkstyle.compile.version");
098
099        mWriter.println("<checkstyle version=\"" + version + "\">");
100    }
101
102    /** {@inheritDoc} */
103    public void auditFinished(AuditEvent aEvt)
104    {
105        mWriter.println("</checkstyle>");
106        if (mCloseStream) {
107            mWriter.close();
108        }
109        else {
110            mWriter.flush();
111        }
112    }
113
114    /** {@inheritDoc} */
115    public void fileStarted(AuditEvent aEvt)
116    {
117        mWriter.println("<file name=\"" + encode(aEvt.getFileName()) + "\">");
118    }
119
120    /** {@inheritDoc} */
121    public void fileFinished(AuditEvent aEvt)
122    {
123        mWriter.println("</file>");
124    }
125
126    /** {@inheritDoc} */
127    public void addError(AuditEvent aEvt)
128    {
129        if (!SeverityLevel.IGNORE.equals(aEvt.getSeverityLevel())) {
130            mWriter.print("<error" + " line=\"" + aEvt.getLine() + "\"");
131            if (aEvt.getColumn() > 0) {
132                mWriter.print(" column=\"" + aEvt.getColumn() + "\"");
133            }
134            mWriter.print(" severity=\""
135                + aEvt.getSeverityLevel().getName()
136                + "\"");
137            mWriter.print(" message=\""
138                + encode(aEvt.getMessage())
139                + "\"");
140            mWriter.println(" source=\""
141                + encode(aEvt.getSourceName())
142                + "\"/>");
143        }
144    }
145
146    /** {@inheritDoc} */
147    public void addException(AuditEvent aEvt, Throwable aThrowable)
148    {
149        final StringWriter sw = new StringWriter();
150        final PrintWriter pw = new PrintWriter(sw);
151        pw.println("<exception>");
152        pw.println("<![CDATA[");
153        aThrowable.printStackTrace(pw);
154        pw.println("]]>");
155        pw.println("</exception>");
156        pw.flush();
157        mWriter.println(encode(sw.toString()));
158    }
159
160    /**
161     * Escape &lt;, &gt; &amp; &#39; and &quot; as their entities.
162     * @param aValue the value to escape.
163     * @return the escaped value if necessary.
164     */
165    public String encode(String aValue)
166    {
167        final StringBuffer sb = new StringBuffer();
168        for (int i = 0; i < aValue.length(); i++) {
169            final char c = aValue.charAt(i);
170            switch (c) {
171            case '<':
172                sb.append("&lt;");
173                break;
174            case '>':
175                sb.append("&gt;");
176                break;
177            case '\'':
178                sb.append("&apos;");
179                break;
180            case '\"':
181                sb.append("&quot;");
182                break;
183            case '&':
184                final int nextSemi = aValue.indexOf(";", i);
185                if ((nextSemi < 0)
186                    || !isReference(aValue.substring(i, nextSemi + 1)))
187                {
188                    sb.append("&amp;");
189                }
190                else {
191                    sb.append('&');
192                }
193                break;
194            default:
195                sb.append(c);
196                break;
197            }
198        }
199        return sb.toString();
200    }
201
202    /**
203     * @return whether the given argument a character or entity reference
204     * @param aEnt the possible entity to look for.
205     */
206    public boolean isReference(String aEnt)
207    {
208        if (!(aEnt.charAt(0) == '&') || !aEnt.endsWith(";")) {
209            return false;
210        }
211
212        if (aEnt.charAt(1) == '#') {
213            int prefixLength = 2; // "&#"
214            int radix = BASE_10;
215            if (aEnt.charAt(2) == 'x') {
216                prefixLength++;
217                radix = BASE_16;
218            }
219            try {
220                Integer.parseInt(
221                    aEnt.substring(prefixLength, aEnt.length() - 1), radix);
222                return true;
223            }
224            catch (final NumberFormatException nfe) {
225                return false;
226            }
227        }
228
229        final String name = aEnt.substring(1, aEnt.length() - 1);
230        for (String element : ENTITIES) {
231            if (name.equals(element)) {
232                return true;
233            }
234        }
235        return false;
236    }
237}