001    /* XMLFormatter.java --
002       A class for formatting log messages into a standard XML format
003       Copyright (C) 2002, 2004 Free Software Foundation, Inc.
004    
005    This file is part of GNU Classpath.
006    
007    GNU Classpath is free software; you can redistribute it and/or modify
008    it under the terms of the GNU General Public License as published by
009    the Free Software Foundation; either version 2, or (at your option)
010    any later version.
011    
012    GNU Classpath is distributed in the hope that it will be useful, but
013    WITHOUT ANY WARRANTY; without even the implied warranty of
014    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015    General Public License for more details.
016    
017    You should have received a copy of the GNU General Public License
018    along with GNU Classpath; see the file COPYING.  If not, write to the
019    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
020    02110-1301 USA.
021    
022    Linking this library statically or dynamically with other modules is
023    making a combined work based on this library.  Thus, the terms and
024    conditions of the GNU General Public License cover the whole
025    combination.
026    
027    As a special exception, the copyright holders of this library give you
028    permission to link this library with independent modules to produce an
029    executable, regardless of the license terms of these independent
030    modules, and to copy and distribute the resulting executable under
031    terms of your choice, provided that you also meet, for each linked
032    independent module, the terms and conditions of the license of that
033    module.  An independent module is a module which is not derived from
034    or based on this library.  If you modify this library, you may extend
035    this exception to your version of the library, but you are not
036    obligated to do so.  If you do not wish to do so, delete this
037    exception statement from your version. */
038    
039    
040    package java.util.logging;
041    
042    import gnu.java.lang.CPStringBuilder;
043    
044    import java.text.SimpleDateFormat;
045    import java.util.Date;
046    import java.util.ResourceBundle;
047    
048    /**
049     * An <code>XMLFormatter</code> formats LogRecords into
050     * a standard XML format.
051     *
052     * @author Sascha Brawer (brawer@acm.org)
053     */
054    public class XMLFormatter
055      extends Formatter
056    {
057      /**
058       * Constructs a new XMLFormatter.
059       */
060      public XMLFormatter()
061      {
062      }
063    
064    
065      /**
066       * The character sequence that is used to separate lines in the
067       * generated XML stream. Somewhat surprisingly, the Sun J2SE 1.4
068       * reference implementation always uses UNIX line endings, even on
069       * platforms that have different line ending conventions (i.e.,
070       * DOS). The GNU Classpath implementation does not replicates this
071       * bug.
072       *
073       * See also the Sun bug parade, bug #4462871,
074       * "java.util.logging.SimpleFormatter uses hard-coded line separator".
075       */
076      private static final String lineSep = SimpleFormatter.lineSep;
077    
078    
079      /**
080       * A DateFormat for emitting time in the ISO 8601 format.
081       * Since the API specification of SimpleDateFormat does not talk
082       * about its thread-safety, we cannot share a singleton instance.
083       */
084      private final SimpleDateFormat iso8601
085        = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
086    
087    
088      /**
089       * Appends a line consisting of indentation, opening element tag,
090       * element content, closing element tag and line separator to
091       * a CPStringBuilder, provided that the element content is
092       * actually existing.
093       *
094       * @param buf the CPStringBuilder to which the line will be appended.
095       *
096       * @param indent the indentation level.
097       *
098       * @param tag the element tag name, for instance <code>method</code>.
099       *
100       * @param content the element content, or <code>null</code> to
101       *        have no output whatsoever appended to <code>buf</code>.
102       */
103      private static void appendTag(CPStringBuilder buf, int indent,
104                                    String tag, String content)
105      {
106        int i;
107    
108        if (content == null)
109          return;
110    
111        for (i = 0; i < indent * 2; i++)
112          buf.append(' ');
113    
114        buf.append("<");
115        buf.append(tag);
116        buf.append('>');
117    
118        /* Append the content, but escape for XML by replacing
119         * '&', '<', '>' and all non-ASCII characters with
120         * appropriate escape sequences.
121         * The Sun J2SE 1.4 reference implementation does not
122         * escape non-ASCII characters. This is a bug in their
123         * implementation which has been reported in the Java
124         * bug parade as bug number (FIXME: Insert number here).
125         */
126        for (i = 0; i < content.length(); i++)
127        {
128          char c = content.charAt(i);
129          switch (c)
130          {
131          case '&':
132            buf.append("&amp;");
133            break;
134    
135          case '<':
136            buf.append("&lt;");
137            break;
138    
139          case '>':
140            buf.append("&gt;");
141            break;
142    
143          default:
144            if (((c >= 0x20) && (c <= 0x7e))
145                || (c == /* line feed */ 10)
146                || (c == /* carriage return */ 13))
147              buf.append(c);
148            else
149            {
150              buf.append("&#");
151              buf.append((int) c);
152              buf.append(';');
153            }
154            break;
155          } /* switch (c) */
156        } /* for i */
157    
158        buf.append("</");
159        buf.append(tag);
160        buf.append(">");
161        buf.append(lineSep);
162      }
163    
164    
165      /**
166       * Appends a line consisting of indentation, opening element tag,
167       * numeric element content, closing element tag and line separator
168       * to a CPStringBuilder.
169       *
170       * @param buf the CPStringBuilder to which the line will be appended.
171       *
172       * @param indent the indentation level.
173       *
174       * @param tag the element tag name, for instance <code>method</code>.
175       *
176       * @param content the element content.
177       */
178      private static void appendTag(CPStringBuilder buf, int indent,
179                                    String tag, long content)
180      {
181        appendTag(buf, indent, tag, Long.toString(content));
182      }
183    
184    
185      public String format(LogRecord record)
186      {
187        CPStringBuilder    buf = new CPStringBuilder(400);
188        Level           level = record.getLevel();
189        long            millis = record.getMillis();
190        Object[]        params = record.getParameters();
191        ResourceBundle  bundle = record.getResourceBundle();
192        String          message;
193    
194        buf.append("<record>");
195        buf.append(lineSep);
196    
197    
198        appendTag(buf, 1, "date", iso8601.format(new Date(millis)));
199        appendTag(buf, 1, "millis", millis);
200        appendTag(buf, 1, "sequence", record.getSequenceNumber());
201        appendTag(buf, 1, "logger", record.getLoggerName());
202    
203        if (level.isStandardLevel())
204          appendTag(buf, 1, "level", level.toString());
205        else
206          appendTag(buf, 1, "level", level.intValue());
207    
208        appendTag(buf, 1, "class", record.getSourceClassName());
209        appendTag(buf, 1, "method", record.getSourceMethodName());
210        appendTag(buf, 1, "thread", record.getThreadID());
211    
212        /* The Sun J2SE 1.4 reference implementation does not emit the
213         * message in localized form. This is in violation of the API
214         * specification. The GNU Classpath implementation intentionally
215         * replicates the buggy behavior of the Sun implementation, as
216         * different log files might be a big nuisance to users.
217         */
218        try
219        {
220          record.setResourceBundle(null);
221          message = formatMessage(record);
222        }
223        finally
224        {
225          record.setResourceBundle(bundle);
226        }
227        appendTag(buf, 1, "message", message);
228    
229        /* The Sun J2SE 1.4 reference implementation does not
230         * emit key, catalog and param tags. This is in violation
231         * of the API specification.  The Classpath implementation
232         * intentionally replicates the buggy behavior of the
233         * Sun implementation, as different log files might be
234         * a big nuisance to users.
235         *
236         * FIXME: File a bug report with Sun. Insert bug number here.
237         *
238         *
239         * key = record.getMessage();
240         * if (key == null)
241         *   key = "";
242         *
243         * if ((bundle != null) && !key.equals(message))
244         * {
245         *   appendTag(buf, 1, "key", key);
246         *   appendTag(buf, 1, "catalog", record.getResourceBundleName());
247         * }
248         *
249         * if (params != null)
250         * {
251         *   for (int i = 0; i < params.length; i++)
252         *     appendTag(buf, 1, "param", params[i].toString());
253         * }
254         */
255    
256        /* FIXME: We have no way to obtain the stacktrace before free JVMs
257         * support the corresponding method in java.lang.Throwable.  Well,
258         * it would be possible to parse the output of printStackTrace,
259         * but this would be pretty kludgy. Instead, we postpose the
260         * implementation until Throwable has made progress.
261         */
262        Throwable thrown = record.getThrown();
263        if (thrown != null)
264        {
265          buf.append("  <exception>");
266          buf.append(lineSep);
267    
268          /* The API specification is not clear about what exactly
269           * goes into the XML record for a thrown exception: It
270           * could be the result of getMessage(), getLocalizedMessage(),
271           * or toString(). Therefore, it was necessary to write a
272           * Mauve testlet and run it with the Sun J2SE 1.4 reference
273           * implementation. It turned out that the we need to call
274           * toString().
275           *
276           * FIXME: File a bug report with Sun, asking for clearer
277           * specs.
278           */
279          appendTag(buf, 2, "message", thrown.toString());
280    
281          /* FIXME: The Logging DTD specifies:
282           *
283           * <!ELEMENT exception (message?, frame+)>
284           *
285           * However, java.lang.Throwable.getStackTrace() is
286           * allowed to return an empty array. So, what frame should
287           * be emitted for an empty stack trace? We probably
288           * should file a bug report with Sun, asking for the DTD
289           * to be changed.
290           */
291    
292          buf.append("  </exception>");
293          buf.append(lineSep);
294        }
295    
296    
297        buf.append("</record>");
298        buf.append(lineSep);
299    
300        return buf.toString();
301      }
302    
303    
304      /**
305       * Returns a string that handlers are supposed to emit before
306       * the first log record.  The base implementation returns an
307       * empty string, but subclasses such as {@link XMLFormatter}
308       * override this method in order to provide a suitable header.
309       *
310       * @return a string for the header.
311       *
312       * @param h the handler which will prepend the returned
313       *     string in front of the first log record.  This method
314       *     will inspect certain properties of the handler, for
315       *     example its encoding, in order to construct the header.
316       */
317      public String getHead(Handler h)
318      {
319        CPStringBuilder  buf;
320        String        encoding;
321    
322        buf = new CPStringBuilder(80);
323        buf.append("<?xml version=\"1.0\" encoding=\"");
324    
325        encoding = h.getEncoding();
326    
327        /* file.encoding is a system property with the Sun JVM, indicating
328         * the platform-default file encoding. Unfortunately, the API
329         * specification for java.lang.System.getProperties() does not
330         * list this property.
331         */
332        if (encoding == null)
333          encoding = System.getProperty("file.encoding");
334    
335        /* Since file.encoding is not listed with the API specification of
336         * java.lang.System.getProperties(), there might be some VMs that
337         * do not define this system property.  Therefore, we use UTF-8 as
338         * a reasonable default. Please note that if the platform encoding
339         * uses the same codepoints as US-ASCII for the US-ASCII character
340         * set (e.g, 65 for A), it does not matter whether we emit the
341         * wrong encoding into the XML header -- the GNU Classpath will
342         * emit XML escape sequences like &#1234; for any non-ASCII
343         * character.  Virtually all character encodings use the same code
344         * points as US-ASCII for ASCII characters.  Probably, EBCDIC is
345         * the only exception.
346         */
347        if (encoding == null)
348          encoding = "UTF-8";
349    
350        /* On Windows XP localized for Swiss German (this is one of
351         * my [Sascha Brawer's] test machines), the default encoding
352         * has the canonical name "windows-1252". The "historical" name
353         * of this encoding is "Cp1252" (see the Javadoc for the class
354         * java.nio.charset.Charset for the distinction). Now, that class
355         * does have a method for mapping historical to canonical encoding
356         * names. However, if we used it here, we would be come dependent
357         * on java.nio.*, which was only introduced with J2SE 1.4.
358         * Thus, we do this little hack here. As soon as Classpath supports
359         * java.nio.charset.CharSet, this hack should be replaced by
360         * code that correctly canonicalizes the encoding name.
361         */
362        if ((encoding.length() > 2) && encoding.startsWith("Cp"))
363          encoding = "windows-" + encoding.substring(2);
364    
365        buf.append(encoding);
366    
367        buf.append("\" standalone=\"no\"?>");
368        buf.append(lineSep);
369    
370        /* SYSTEM is not a fully qualified URL so that validating
371         * XML parsers do not need to connect to the Internet in
372         * order to read in a log file.  See also the Sun Bug Parade,
373         * bug #4372790, "Logging APIs: need to use relative URL for XML
374         * doctype".
375         */
376        buf.append("<!DOCTYPE log SYSTEM \"logger.dtd\">");
377        buf.append(lineSep);
378        buf.append("<log>");
379        buf.append(lineSep);
380    
381        return buf.toString();
382      }
383    
384    
385      public String getTail(Handler h)
386      {
387        return "</log>" + lineSep;
388      }
389    }