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
005This file is part of GNU Classpath.
006
007GNU Classpath is free software; you can redistribute it and/or modify
008it under the terms of the GNU General Public License as published by
009the Free Software Foundation; either version 2, or (at your option)
010any later version.
011
012GNU Classpath is distributed in the hope that it will be useful, but
013WITHOUT ANY WARRANTY; without even the implied warranty of
014MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015General Public License for more details.
016
017You should have received a copy of the GNU General Public License
018along with GNU Classpath; see the file COPYING.  If not, write to the
019Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02002110-1301 USA.
021
022Linking this library statically or dynamically with other modules is
023making a combined work based on this library.  Thus, the terms and
024conditions of the GNU General Public License cover the whole
025combination.
026
027As a special exception, the copyright holders of this library give you
028permission to link this library with independent modules to produce an
029executable, regardless of the license terms of these independent
030modules, and to copy and distribute the resulting executable under
031terms of your choice, provided that you also meet, for each linked
032independent module, the terms and conditions of the license of that
033module.  An independent module is a module which is not derived from
034or based on this library.  If you modify this library, you may extend
035this exception to your version of the library, but you are not
036obligated to do so.  If you do not wish to do so, delete this
037exception statement from your version. */
038
039
040package java.util.logging;
041
042import gnu.java.lang.CPStringBuilder;
043
044import java.text.SimpleDateFormat;
045import java.util.Date;
046import 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 */
054public 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}