/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.util.logging; import java.io.IOException; import java.io.StringWriter; import java.text.FieldPosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; import java.util.logging.ConsoleHandler; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import org.geotools.io.LineWriter; import org.geotools.util.Utilities; /** * A formatter writting log messages on a single line. Compared to {@link SimpleFormatter}, this * formatter uses only one line per message instead of two. For example a message formatted by * {@code MonolineFormatter} looks like: * * <blockquote><pre> * FINE core - A log message logged with level FINE from the "org.geotools.core" logger. * </pre></blockquote> * * By default, {@code MonolineFormatter} displays only the level and the message. Additional * fields can be formatted if {@link #setTimeFormat} or {@link #setSourceFormat} methods are * invoked with a non-null argument. The format can also be set from the * {@code jre/lib/logging.properties} file. For example, user can cut and paste the following * properties into {@code logging.properties}: * * <blockquote><pre> * ############################################################ * # Properties for the Geotools's MonolineFormatter. * # By default, the monoline formatter display only the level * # and the message. Additional fields can be specified here: * # * # time: If set, writes the time ellapsed since the initialization. * # The argument specifies the output pattern. For example, the * # pattern HH:mm:ss.SSSS displays the hours, minutes, seconds * # and milliseconds. * # * # source: If set, writes the source logger or the source class name. * # The argument specifies the type of source to display. Valid * # values are none, logger:short, logger:long, class:short and * # class:long. * ############################################################ * org.geotools.util.logging.MonolineFormatter.time = HH:mm:ss.SSS * org.geotools.util.logging.MonolineFormatter.source = class:short * </pre></blockquote> * * The example below set the {@code MonolineFormatter} for the whole system * with level FINE and "Cp850" page encoding (which is appropriate for some * DOS command lines on Windows). * * <blockquote><pre> * java.util.logging.ConsoleHandler.formatter = org.geotools.util.logging.MonolineFormatter * java.util.logging.ConsoleHandler.encoding = Cp850 * java.util.logging.ConsoleHandler.level = FINE * </pre></blockquote> * * @since 2.0 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class MonolineFormatter extends Formatter { /** * The string to write at the begining of all log headers (e.g. "[FINE core]") */ private static final String PREFIX = ""; /** * The string to write at the end of every log header (e.g. "[FINE core]"). * It should includes the spaces between the header and the message body. */ private static final String SUFFIX = " - "; /** * The default header width. */ private static final int DEFAULT_WIDTH = 9; /** Do not format source class name. */ private static final int NO_SOURCE = 0; /** Explicit value for 'none'. */ private static final int NO_SOURCE_EX = 1; /** Format the source logger without base. */ private static final int LOGGER_SHORT = 2; /** Format the source logger only. */ private static final int LOGGER_LONG = 3; /** Format the class name without package. */ private static final int CLASS_SHORT = 4; /** Format the fully qualified class name. */ private static final int CLASS_LONG = 5; /** * The label to use in the {@code logging.properties} for setting the source format. */ private static String[] FORMAT_LABELS = new String[6]; static { FORMAT_LABELS[NO_SOURCE_EX] = "none"; FORMAT_LABELS[LOGGER_SHORT] = "logger:short"; FORMAT_LABELS[LOGGER_LONG ] = "logger:long"; FORMAT_LABELS[ CLASS_SHORT] = "class:short"; FORMAT_LABELS[ CLASS_LONG ] = "class:long"; } /** * The line separator. This is the value of the "line.separator" * property at the time the {@code MonolineFormatter} was created. */ private final String lineSeparator = System.getProperty("line.separator", "\n"); /** * The line separator for the message body. This line always begin with * {@link #lineSeparator}, followed by some amount of spaces in order to * align the message. */ private String bodyLineSeparator = lineSeparator; /** * The minimum amount of spaces to use for writting level and module name * before the message. For example if this value is 12, then a message from * module "org.geotools.core" with level FINE would be formatted as * "<code>[core  FINE]</code> <cite>the message</cite>" * (i.e. the whole <code>[ ]</code> part is 12 characters wide). */ private final int margin; /** * Time of {@code MonolineFormatter} creation, * in milliseconds ellapsed since January 1, 1970. */ private final long startMillis; /** * The format to use for formatting ellapsed time, * or {@code null} if there is none. */ private SimpleDateFormat timeFormat = null; /** * One of the following constants: {@link #NO_SOURCE}, * {@link #LOGGER_SHORT}, {@link #LOGGER_LONG}, * {@link #CLASS_SHORT} or {@link #CLASS_LONG}. */ private int sourceFormat = NO_SOURCE; /** * Buffer for formatting messages. We will reuse this * buffer in order to reduce memory allocations. */ private final StringBuffer buffer; /** * The line writer. This object transform all "\r", "\n" or "\r\n" occurences * into a single line separator. This line separator will include space for * the marging, if needed. */ private final LineWriter writer; /** * Constructs a default {@code MonolineFormatter}. */ public MonolineFormatter() { this.startMillis = System.currentTimeMillis(); this.margin = DEFAULT_WIDTH; StringWriter str = new StringWriter(); writer = new LineWriter(str); buffer = str.getBuffer(); buffer.append(PREFIX); // Configure this formatter final LogManager manager = LogManager.getLogManager(); final String classname = MonolineFormatter.class.getName(); try { setTimeFormat(manager.getProperty(classname + ".time")); } catch (IllegalArgumentException exception) { // Can't use the logging framework, since we are configuring it. // Display the exception name only, not the trace. System.err.println(exception); } try { setSourceFormat(manager.getProperty(classname + ".source")); } catch (IllegalArgumentException exception) { System.err.println(exception); } } /** * Sets the format for displaying ellapsed time. The pattern must matches * the format specified in {@link SimpleDateFormat}. For example, the * pattern <code>"HH:mm:ss.SSS"</code> will display the ellapsed time * in hours, minutes, seconds and milliseconds. * * @param pattern The time patter, or {@code null} to disable time formatting. */ public synchronized void setTimeFormat(final String pattern) { if (pattern == null) { timeFormat = null; } else if (timeFormat == null) { timeFormat = new SimpleDateFormat(pattern); timeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } else { timeFormat.applyPattern(pattern); } } /** * Returns the format for displaying ellapsed time. This is the pattern specified * to the last call to {@link #setTimeFormat}, or the patten specified in the * {@code org.geotools.MonolineFormater.time} property in the * {@code jre/lib/logging.properties} file. * * @return The time pattern, or {@code null} if time is not formatted. */ public synchronized String getTimeFormat() { return (timeFormat != null) ? timeFormat.toPattern() : null; } /** * Sets the format for displaying the source. The pattern may be one of the following: * * <code>"none"</code>, * <code>"logger:short"</code>, <code>"class:short"</code>, * <code>"logger:long"</code> or <code>"class:long"</code>. * * The difference between a {@code null} and <code>"none"</code> is that {@code null} * may be replaced by a default value, while <code>"none"</code> means that the user * explicitly requested no source. * * @param format The format for displaying the source. */ public synchronized void setSourceFormat(String format) { if (format != null) { format = format.trim().toLowerCase(); } for (int i=0; i<FORMAT_LABELS.length; i++) { if (Utilities.equals(FORMAT_LABELS[i], format)) { sourceFormat = i; return; } } throw new IllegalArgumentException(format); } /** * Returns the format for displaying the source. This is the pattern specified * to the last call to {@link #setSourceFormat}, or the patten specified in the * {@code org.geotools.MonolineFormater.source} property in the * {@code jre/lib/logging.properties} file. * * @return The source pattern, or {@code null} if source is not formatted. */ public String getSourceFormat() { return FORMAT_LABELS[sourceFormat]; } /** * Formats the given log record and return the formatted string. * * @param record the log record to be formatted. * @return a formatted log record */ @SuppressWarnings("fallthrough") public synchronized String format(final LogRecord record) { buffer.setLength(PREFIX.length()); /* * Formats the time (e.g. "00:00:12.365"). The time pattern can be set * either programmatically with a call to setTimeFormat(String), or in * the logging.properties file with the * "org.geotools.util.logging.MonolineFormatter.time" property. */ if (timeFormat != null) { Date time = new Date(Math.max(0, record.getMillis() - startMillis)); timeFormat.format(time, buffer, new FieldPosition(0)); buffer.append(' '); } /* * Formats the level (e.g. "FINE"). We do not provide * the option to turn level off for now. */ if (true) { int offset = buffer.length(); buffer.append(record.getLevel().getLocalizedName()); offset = buffer.length() - offset; buffer.append(Utilities.spaces(margin-offset)); } /* * Adds the source. It may be either the source logger or the source class name. */ String logger = record.getLoggerName(); String classname = record.getSourceClassName(); switch (sourceFormat) { case LOGGER_SHORT: { int pos = logger.lastIndexOf('.'); if (pos >= 0) { logger = logger.substring(pos); } // fall through } case LOGGER_LONG: { buffer.append(' '); buffer.append(logger); break; } case CLASS_SHORT: { int dot = classname.lastIndexOf('.'); if (dot >= 0) { classname = classname.substring(dot+1); } classname = classname.replace('$','.'); // fall through } case CLASS_LONG: { buffer.append(' '); buffer.append(classname); break; } } buffer.append(SUFFIX); /* * Now format the message. We will use a line separator made of the * usual EOL ("\r", "\n", or "\r\n", which is plateform specific) * following by some amout of space in order to align message body. */ final int margin = buffer.length(); assert margin >= this.margin; if (bodyLineSeparator.length() != lineSeparator.length()+margin) { bodyLineSeparator = lineSeparator + Utilities.spaces(margin); } try { writer.setLineSeparator(bodyLineSeparator); writer.write(String.valueOf(formatMessage(record))); writer.setLineSeparator(lineSeparator); writer.write('\n'); writer.flush(); } catch (IOException exception) { // Should never happen, since we are writting into a StringBuffer. throw new AssertionError(exception); } return buffer.toString(); } /** * Setup a {@code MonolineFormatter} for the specified logger and its children. This method * search for all instances of {@link ConsoleHandler} using the {@link SimpleFormatter}. If * such instances are found, they are replaced by a single instance of {@code MonolineFormatter}. * If no such {@link ConsoleHandler} are found, then a new one is created with this * {@code MonolineFormatter}. * <p> * In addition, this method can set the handler levels. If the level is non-null, then all * {@link Handler}s using the monoline formatter will be set to the specified level. This * is provided for convenience, but non-null {@code level} argument should be avoided as * much as possible because it overrides user's level settings. A user trying to configure * his logging properties file may find confusing to see his setting ignored. * * @param logger The base logger to apply the change on. * @param level The desired level, or {@code null} if no level should be set. * @return The registered {@code MonolineFormatter} (never {@code null}). * The formatter output can be configured using the {@link #setTimeFormat} * and {@link #setSourceFormat} methods. */ public static MonolineFormatter configureConsoleHandler(final Logger logger, final Level level) { MonolineFormatter monoline = null; boolean foundConsoleHandler = false; Handler[] handlers = logger.getHandlers(); for (int i=0; i<handlers.length; i++) { final Handler handler = handlers[i]; if (handler.getClass().equals(ConsoleHandler.class)) { foundConsoleHandler = true; final Formatter formatter = handler.getFormatter(); if (formatter instanceof MonolineFormatter) { /* * A MonolineFormatter already existed. Sets the level only for the first * instance (only one instance should exists anyway) for consistency with * the fact that this method returns only one MonolineFormatter for further * configuration. */ if (monoline == null) { monoline = (MonolineFormatter) formatter; if (level != null) { handler.setLevel(level); } } } else if (formatter.getClass().equals(SimpleFormatter.class)) { /* * A ConsoleHandler using the SimpleFormatter has been found. Replaces * the SimpleFormatter by MonolineFormatter, creating it if necessary. * If the handler setting fail with an exception, then we will continue * to use the old J2SE handler instead. */ if (monoline == null) { monoline = new MonolineFormatter(); } try { handler.setFormatter(monoline); if (level != null) { handler.setLevel(level); } } catch (SecurityException exception) { unexpectedException(exception); } } } } /* * If the logger uses parent handlers, copy them to the logger that we are initializing, * because we will not use parent handlers anymore at the end of this method. */ for (Logger parent=logger; parent.getUseParentHandlers();) { parent = parent.getParent(); if (parent == null) { break; } handlers = parent.getHandlers(); for (int i=0; i<handlers.length; i++) { Handler handler = handlers[i]; if (handler.getClass().equals(ConsoleHandler.class)) { if (!foundConsoleHandler) { // We have already set a ConsoleHandler and we don't want a second one. continue; } foundConsoleHandler = true; final Formatter formatter = handler.getFormatter(); if (formatter.getClass().equals(SimpleFormatter.class)) { monoline = addHandler(logger, level); continue; } } logger.addHandler(handler); } } logger.setUseParentHandlers(false); if (!foundConsoleHandler) { monoline = addHandler(logger, level); } return monoline; } /** * Adds to the specified logger a {@link Handler} using a {@code MonolineFormatter} * set at the specified level. The formatter is returned for convenience. */ private static MonolineFormatter addHandler(final Logger logger, final Level level) { final MonolineFormatter monoline = new MonolineFormatter(); try { final Handler handler = new ConsoleHandler(); handler.setFormatter(monoline); if (level != null) { handler.setLevel(level); } logger.addHandler(handler); } catch (SecurityException exception) { unexpectedException(exception); /* * Returns without any change to the J2SE configuration. Note that the returned * MonolineFormatter is really a dummy one, since we failed to register it. It * will not prevent to program to work; just produces different logging outputs. */ } return monoline; } /** * Invoked when an error occurs during the initialization. */ private static void unexpectedException(final Exception exception) { Logging.unexpectedException(MonolineFormatter.class, "configureConsoleHandler", exception); } }