001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.util.logging;
018:
019: import java.io.IOException;
020: import java.io.StringWriter;
021: import java.text.FieldPosition;
022: import java.text.SimpleDateFormat;
023: import java.util.Date;
024: import java.util.TimeZone;
025: import java.util.logging.ConsoleHandler;
026: import java.util.logging.Formatter;
027: import java.util.logging.Handler;
028: import java.util.logging.Level;
029: import java.util.logging.LogManager;
030: import java.util.logging.LogRecord;
031: import java.util.logging.Logger;
032: import java.util.logging.SimpleFormatter;
033:
034: import org.geotools.io.LineWriter;
035: import org.geotools.resources.Utilities;
036:
037: /**
038: * A formatter writting log messages on a single line. Compared to {@link SimpleFormatter}, this
039: * formatter uses only one line per message instead of two. For example a message formatted by
040: * {@code MonolineFormatter} looks like:
041: *
042: * <blockquote><pre>
043: * FINE core - A log message logged with level FINE from the "org.geotools.core" logger.
044: * </pre></blockquote>
045: *
046: * By default, {@code MonolineFormatter} displays only the level and the message. Additional
047: * fields can be formatted if {@link #setTimeFormat} or {@link #setSourceFormat} methods are
048: * invoked with a non-null argument. The format can also be set from the
049: * {@code jre/lib/logging.properties} file. For example, user can cut and paste the following
050: * properties into {@code logging.properties}:
051: *
052: * <blockquote><pre>
053: * ############################################################
054: * # Properties for the Geotools's MonolineFormatter.
055: * # By default, the monoline formatter display only the level
056: * # and the message. Additional fields can be specified here:
057: * #
058: * # time: If set, writes the time ellapsed since the initialization.
059: * # The argument specifies the output pattern. For example, the
060: * # pattern HH:mm:ss.SSSS displays the hours, minutes, seconds
061: * # and milliseconds.
062: * #
063: * # source: If set, writes the source logger or the source class name.
064: * # The argument specifies the type of source to display. Valid
065: * # values are none, logger:short, logger:long, class:short and
066: * # class:long.
067: * ############################################################
068: * org.geotools.util.logging.MonolineFormatter.time = HH:mm:ss.SSS
069: * org.geotools.util.logging.MonolineFormatter.source = class:short
070: * </pre></blockquote>
071: *
072: * The example below set the {@code MonolineFormatter} for the whole system
073: * with level FINE and "Cp850" page encoding (which is appropriate for some
074: * DOS command lines on Windows).
075: *
076: * <blockquote><pre>
077: * java.util.logging.ConsoleHandler.formatter = org.geotools.util.logging.MonolineFormatter
078: * java.util.logging.ConsoleHandler.encoding = Cp850
079: * java.util.logging.ConsoleHandler.level = FINE
080: * </pre></blockquote>
081: *
082: * @since 2.0
083: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/util/logging/MonolineFormatter.java $
084: * @version $Id: MonolineFormatter.java 27848 2007-11-12 13:10:32Z desruisseaux $
085: * @author Martin Desruisseaux
086: */
087: public class MonolineFormatter extends Formatter {
088: /**
089: * The string to write at the begining of all log headers (e.g. "[FINE core]")
090: */
091: private static final String PREFIX = "";
092:
093: /**
094: * The string to write at the end of every log header (e.g. "[FINE core]").
095: * It should includes the spaces between the header and the message body.
096: */
097: private static final String SUFFIX = " - ";
098:
099: /**
100: * The default header width.
101: */
102: private static final int DEFAULT_WIDTH = 9;
103:
104: /** Do not format source class name. */
105: private static final int NO_SOURCE = 0;
106: /** Explicit value for 'none'. */
107: private static final int NO_SOURCE_EX = 1;
108: /** Format the source logger without base. */
109: private static final int LOGGER_SHORT = 2;
110: /** Format the source logger only. */
111: private static final int LOGGER_LONG = 3;
112: /** Format the class name without package. */
113: private static final int CLASS_SHORT = 4;
114: /** Format the fully qualified class name. */
115: private static final int CLASS_LONG = 5;
116:
117: /**
118: * The label to use in the {@code logging.properties} for setting the source format.
119: */
120: private static String[] FORMAT_LABELS = new String[6];
121: static {
122: FORMAT_LABELS[NO_SOURCE_EX] = "none";
123: FORMAT_LABELS[LOGGER_SHORT] = "logger:short";
124: FORMAT_LABELS[LOGGER_LONG] = "logger:long";
125: FORMAT_LABELS[CLASS_SHORT] = "class:short";
126: FORMAT_LABELS[CLASS_LONG] = "class:long";
127: }
128:
129: /**
130: * The line separator. This is the value of the "line.separator"
131: * property at the time the {@code MonolineFormatter} was created.
132: */
133: private final String lineSeparator = System.getProperty(
134: "line.separator", "\n");
135:
136: /**
137: * The line separator for the message body. This line always begin with
138: * {@link #lineSeparator}, followed by some amount of spaces in order to
139: * align the message.
140: */
141: private String bodyLineSeparator = lineSeparator;
142:
143: /**
144: * The minimum amount of spaces to use for writting level and module name
145: * before the message. For example if this value is 12, then a message from
146: * module "org.geotools.core" with level FINE would be formatted as
147: * "<code>[core FINE]</code> <cite>the message</cite>"
148: * (i.e. the whole <code>[ ]</code> part is 12 characters wide).
149: */
150: private final int margin;
151:
152: /**
153: * Time of {@code MonolineFormatter} creation,
154: * in milliseconds ellapsed since January 1, 1970.
155: */
156: private final long startMillis;
157:
158: /**
159: * The format to use for formatting ellapsed time,
160: * or {@code null} if there is none.
161: */
162: private SimpleDateFormat timeFormat = null;
163:
164: /**
165: * One of the following constants: {@link #NO_SOURCE},
166: * {@link #LOGGER_SHORT}, {@link #LOGGER_LONG},
167: * {@link #CLASS_SHORT} or {@link #CLASS_LONG}.
168: */
169: private int sourceFormat = NO_SOURCE;
170:
171: /**
172: * Buffer for formatting messages. We will reuse this
173: * buffer in order to reduce memory allocations.
174: */
175: private final StringBuffer buffer;
176:
177: /**
178: * The line writer. This object transform all "\r", "\n" or "\r\n" occurences
179: * into a single line separator. This line separator will include space for
180: * the marging, if needed.
181: */
182: private final LineWriter writer;
183:
184: /**
185: * Constructs a default {@code MonolineFormatter}.
186: */
187: public MonolineFormatter() {
188: this .startMillis = System.currentTimeMillis();
189: this .margin = DEFAULT_WIDTH;
190: StringWriter str = new StringWriter();
191: writer = new LineWriter(str);
192: buffer = str.getBuffer();
193: buffer.append(PREFIX);
194:
195: // Configure this formatter
196: final LogManager manager = LogManager.getLogManager();
197: final String classname = MonolineFormatter.class.getName();
198: try {
199: setTimeFormat(manager.getProperty(classname + ".time"));
200: } catch (IllegalArgumentException exception) {
201: // Can't use the logging framework, since we are configuring it.
202: // Display the exception name only, not the trace.
203: System.err.println(exception);
204: }
205: try {
206: setSourceFormat(manager.getProperty(classname + ".source"));
207: } catch (IllegalArgumentException exception) {
208: System.err.println(exception);
209: }
210: }
211:
212: /**
213: * Sets the format for displaying ellapsed time. The pattern must matches
214: * the format specified in {@link SimpleDateFormat}. For example, the
215: * pattern <code>"HH:mm:ss.SSS"</code> will display the ellapsed time
216: * in hours, minutes, seconds and milliseconds.
217: *
218: * @param pattern The time patter, or {@code null} to disable time formatting.
219: */
220: public synchronized void setTimeFormat(final String pattern) {
221: if (pattern == null) {
222: timeFormat = null;
223: } else if (timeFormat == null) {
224: timeFormat = new SimpleDateFormat(pattern);
225: timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
226: } else {
227: timeFormat.applyPattern(pattern);
228: }
229: }
230:
231: /**
232: * Returns the format for displaying ellapsed time. This is the pattern specified
233: * to the last call to {@link #setTimeFormat}, or the patten specified in the
234: * {@code org.geotools.MonolineFormater.time} property in the
235: * {@code jre/lib/logging.properties} file.
236: *
237: * @return The time pattern, or {@code null} if time is not formatted.
238: */
239: public synchronized String getTimeFormat() {
240: return (timeFormat != null) ? timeFormat.toPattern() : null;
241: }
242:
243: /**
244: * Sets the format for displaying the source. The pattern may be one of the following:
245: *
246: * <code>"none"</code>,
247: * <code>"logger:short"</code>, <code>"class:short"</code>,
248: * <code>"logger:long"</code> or <code>"class:long"</code>.
249: *
250: * The difference between a {@code null} and <code>"none"</code> is that {@code null}
251: * may be replaced by a default value, while <code>"none"</code> means that the user
252: * explicitly requested no source.
253: *
254: * @param format The format for displaying the source.
255: */
256: public synchronized void setSourceFormat(String format) {
257: if (format != null) {
258: format = format.trim().toLowerCase();
259: }
260: for (int i = 0; i < FORMAT_LABELS.length; i++) {
261: if (Utilities.equals(FORMAT_LABELS[i], format)) {
262: sourceFormat = i;
263: return;
264: }
265: }
266: throw new IllegalArgumentException(format);
267: }
268:
269: /**
270: * Returns the format for displaying the source. This is the pattern specified
271: * to the last call to {@link #setSourceFormat}, or the patten specified in the
272: * {@code org.geotools.MonolineFormater.source} property in the
273: * {@code jre/lib/logging.properties} file.
274: *
275: * @return The source pattern, or {@code null} if source is not formatted.
276: */
277: public String getSourceFormat() {
278: return FORMAT_LABELS[sourceFormat];
279: }
280:
281: /**
282: * Formats the given log record and return the formatted string.
283: *
284: * @param record the log record to be formatted.
285: * @return a formatted log record
286: */
287: public synchronized String format(final LogRecord record) {
288: buffer.setLength(PREFIX.length());
289: /*
290: * Formats the time (e.g. "00:00:12.365"). The time pattern can be set
291: * either programmatically with a call to setTimeFormat(String), or in
292: * the logging.properties file with the
293: * "org.geotools.util.logging.MonolineFormatter.time" property.
294: */
295: if (timeFormat != null) {
296: Date time = new Date(Math.max(0, record.getMillis()
297: - startMillis));
298: timeFormat.format(time, buffer, new FieldPosition(0));
299: buffer.append(' ');
300: }
301: /*
302: * Formats the level (e.g. "FINE"). We do not provide
303: * the option to turn level off for now.
304: */
305: if (true) {
306: int offset = buffer.length();
307: buffer.append(record.getLevel().getLocalizedName());
308: offset = buffer.length() - offset;
309: buffer.append(Utilities.spaces(margin - offset));
310: }
311: /*
312: * Adds the source. It may be either the source logger or the source class name.
313: */
314: String logger = record.getLoggerName();
315: String classname = record.getSourceClassName();
316: switch (sourceFormat) {
317: case LOGGER_SHORT: {
318: int pos = logger.lastIndexOf('.');
319: if (pos >= 0) {
320: logger = logger.substring(pos);
321: }
322: // fall through
323: }
324: case LOGGER_LONG: {
325: buffer.append(' ');
326: buffer.append(logger);
327: break;
328: }
329: case CLASS_SHORT: {
330: int dot = classname.lastIndexOf('.');
331: if (dot >= 0) {
332: classname = classname.substring(dot + 1);
333: }
334: classname = classname.replace('$', '.');
335: // fall through
336: }
337: case CLASS_LONG: {
338: buffer.append(' ');
339: buffer.append(classname);
340: break;
341: }
342: }
343: buffer.append(SUFFIX);
344: /*
345: * Now format the message. We will use a line separator made of the
346: * usual EOL ("\r", "\n", or "\r\n", which is plateform specific)
347: * following by some amout of space in order to align message body.
348: */
349: final int margin = buffer.length();
350: assert margin >= this .margin;
351: if (bodyLineSeparator.length() != lineSeparator.length()
352: + margin) {
353: bodyLineSeparator = lineSeparator
354: + Utilities.spaces(margin);
355: }
356: try {
357: writer.setLineSeparator(bodyLineSeparator);
358: writer.write(String.valueOf(formatMessage(record)));
359: writer.setLineSeparator(lineSeparator);
360: writer.write('\n');
361: writer.flush();
362: } catch (IOException exception) {
363: // Should never happen, since we are writting into a StringBuffer.
364: throw new AssertionError(exception);
365: }
366: return buffer.toString();
367: }
368:
369: /**
370: * Setup a {@code MonolineFormatter} for the specified logger and its children. This method
371: * search for all instances of {@link ConsoleHandler} using the {@link SimpleFormatter}. If
372: * such instances are found, they are replaced by a single instance of {@code MonolineFormatter}.
373: * If no such {@link ConsoleHandler} are found, then a new one is created with this
374: * {@code MonolineFormatter}.
375: * <p>
376: * In addition, this method can set the handler levels. If the level is non-null, then all
377: * {@link Handler}s using the monoline formatter will be set to the specified level. This
378: * is provided for convenience, but non-null {@code level} argument should be avoided as
379: * much as possible because it overrides user's level settings. A user trying to configure
380: * his logging properties file may find confusing to see his setting ignored.
381: *
382: * @param logger The base logger to apply the change on.
383: * @param level The desired level, or {@code null} if no level should be set.
384: * @return The registered {@code MonolineFormatter} (never {@code null}).
385: * The formatter output can be configured using the {@link #setTimeFormat}
386: * and {@link #setSourceFormat} methods.
387: */
388: public static MonolineFormatter configureConsoleHandler(
389: final Logger logger, final Level level) {
390: MonolineFormatter monoline = null;
391: boolean foundConsoleHandler = false;
392: Handler[] handlers = logger.getHandlers();
393: for (int i = 0; i < handlers.length; i++) {
394: final Handler handler = handlers[i];
395: if (handler.getClass().equals(ConsoleHandler.class)) {
396: foundConsoleHandler = true;
397: final Formatter formatter = handler.getFormatter();
398: if (formatter instanceof MonolineFormatter) {
399: /*
400: * A MonolineFormatter already existed. Sets the level only for the first
401: * instance (only one instance should exists anyway) for consistency with
402: * the fact that this method returns only one MonolineFormatter for further
403: * configuration.
404: */
405: if (monoline == null) {
406: monoline = (MonolineFormatter) formatter;
407: if (level != null) {
408: handler.setLevel(level);
409: }
410: }
411: } else if (formatter.getClass().equals(
412: SimpleFormatter.class)) {
413: /*
414: * A ConsoleHandler using the SimpleFormatter has been found. Replaces
415: * the SimpleFormatter by MonolineFormatter, creating it if necessary.
416: * If the handler setting fail with an exception, then we will continue
417: * to use the old J2SE handler instead.
418: */
419: if (monoline == null) {
420: monoline = new MonolineFormatter();
421: }
422: try {
423: handler.setFormatter(monoline);
424: if (level != null) {
425: handler.setLevel(level);
426: }
427: } catch (SecurityException exception) {
428: unexpectedException(exception);
429: }
430: }
431: }
432: }
433: /*
434: * If the logger uses parent handlers, copy them to the logger that we are initializing,
435: * because we will not use parent handlers anymore at the end of this method.
436: */
437: for (Logger parent = logger; parent.getUseParentHandlers();) {
438: parent = parent.getParent();
439: if (parent == null) {
440: break;
441: }
442: handlers = parent.getHandlers();
443: for (int i = 0; i < handlers.length; i++) {
444: Handler handler = handlers[i];
445: if (handler.getClass().equals(ConsoleHandler.class)) {
446: if (!foundConsoleHandler) {
447: // We have already set a ConsoleHandler and we don't want a second one.
448: continue;
449: }
450: foundConsoleHandler = true;
451: final Formatter formatter = handler.getFormatter();
452: if (formatter.getClass().equals(
453: SimpleFormatter.class)) {
454: monoline = addHandler(logger, level);
455: continue;
456: }
457: }
458: logger.addHandler(handler);
459: }
460: }
461: logger.setUseParentHandlers(false);
462: if (!foundConsoleHandler) {
463: monoline = addHandler(logger, level);
464: }
465: return monoline;
466: }
467:
468: /**
469: * Adds to the specified logger a {@link Handler} using a {@code MonolineFormatter}
470: * set at the specified level. The formatter is returned for convenience.
471: */
472: private static MonolineFormatter addHandler(final Logger logger,
473: final Level level) {
474: final MonolineFormatter monoline = new MonolineFormatter();
475: try {
476: final Handler handler = new ConsoleHandler();
477: handler.setFormatter(monoline);
478: if (level != null) {
479: handler.setLevel(level);
480: }
481: logger.addHandler(handler);
482: } catch (SecurityException exception) {
483: unexpectedException(exception);
484: /*
485: * Returns without any change to the J2SE configuration. Note that the returned
486: * MonolineFormatter is really a dummy one, since we failed to register it. It
487: * will not prevent to program to work; just produces different logging outputs.
488: */
489: }
490: return monoline;
491: }
492:
493: /**
494: * Invoked when an error occurs during the initialization.
495: */
496: private static void unexpectedException(final Exception exception) {
497: Logging.unexpectedException("org.geotools.util.logging",
498: MonolineFormatter.class, "configureConsoleHandler",
499: exception);
500: }
501: }
|