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("&");
131 break;
132
133 case '<':
134 buf.append("<");
135 break;
136
137 case '>':
138 buf.append(">");
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 Ӓ 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 }