// Copyright (C) 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.connector.logging;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
/**
* Log message layout pattern for text (non-XML) format messages. It is
* a flexible layout configurable with pattern string. The pattern
* string syntax is based upon the one used by the Log4j PatternLayout
* class. However, since this class is built on top of java.util.logging,
* not all of the log4j conversion patterns are supported.
*
* <p>The goal of this class is to {@link #format format} a
* {@link java.util.logging.LogRecord} and return the results as a String.
* The results depend on the <em>conversion pattern</em> used.
*
* <p>A conversion pattern is composed of literal text and format
* control expressions called <em>conversion specifiers</em>.
*
* <p>Each conversion specifier starts with a percent sign (%) and is
* followed by optional <em>format modifiers</em> and a <em>conversion
* character</em>. The conversion character specifies the type of
* data, e.g. log message, level, date, thread name. The format
* modifiers control such things as field width, padding, left and
* right justification.
*
* <p>For example, given the layout conversion pattern
* <b>"%-4p [%t]: %m%n"</b>, then the code:
* <pre>
Logger logger = getClass().getLogger();
logger.info("Message 1");
logger.warn("Message 2");
</pre>
would yield the output:
<pre>
INFO [main]: Message 1
WARN [main]: Message 2
</pre>
*
* The recognized conversion characters are:
*
* <p> <table border="0" CELLPADDING="8">
*
* <tr><td align="center" valign="top"><b>C</b></td>
* <td>Used to output the class name of the caller
* issuing the logging request. This conversion specifier
* can be optionally followed by <em>precision specifier</em>,
* that is a decimal constant in brackets.
*
* <p>If a precision specifier is given, then only the corresponding
* number of right-most components of the class name will be
* printed. By default the class name is output in fully qualified form.
*
* <p>For example, for the class name "com.google.enterprise.SomeClass",
* the pattern <b>%C{1}</b> will output "SomeClass".
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>d</b></td>
* <td>Used to output the date of the logging event. The date conversion
* specifier may be followed by a <em>date format specifier</em> enclosed
* between braces. For example, <b>%d{HH:mm:ss,SSS}</b> or
* <b>%d{dd MMM yyyy HH:mm:ss,SSS}</b>. If no
* date format specifier is given, then ISO8601 format is assumed.
*
* <p>The date format specifier accepts the same syntax as the
* time pattern string of the {@link java.text.SimpleDateFormat}.
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>f</b></td>
* <td>Expands to the output returned by the Base Formatter for this record.
* This returns the fully formatted output of the underlying Formatter,
* for instance, the output of
* <code>java.util.logging.SimpleFormatter.format()</code>.
*
* <p>This can be useful when embellishing Base Formatter output with
* diagnostic information. For instance, a layout pattern like:
* <b>"[%T %t %X{connectorName}] %f"</b> would prepend a
* formatted log message with the ThreadID, ThreadName, and ConnectorName.
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>m</b></td>
* <td>Outputs the application supplied message associated with
* the logging event.</td>
* </tr>
*
* <tr><td align="center" valign="top"><b>M</b></td>
* <td>Outputs the method name where the logging request was issued.</td>
* </tr>
*
* <tr><td align="center" valign="top"><b>n</b></td>
* <td>Outputs the platform dependent line separator character or characters.
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>p</b></td>
* <td>Outputs the priority of the logging event. This
* cooresponds to the java.util.logging.Level of the LogRecord.</td>
* </tr>
*
* <tr><td align="center" valign="top"><b>t</b></td>
* <td>Outputs the name of the thread that generated the
* logging event.</td>
* </tr>
*
* <tr><td align="center" valign="top"><b>T</b></td>
* <td>Outputs the Id of the thread that generated the
* logging event.</td>
* </tr>
*
* <tr><td align="center" valign="top"><b>x</b></td>
* <td>Outputs the {@link NDC} (nested diagnostic context) associated
* with the thread that generated the logging event.
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>X</b></td>
* <td>Outputs the {@link MDC} (mapped diagnostic context) associated
* with the thread that generated the logging event. The <b>X</b>
* conversion character <em>must</em> be followed by the key for the
* map placed between braces, as in <b>%X{clientNumber}</b> where
* <code>clientNumber</code> is the key. The value in the MDC
* corresponding to the key will be output.
* </td>
* </tr>
*
* <tr><td align="center" valign="top"><b>%</b></td>
* <td>The sequence %% outputs a single percent sign.</td>
* </tr>
* </table>
*
* <p>By default the relevant information is output as is. However,
* with the aid of format modifiers it is possible to change the
* minimum field width, the maximum field width, and justification.
*
* <p>The optional format modifier is placed between the percent sign
* and the conversion character.
*
* <p>The first optional format modifier is the <em>left justification
* flag</em> which is just the minus (-) character. Then comes the
* optional <em>minimum field width</em> modifier. This is a decimal
* constant that represents the minimum number of characters to
* output. If the data item requires fewer characters, it is padded on
* either the left or the right until the minimum width is
* reached. The default is to pad on the left (right justify) but you
* can specify right padding with the left justification flag. The
* padding character is space. If the data item is larger than the
* minimum field width, the field is expanded to accommodate the
* data. The value is never truncated.
*
* <p>This behavior can be changed using the <em>maximum field
* width</em> modifier which is designated by a period followed by a
* decimal constant. If the data item is longer than the maximum
* field, then the extra characters are removed from the end.
*/
public class LayoutPattern {
private static final String NL = System.getProperty("line.separator");
private FormatElement[] formatElements;
private Formatter baseFormatter;
/**
* Construct a layout that uses the supplied layout conversion pattern.
*
* @param format logging conversion format String
*/
public LayoutPattern(String format) {
this(format, null);
}
/**
* Construct a layout that uses the supplied layout conversion pattern.
* It may also make use of an underlying Formatter for some of its work.
*
* @param format logging conversion format String
* @param baseFormatter underlying Formatter that this may use.
*/
public LayoutPattern(String format, Formatter baseFormatter) {
this.baseFormatter = baseFormatter;
parse(format);
}
/**
* Compile the format string into an array format elements
* that can be more quickly evaluated when formatting log entries.
* An example format string might look like:<pre>
* "[%T %t %X{ConnectorName}] %d %C %M%n%p: %m%n"</pre>
*
* @param format logging conversion format String
*/
private void parse(String format) {
ArrayList<FormatElement> elems = new ArrayList<FormatElement>();
if (format != null && format.length() > 0) {
String[] strs = format.split("%", -1);
boolean inConversion = false;
for (int i = 0; i < strs.length ; i++) {
String str = strs[i];
int strLen = str.length();
// If not in a conversion, this token is pure string constant.
if (!inConversion) {
if (strLen > 0) {
elems.add(new StringElement(str));
}
inConversion = true;
continue;
}
// %% - quoted percent sign.
if (strLen == 0) {
elems.add(new StringElement("%"));
inConversion = false;
continue;
}
int skipCount = 0;
// Pull out format modifier.
while ("-0123456789.".indexOf(str.charAt(skipCount)) >= 0) {
skipCount++;
}
String modifier = (skipCount > 0) ? str.substring(0, skipCount) : null;
FormatElement elem = null;
String arg = null;
switch (str.charAt(skipCount++)) {
case 'C': if ((arg = getArg(str, skipCount, strLen)) != null) {
elem = new ClassNameElement(Integer.parseInt(arg));
skipCount += arg.length() + 2;
} else {
elem = new ClassNameElement();
}
break;
case 'd': if ((arg = getArg(str, skipCount, strLen)) != null) {
elem = new DateElement(arg);
skipCount += arg.length() + 2;
} else {
elem = new DateElement();
}
break;
case 'f': if (baseFormatter == null) {
throw new IllegalArgumentException(
"%f format conversion requires a base Formatter");
}
elem = new FormattedRecordElement();
break;
case 'M': elem = new MethodNameElement();
break;
case 'm': elem = new MessageElement();
break;
case 'N': elem = new SequenceNumberElement();
break;
case 'n': elem = new StringElement(NL);
break;
case 'p': elem = new LevelElement();
break;
case 'T': elem = new ThreadIdElement();
break;
case 't': elem = new ThreadNameElement();
break;
case 'x': elem = new NDCElement();
break;
case 'X': if ((arg = getArg(str, skipCount, strLen)) != null) {
elem = new MDCElement(arg);
skipCount += arg.length() + 2;
} else {
throw new IllegalArgumentException(
"MDC format conversion must specify key "
+ str.substring(skipCount));
}
break;
default: // Unimplemented format Conversion. Skip it.
break;
}
// Add the format element to the list of compiled elements.
if (elem != null) {
// If format conversion had a modifier, wrap it around the element.
if (modifier != null) {
elem = new ModifierElement(modifier, elem);
}
elems.add(elem);
}
// If there's anything left in the token, consider it a string constant.
if (skipCount < strLen) {
elems.add(new StringElement(str.substring(skipCount)));
}
}
}
formatElements = elems.toArray(new FormatElement[elems.size()]);
}
/**
* Parse out an argument from the format string. Looks for an argument
* like <code>{xyzzy}</code> occurring at the start offset of the string.
* If found, the substring between the braces is returned. Otherwise null
* is returned.
*
* @param str String we are tokenizing.
* @param off offset into the string to start looking.
* @param len the length of str.
*/
private String getArg(String str, int off, int len) {
if (off < len && str.charAt(off) == '{') {
int end = str.indexOf('}', off + 1);
if (end > 0) {
return str.substring(off + 1, end);
}
}
return null;
}
/**
* Format the supplied LogRecord according to the layout conversion pattern.
*
* @param logRecord The LogRecord to format.
* @return a formatted log entry String.
*/
public String format(LogRecord logRecord) {
StringBuilder str = new StringBuilder();
for (FormatElement elem : formatElements) {
elem.format(str, logRecord);
}
return str.toString();
}
/* *** Compiled Format Elements *** */
private interface FormatElement {
public void format(StringBuilder builder, LogRecord logRecord);
}
/**
* Modifier wraps another FormatElement, adding padding, justification,
* truncation, etc.
*/
private class ModifierElement implements FormatElement {
private boolean leftJustified;
private int minWidth;
private int maxWidth;
private FormatElement base;
/**
* Wrap the supplied base FormatElement, modifying its output
* with padding, justification, and/or truncation.
*/
public ModifierElement(String modifier, FormatElement base) {
this.base = base;
this.parse(modifier);
}
/**
* Parse the modifier string in the form of: "-minWidth.maxWidth"
*/
private void parse(String modifier) {
int start;
if (modifier.charAt(0) == '-') {
leftJustified = true;
start = 1;
} else {
leftJustified = false;
start = 0;
}
int dotPos = modifier.indexOf('.');
// Extract the minWidth.
try {
if (dotPos < 0) {
minWidth = Integer.parseInt(modifier.substring(start));
} else {
minWidth = Integer.parseInt(modifier.substring(start, dotPos));
}
} catch (NumberFormatException nfe) {
minWidth = 0; // Empty string is 0
}
// Extract the maxWidth.
maxWidth = Integer.MAX_VALUE;
if ((dotPos >= 0) && (dotPos < modifier.length())) {
try {
maxWidth = Integer.parseInt(modifier.substring(dotPos + 1));
} catch (NumberFormatException nfe) {
// Empty string is MAX_INT.
}
}
}
@Override
public void format(StringBuilder builder, LogRecord record) {
StringBuilder baseBuilder = new StringBuilder();
base.format(baseBuilder, record);
int len = baseBuilder.length();
if (len >= minWidth && len <= maxWidth) {
builder.append(baseBuilder);
} else if (len < minWidth) {
if (leftJustified) {
builder.append(baseBuilder);
for (; len < minWidth; len++) {
builder.append(' ');
}
} else {
for (; len < minWidth; len++) {
builder.append(' ');
}
builder.append(baseBuilder);
}
} else { // len > maxWidth
builder.append(baseBuilder, 0, maxWidth);
}
}
}
// String constant.
private class StringElement implements FormatElement {
private String string;
public StringElement(String string) {
this.string = string;
}
@Override
public void format(StringBuilder builder, LogRecord ignored) {
builder.append(string);
}
}
// %C - Class Name. %C{n} shows the rightmost n segments of the
// fully qualified class name.
private class ClassNameElement implements FormatElement {
private int segments = 0;
public ClassNameElement() {}
public ClassNameElement(int numSegments) {
this.segments = numSegments;
}
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
String name = logRecord.getSourceClassName();
if (segments > 0) {
int start = name.length();
for (int i = 0; i < segments; i++) {
if ((start = name.lastIndexOf('.', start - 1)) < 0) {
builder.append(name);
return;
}
}
builder.append(name.substring(start + 1));
} else {
builder.append(name);
}
}
}
// %d{date-format} - Date. The date-format specifier uses the same syntax
// as java.text.SimpleDateFormat. ISO8601 is the default.
private class DateElement implements FormatElement {
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd' 'HH:mm:ss";
private SimpleDateFormat dateFormat;
public DateElement() {
this(DEFAULT_DATE_FORMAT);
}
public DateElement(String dateFormat) {
this.dateFormat = new SimpleDateFormat(dateFormat);
}
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(dateFormat.format(new Date(logRecord.getMillis())));
}
}
// %f - Base Formatter Formatted Record.
// This returns the fully formatted output of the underlying Formatter.
// For instance, the output of java.util.logging.SimpleFormatter.format().
// This can be useful when embellishing Base Formatter output with
// MDC information. For instance, given a layout pattern like:
// "[%T %t %X{connectorName}] %f" would prepend a formatted log message
// with the ThreadID, ThreadName, and ConnectorName.
private class FormattedRecordElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
if (baseFormatter != null) {
builder.append(baseFormatter.format(logRecord));
}
}
}
// %M - Method Name.
private class MethodNameElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(logRecord.getSourceMethodName());
}
}
// %m - Message.
private class MessageElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
if (baseFormatter == null) {
builder.append(logRecord.getMessage());
} else {
builder.append(baseFormatter.formatMessage(logRecord));
}
}
}
// %N - Sequence Number.
private class SequenceNumberElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(logRecord.getSequenceNumber());
}
}
// %p - Priority/Level.
private class LevelElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(logRecord.getLevel().getName());
}
}
// %T - Thread ID.
private class ThreadIdElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(Thread.currentThread().getId());
}
}
// %t - Thread Name.
private class ThreadNameElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord logRecord) {
builder.append(Thread.currentThread().getName());
}
}
// %X - MDC value for key.
private class MDCElement implements FormatElement {
private String key;
public MDCElement(String key) {
this.key = key;
}
@Override
public void format(StringBuilder builder, LogRecord ignored) {
builder.append(MDC.get(key));
}
}
// %x - NDC value for thread.
private class NDCElement implements FormatElement {
@Override
public void format(StringBuilder builder, LogRecord ignored) {
builder.append(NDC.peek());
}
}
}