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 }