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