1 /*
   2  * Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 
  27 package java.util.logging;
  28 
  29 import java.nio.charset.Charset;
  30 import java.time.Instant;
  31 import java.time.format.DateTimeFormatter;
  32 import java.util.*;
  33 
  34 /**
  35  * Format a LogRecord into a standard XML format.
  36  * <p>
  37  * The DTD specification is provided as Appendix A to the
  38  * Java Logging APIs specification.
  39  * <p>
  40  * The XMLFormatter can be used with arbitrary character encodings,
  41  * but it is recommended that it normally be used with UTF-8.  The
  42  * character encoding can be set on the output Handler.
  43  *
  44  * @implSpec Since JDK 9, instances of {@linkplain LogRecord} contain
  45  * an {@link LogRecord#getInstant() Instant} which can have nanoseconds below
  46  * the millisecond resolution.
  47  * The DTD specification has been updated to allow for an optional
  48  * {@code <nanos>} element. By default, the XMLFormatter will compute the
  49  * nanosecond adjustment below the millisecond resolution (using
  50  * {@code LogRecord.getInstant().getNano() % 1000_000}) - and if this is not 0,
  51  * this adjustment value will be printed in the new {@code <nanos>} element.
  52  * The event instant can then be reconstructed using
  53  * {@code Instant.ofEpochSecond(millis/1000L, (millis % 1000L) * 1000_000L + nanos)}
  54  * where {@code millis} and {@code nanos} represent the numbers serialized in
  55  * the {@code <millis>} and {@code <nanos>} elements, respectively.
  56  * <br>
  57  * The {@code <date>} element will now contain the whole instant as formatted
  58  * by the {@link DateTimeFormatter#ISO_INSTANT DateTimeFormatter.ISO_INSTANT}
  59  * formatter.
  60  * <p>
  61  * For compatibility with old parsers, XMLFormatters can
  62  * be configured to revert to the old format by specifying a
  63  * {@code <xml-formatter-fully-qualified-class-name>.useInstant = false}
  64  * {@linkplain LogManager#getProperty(java.lang.String) property} in the
  65  * logging configuration. When {@code useInstant} is {@code false}, the old
  66  * formatting will be preserved. When {@code useInstant} is {@code true}
  67  * (the default), the {@code <nanos>} element will be printed and the
  68  * {@code <date>} element will contain the {@linkplain
  69  * DateTimeFormatter#ISO_INSTANT formatted} instant.
  70  * <p>
  71  * For instance, in order to configure plain instances of XMLFormatter to omit
  72  * the new {@code <nano>} element,
  73  * {@code java.util.logging.XMLFormatter.useInstant = false} can be specified
  74  * in the logging configuration.
  75  *
  76  * @since 1.4
  77  */
  78 
  79 public class XMLFormatter extends Formatter {
  80     private final LogManager manager = LogManager.getLogManager();
  81     private final boolean useInstant;
  82 
  83     /**
  84      * Creates a new instance of XMLFormatter.
  85      *
  86      * @implSpec
  87      *    Since JDK 9, the XMLFormatter will print out the record {@linkplain
  88      *    LogRecord#getInstant() event time} as an Instant. This instant
  89      *    has the best resolution available on the system. The {@code <date>}
  90      *    element will contain the instant as formatted by the {@link
  91      *    DateTimeFormatter#ISO_INSTANT}.
  92      *    In addition, an optional {@code <nanos>} element containing a
  93      *    nanosecond adjustment will be printed if the instant contains some
  94      *    nanoseconds below the millisecond resolution.
  95      *    <p>
  96      *    This new behavior can be turned off, and the old formatting restored,
  97      *    by specifying a property in the {@linkplain
  98      *    LogManager#getProperty(java.lang.String) logging configuration}.
  99      *    If {@code LogManager.getLogManager().getProperty(
 100      *    this.getClass().getName()+".useInstant")} is {@code "false"} or
 101      *    {@code "0"}, the old formatting will be restored.
 102      */
 103     public XMLFormatter() {
 104         useInstant = (manager == null)
 105             || manager.getBooleanProperty(
 106                     this.getClass().getName()+".useInstant", true);
 107     }
 108 
 109     // Append a two digit number.
 110     private void a2(StringBuilder sb, int x) {
 111         if (x < 10) {
 112             sb.append('0');
 113         }
 114         sb.append(x);
 115     }
 116 
 117     // Append the time and date in ISO 8601 format
 118     private void appendISO8601(StringBuilder sb, long millis) {
 119         GregorianCalendar cal = new GregorianCalendar();
 120         cal.setTimeInMillis(millis);
 121         sb.append(cal.get(Calendar.YEAR));
 122         sb.append('-');
 123         a2(sb, cal.get(Calendar.MONTH) + 1);
 124         sb.append('-');
 125         a2(sb, cal.get(Calendar.DAY_OF_MONTH));
 126         sb.append('T');
 127         a2(sb, cal.get(Calendar.HOUR_OF_DAY));
 128         sb.append(':');
 129         a2(sb, cal.get(Calendar.MINUTE));
 130         sb.append(':');
 131         a2(sb, cal.get(Calendar.SECOND));
 132     }
 133 
 134     // Append to the given StringBuilder an escaped version of the
 135     // given text string where XML special characters have been escaped.
 136     // For a null string we append "<null>"
 137     private void escape(StringBuilder sb, String text) {
 138         if (text == null) {
 139             text = "<null>";
 140         }
 141         for (int i = 0; i < text.length(); i++) {
 142             char ch = text.charAt(i);
 143             if (ch == '<') {
 144                 sb.append("&lt;");
 145             } else if (ch == '>') {
 146                 sb.append("&gt;");
 147             } else if (ch == '&') {
 148                 sb.append("&amp;");
 149             } else {
 150                 sb.append(ch);
 151             }
 152         }
 153     }
 154 
 155     /**
 156      * Format the given message to XML.
 157      * <p>
 158      * This method can be overridden in a subclass.
 159      * It is recommended to use the {@link Formatter#formatMessage}
 160      * convenience method to localize and format the message field.
 161      *
 162      * @param record the log record to be formatted.
 163      * @return a formatted log record
 164      */
 165     @Override
 166     public String format(LogRecord record) {
 167         StringBuilder sb = new StringBuilder(500);
 168         sb.append("<record>\n");
 169 
 170         final Instant instant = record.getInstant();
 171 
 172         sb.append("  <date>");
 173         if (useInstant) {
 174             // If useInstant is true - we will print the instant in the
 175             // date field, using the ISO_INSTANT formatter.
 176             DateTimeFormatter.ISO_INSTANT.formatTo(instant, sb);
 177         } else {
 178             // If useInstant is false - we will keep the 'old' formating
 179             appendISO8601(sb, instant.toEpochMilli());
 180         }
 181         sb.append("</date>\n");
 182 
 183         sb.append("  <millis>");
 184         sb.append(instant.toEpochMilli());
 185         sb.append("</millis>\n");
 186 
 187         final int nanoAdjustment = instant.getNano() % 1000_000;
 188         if (useInstant && nanoAdjustment != 0) {
 189             sb.append("  <nanos>");
 190             sb.append(nanoAdjustment);
 191             sb.append("</nanos>\n");
 192         }
 193 
 194         sb.append("  <sequence>");
 195         sb.append(record.getSequenceNumber());
 196         sb.append("</sequence>\n");
 197 
 198         String name = record.getLoggerName();
 199         if (name != null) {
 200             sb.append("  <logger>");
 201             escape(sb, name);
 202             sb.append("</logger>\n");
 203         }
 204 
 205         sb.append("  <level>");
 206         escape(sb, record.getLevel().toString());
 207         sb.append("</level>\n");
 208 
 209         if (record.getSourceClassName() != null) {
 210             sb.append("  <class>");
 211             escape(sb, record.getSourceClassName());
 212             sb.append("</class>\n");
 213         }
 214 
 215         if (record.getSourceMethodName() != null) {
 216             sb.append("  <method>");
 217             escape(sb, record.getSourceMethodName());
 218             sb.append("</method>\n");
 219         }
 220 
 221         sb.append("  <thread>");
 222         sb.append(record.getThreadID());
 223         sb.append("</thread>\n");
 224 
 225         if (record.getMessage() != null) {
 226             // Format the message string and its accompanying parameters.
 227             String message = formatMessage(record);
 228             sb.append("  <message>");
 229             escape(sb, message);
 230             sb.append("</message>");
 231             sb.append("\n");
 232         }
 233 
 234         // If the message is being localized, output the key, resource
 235         // bundle name, and params.
 236         ResourceBundle bundle = record.getResourceBundle();
 237         try {
 238             if (bundle != null && bundle.getString(record.getMessage()) != null) {
 239                 sb.append("  <key>");
 240                 escape(sb, record.getMessage());
 241                 sb.append("</key>\n");
 242                 sb.append("  <catalog>");
 243                 escape(sb, record.getResourceBundleName());
 244                 sb.append("</catalog>\n");
 245             }
 246         } catch (Exception ex) {
 247             // The message is not in the catalog.  Drop through.
 248         }
 249 
 250         Object parameters[] = record.getParameters();
 251         //  Check to see if the parameter was not a messagetext format
 252         //  or was not null or empty
 253         if (parameters != null && parameters.length != 0
 254                 && record.getMessage().indexOf('{') == -1 ) {
 255             for (Object parameter : parameters) {
 256                 sb.append("  <param>");
 257                 try {
 258                     escape(sb, parameter.toString());
 259                 } catch (Exception ex) {
 260                     sb.append("???");
 261                 }
 262                 sb.append("</param>\n");
 263             }
 264         }
 265 
 266         if (record.getThrown() != null) {
 267             // Report on the state of the throwable.
 268             Throwable th = record.getThrown();
 269             sb.append("  <exception>\n");
 270             sb.append("    <message>");
 271             escape(sb, th.toString());
 272             sb.append("</message>\n");
 273             StackTraceElement trace[] = th.getStackTrace();
 274             for (StackTraceElement frame : trace) {
 275                 sb.append("    <frame>\n");
 276                 sb.append("      <class>");
 277                 escape(sb, frame.getClassName());
 278                 sb.append("</class>\n");
 279                 sb.append("      <method>");
 280                 escape(sb, frame.getMethodName());
 281                 sb.append("</method>\n");
 282                 // Check for a line number.
 283                 if (frame.getLineNumber() >= 0) {
 284                     sb.append("      <line>");
 285                     sb.append(frame.getLineNumber());
 286                     sb.append("</line>\n");
 287                 }
 288                 sb.append("    </frame>\n");
 289             }
 290             sb.append("  </exception>\n");
 291         }
 292 
 293         sb.append("</record>\n");
 294         return sb.toString();
 295     }
 296 
 297     /**
 298      * Return the header string for a set of XML formatted records.
 299      *
 300      * @param   h  The target handler (can be null)
 301      * @return  a valid XML string
 302      */
 303     @Override
 304     public String getHead(Handler h) {
 305         StringBuilder sb = new StringBuilder();
 306         String encoding;
 307         sb.append("<?xml version=\"1.0\"");
 308 
 309         if (h != null) {
 310             encoding = h.getEncoding();
 311         } else {
 312             encoding = null;
 313         }
 314 
 315         if (encoding == null) {
 316             // Figure out the default encoding.
 317             encoding = java.nio.charset.Charset.defaultCharset().name();
 318         }
 319         // Try to map the encoding name to a canonical name.
 320         try {
 321             Charset cs = Charset.forName(encoding);
 322             encoding = cs.name();
 323         } catch (Exception ex) {
 324             // We hit problems finding a canonical name.
 325             // Just use the raw encoding name.
 326         }
 327 
 328         sb.append(" encoding=\"");
 329         sb.append(encoding);
 330         sb.append("\"");
 331         sb.append(" standalone=\"no\"?>\n");
 332 
 333         sb.append("<!DOCTYPE log SYSTEM \"logger.dtd\">\n");
 334         sb.append("<log>\n");
 335         return sb.toString();
 336     }
 337 
 338     /**
 339      * Return the tail string for a set of XML formatted records.
 340      *
 341      * @param   h  The target handler (can be null)
 342      * @return  a valid XML string
 343      */
 344     @Override
 345     public String getTail(Handler h) {
 346         return "</log>\n";
 347     }
 348 }