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