001: /*
002: * Copyright (C) The Apache Software Foundation. All rights reserved.
003: *
004: * This software is published under the terms of the Apache Software License
005: * version 1.1, a copy of which has been included with this distribution in
006: * the LICENSE file.
007: */
008: package org.jivesoftware.util.log.format;
009:
010: import org.jivesoftware.util.FastDateFormat;
011: import org.jivesoftware.util.log.ContextMap;
012: import org.jivesoftware.util.log.LogEvent;
013: import org.jivesoftware.util.log.Priority;
014:
015: import java.io.StringWriter;
016: import java.util.Date;
017: import java.util.Stack;
018:
019: /**
020: * This formater formats the LogEvents according to a input pattern
021: * string.
022: * <p/>
023: * The format of each pattern element can be %[+|-][#[.#]]{field:subformat}.
024: * </p>
025: * <ul>
026: * <li>The +|- indicates left or right justify.
027: * </li>
028: * <li>The #.# indicates the minimum and maximum size of output.<br>
029: * You may omit the values and the field will be formatted without size
030: * restriction.<br>
031: * You may specify '#', or '#.' to define an minimum size, only.</br>
032: * You may specify '.#' to define an maximum size only.
033: * </li>
034: * <li>
035: * 'field' indicates which field is to be output and must be one of
036: * properties of LogEvent.<br>
037: * Currently following fields are supported:
038: * <dl>
039: * <dt>category</dt>
040: * <dd>Category value of the logging event.</dd>
041: * <dt>context</dt>
042: * <dd>Context value of the logging event.</dd>
043: * <dt>message</dt>
044: * <dd>Message value of the logging event.</dd>
045: * <dt>time</dt>
046: * <dd>Time value of the logging event.</dd>
047: * <dt>rtime</dt>
048: * <dd>Relative time value of the logging event.</dd>
049: * <dt>throwable</dt>
050: * <dd>Throwable value of the logging event.</dd>
051: * <dt>priority</dt>
052: * <dd>Priority value of the logging event.</dd>
053: * </dl>
054: * </li>
055: * <li>'subformat' indicates a particular subformat and is currently only used
056: * for category context to specify the context map parameter name.
057: * </li>
058: * </ul>
059: * <p>A simple example of a typical PatternFormatter format:
060: * </p>
061: * <pre><code>%{time} %5.5{priority}[%-10.10{category}]: %{message}
062: * </pre></code>
063: * <p/>
064: * This format string will format a log event printing first time value of
065: * of log event with out size restriction, next priority with minum and maximum size 5,
066: * next category right justified having minmum and maximum size of 10,
067: * at last the message of the log event without size restriction.
068: * </p>
069: * <p>A formatted sample message of the above pattern format:
070: * </p>
071: * <pre><code>1000928827905 DEBUG [ junit]: Sample message
072: * </pre><code>
073: *
074: * @author <a href="mailto:peter@apache.org">Peter Donald</a>
075: * @author <a href="mailto:sylvain@apache.org">Sylvain Wallez</a>
076: * @version CVS $Revision: 1747 $ $Date: 2005-08-04 14:36:36 -0700 (Thu, 04 Aug 2005) $
077: */
078: public class PatternFormatter implements Formatter {
079: private final static int TYPE_TEXT = 1;
080: private final static int TYPE_CATEGORY = 2;
081: private final static int TYPE_CONTEXT = 3;
082: private final static int TYPE_MESSAGE = 4;
083: private final static int TYPE_TIME = 5;
084: private final static int TYPE_RELATIVE_TIME = 6;
085: private final static int TYPE_THROWABLE = 7;
086: private final static int TYPE_PRIORITY = 8;
087:
088: /**
089: * The maximum value used for TYPEs. Subclasses can define their own TYPEs
090: * starting at <code>MAX_TYPE + 1</code>.
091: */
092: protected final static int MAX_TYPE = TYPE_PRIORITY;
093:
094: private final static String TYPE_CATEGORY_STR = "category";
095: private final static String TYPE_CONTEXT_STR = "context";
096: private final static String TYPE_MESSAGE_STR = "message";
097: private final static String TYPE_TIME_STR = "time";
098: private final static String TYPE_RELATIVE_TIME_STR = "rtime";
099: private final static String TYPE_THROWABLE_STR = "throwable";
100: private final static String TYPE_PRIORITY_STR = "priority";
101:
102: private final static String SPACE_16 = " ";
103: private final static String SPACE_8 = " ";
104: private final static String SPACE_4 = " ";
105: private final static String SPACE_2 = " ";
106: private final static String SPACE_1 = " ";
107:
108: private final static String EOL = System.getProperty(
109: "line.separator", "\n");
110:
111: protected static class PatternRun {
112: public String m_data;
113: public boolean m_rightJustify;
114: public int m_minSize;
115: public int m_maxSize;
116: public int m_type;
117: public String m_format;
118: }
119:
120: private PatternRun m_formatSpecification[];
121:
122: private FastDateFormat m_simpleDateFormat;
123: private final Date m_date = new Date();
124:
125: /**
126: * @deprecated Use constructor PatternFormatter(String pattern) as this does not
127: * correctly initialize object
128: */
129: public PatternFormatter() {
130: }
131:
132: public PatternFormatter(final String pattern) {
133: parse(pattern);
134: }
135:
136: /**
137: * Extract and build a pattern from input string.
138: *
139: * @param stack the stack on which to place patterns
140: * @param pattern the input string
141: * @param index the start of pattern run
142: * @return the number of characters in pattern run
143: */
144: private int addPatternRun(final Stack stack, final char pattern[],
145: int index) {
146: final PatternRun run = new PatternRun();
147: final int start = index++;
148:
149: //first check for a +|- sign
150: if ('+' == pattern[index])
151: index++;
152: else if ('-' == pattern[index]) {
153: run.m_rightJustify = true;
154: index++;
155: }
156:
157: if (Character.isDigit(pattern[index])) {
158: int total = 0;
159: while (Character.isDigit(pattern[index])) {
160: total = total * 10 + (pattern[index] - '0');
161: index++;
162: }
163: run.m_minSize = total;
164: }
165:
166: //check for . sign indicating a maximum is to follow
167: if (index < pattern.length && '.' == pattern[index]) {
168: index++;
169:
170: if (Character.isDigit(pattern[index])) {
171: int total = 0;
172: while (Character.isDigit(pattern[index])) {
173: total = total * 10 + (pattern[index] - '0');
174: index++;
175: }
176: run.m_maxSize = total;
177: }
178: }
179:
180: if (index >= pattern.length || '{' != pattern[index]) {
181: throw new IllegalArgumentException(
182: "Badly formed pattern at character " + index);
183: }
184:
185: int typeStart = index;
186:
187: while (index < pattern.length && pattern[index] != ':'
188: && pattern[index] != '}') {
189: index++;
190: }
191:
192: int typeEnd = index - 1;
193:
194: final String type = new String(pattern, typeStart + 1, typeEnd
195: - typeStart);
196:
197: run.m_type = getTypeIdFor(type);
198:
199: if (index < pattern.length && pattern[index] == ':') {
200: index++;
201: while (index < pattern.length && pattern[index] != '}')
202: index++;
203:
204: final int length = index - typeEnd - 2;
205:
206: if (0 != length) {
207: run.m_format = new String(pattern, typeEnd + 2, length);
208: }
209: }
210:
211: if (index >= pattern.length || '}' != pattern[index]) {
212: throw new IllegalArgumentException(
213: "Unterminated type in pattern at character "
214: + index);
215: }
216:
217: index++;
218:
219: stack.push(run);
220:
221: return index - start;
222: }
223:
224: /**
225: * Extract and build a text run from input string.
226: * It does special handling of '\n' and '\t' replaceing
227: * them with newline and tab.
228: *
229: * @param stack the stack on which to place runs
230: * @param pattern the input string
231: * @param index the start of the text run
232: * @return the number of characters in run
233: */
234: private int addTextRun(final Stack stack, final char pattern[],
235: int index) {
236: final PatternRun run = new PatternRun();
237: final int start = index;
238: boolean escapeMode = false;
239:
240: if ('%' == pattern[index])
241: index++;
242:
243: final StringBuffer sb = new StringBuffer();
244:
245: while (index < pattern.length && pattern[index] != '%') {
246: if (escapeMode) {
247: if ('n' == pattern[index])
248: sb.append(EOL);
249: else if ('t' == pattern[index])
250: sb.append('\t');
251: else
252: sb.append(pattern[index]);
253: escapeMode = false;
254: } else if ('\\' == pattern[index])
255: escapeMode = true;
256: else
257: sb.append(pattern[index]);
258: index++;
259: }
260:
261: run.m_data = sb.toString();
262: run.m_type = TYPE_TEXT;
263:
264: stack.push(run);
265:
266: return index - start;
267: }
268:
269: /**
270: * Utility to append a string to buffer given certain constraints.
271: *
272: * @param sb the StringBuffer
273: * @param minSize the minimum size of output (0 to ignore)
274: * @param maxSize the maximum size of output (0 to ignore)
275: * @param rightJustify true if the string is to be right justified in it's box.
276: * @param output the input string
277: */
278: private void append(final StringBuffer sb, final int minSize,
279: final int maxSize, final boolean rightJustify,
280: final String output) {
281: final int size = output.length();
282:
283: if (size < minSize) {
284: //assert( minSize > 0 );
285: if (rightJustify) {
286: appendWhiteSpace(sb, minSize - size);
287: sb.append(output);
288: } else {
289: sb.append(output);
290: appendWhiteSpace(sb, minSize - size);
291: }
292: } else if (maxSize > 0 && maxSize < size) {
293: if (rightJustify) {
294: sb.append(output.substring(size - maxSize));
295: } else {
296: sb.append(output.substring(0, maxSize));
297: }
298: } else {
299: sb.append(output);
300: }
301: }
302:
303: /**
304: * Append a certain number of whitespace characters to a StringBuffer.
305: *
306: * @param sb the StringBuffer
307: * @param length the number of spaces to append
308: */
309: private void appendWhiteSpace(final StringBuffer sb, int length) {
310: while (length >= 16) {
311: sb.append(SPACE_16);
312: length -= 16;
313: }
314:
315: if (length >= 8) {
316: sb.append(SPACE_8);
317: length -= 8;
318: }
319:
320: if (length >= 4) {
321: sb.append(SPACE_4);
322: length -= 4;
323: }
324:
325: if (length >= 2) {
326: sb.append(SPACE_2);
327: length -= 2;
328: }
329:
330: if (length >= 1) {
331: sb.append(SPACE_1);
332: length -= 1;
333: }
334: }
335:
336: /**
337: * Format the event according to the pattern.
338: *
339: * @param event the event
340: * @return the formatted output
341: */
342: public String format(final LogEvent event) {
343: final StringBuffer sb = new StringBuffer();
344:
345: for (int i = 0; i < m_formatSpecification.length; i++) {
346: final PatternRun run = m_formatSpecification[i];
347:
348: //treat text differently as it doesn't need min/max padding
349: if (run.m_type == TYPE_TEXT) {
350: sb.append(run.m_data);
351: } else {
352: final String data = formatPatternRun(event, run);
353:
354: if (null != data) {
355: append(sb, run.m_minSize, run.m_maxSize,
356: run.m_rightJustify, data);
357: }
358: }
359: }
360:
361: return sb.toString();
362: }
363:
364: /**
365: * Formats a single pattern run (can be extended in subclasses).
366: *
367: * @param run the pattern run to format.
368: * @return the formatted result.
369: */
370: protected String formatPatternRun(final LogEvent event,
371: final PatternRun run) {
372: switch (run.m_type) {
373: case TYPE_RELATIVE_TIME:
374: return getRTime(event.getRelativeTime(), run.m_format);
375: case TYPE_TIME:
376: return getTime(event.getTime(), run.m_format);
377: case TYPE_THROWABLE:
378: return getStackTrace(event.getThrowable(), run.m_format);
379: case TYPE_MESSAGE:
380: return getMessage(event.getMessage(), run.m_format);
381: case TYPE_CATEGORY:
382: return getCategory(event.getCategory(), run.m_format);
383: case TYPE_PRIORITY:
384: return getPriority(event.getPriority(), run.m_format);
385:
386: case TYPE_CONTEXT:
387: // if( null == run.m_format ||
388: // run.m_format.startsWith( "stack" ) )
389: // {
390: // //Print a warning out to stderr here
391: // //to indicate you are using a deprecated feature?
392: // return getContext( event.getContextStack(), run.m_format );
393: // }
394: // else
395: // {
396: return getContextMap(event.getContextMap(), run.m_format);
397: // }
398:
399: default:
400: throw new IllegalStateException(
401: "Unknown Pattern specification." + run.m_type);
402: }
403: }
404:
405: /**
406: * Utility method to format category.
407: *
408: * @param category the category string
409: * @param format ancilliary format parameter - allowed to be null
410: * @return the formatted string
411: */
412: protected String getCategory(final String category,
413: final String format) {
414: return category;
415: }
416:
417: /**
418: * Get formatted priority string.
419: */
420: protected String getPriority(final Priority priority,
421: final String format) {
422: return priority.getName();
423: }
424:
425: // /**
426: // * Utility method to format context.
427: // *
428: // * @param context the context string
429: // * @param format ancilliary format parameter - allowed to be null
430: // * @return the formatted string
431: // * @deprecated Use getContextStack rather than this method
432: // */
433: // protected String getContext( final ContextStack stack, final String format )
434: // {
435: // return getContextStack( stack, format );
436: // }
437:
438: // /**
439: // * Utility method to format context.
440: // *
441: // * @param context the context string
442: // * @param format ancilliary format parameter - allowed to be null
443: // * @return the formatted string
444: // */
445: // protected String getContextStack( final ContextStack stack, final String format )
446: // {
447: // if( null == stack ) return "";
448: // return stack.toString( Integer.MAX_VALUE );
449: // }
450:
451: /**
452: * Utility method to format context map.
453: *
454: * @param map the context map
455: * @param format ancilliary format parameter - allowed to be null
456: * @return the formatted string
457: */
458: protected String getContextMap(final ContextMap map,
459: final String format) {
460: if (null == map)
461: return "";
462: return map.get(format, "").toString();
463: }
464:
465: /**
466: * Utility method to format message.
467: *
468: * @param message the message string
469: * @param format ancilliary format parameter - allowed to be null
470: * @return the formatted string
471: */
472: protected String getMessage(final String message,
473: final String format) {
474: return message;
475: }
476:
477: /**
478: * Utility method to format stack trace.
479: *
480: * @param throwable the throwable instance
481: * @param format ancilliary format parameter - allowed to be null
482: * @return the formatted string
483: */
484: protected String getStackTrace(final Throwable throwable,
485: final String format) {
486: if (null == throwable)
487: return "";
488: final StringWriter sw = new StringWriter();
489: throwable.printStackTrace(new java.io.PrintWriter(sw));
490: return sw.toString();
491: }
492:
493: /**
494: * Utility method to format relative time.
495: *
496: * @param time the time
497: * @param format ancilliary format parameter - allowed to be null
498: * @return the formatted string
499: */
500: protected String getRTime(final long time, final String format) {
501: return getTime(time, format);
502: }
503:
504: /**
505: * Utility method to format time.
506: *
507: * @param time the time
508: * @param format ancilliary format parameter - allowed to be null
509: * @return the formatted string
510: */
511: protected String getTime(final long time, final String format) {
512: if (null == format) {
513: return Long.toString(time);
514: } else {
515: synchronized (m_date) {
516: if (null == m_simpleDateFormat) {
517: m_simpleDateFormat = FastDateFormat
518: .getInstance(format);
519: }
520: m_date.setTime(time);
521: return m_simpleDateFormat.format(m_date);
522: }
523: }
524: }
525:
526: /**
527: * Retrieve the type-id for a particular string.
528: *
529: * @param type the string
530: * @return the type-id
531: */
532: protected int getTypeIdFor(final String type) {
533: if (type.equalsIgnoreCase(TYPE_CATEGORY_STR))
534: return TYPE_CATEGORY;
535: else if (type.equalsIgnoreCase(TYPE_CONTEXT_STR))
536: return TYPE_CONTEXT;
537: else if (type.equalsIgnoreCase(TYPE_MESSAGE_STR))
538: return TYPE_MESSAGE;
539: else if (type.equalsIgnoreCase(TYPE_PRIORITY_STR))
540: return TYPE_PRIORITY;
541: else if (type.equalsIgnoreCase(TYPE_TIME_STR))
542: return TYPE_TIME;
543: else if (type.equalsIgnoreCase(TYPE_RELATIVE_TIME_STR))
544: return TYPE_RELATIVE_TIME;
545: else if (type.equalsIgnoreCase(TYPE_THROWABLE_STR)) {
546: return TYPE_THROWABLE;
547: } else {
548: throw new IllegalArgumentException(
549: "Unknown Type in pattern - " + type);
550: }
551: }
552:
553: /**
554: * Parse the input pattern and build internal data structures.
555: *
556: * @param patternString the pattern
557: */
558: protected final void parse(final String patternString) {
559: final Stack stack = new Stack();
560: final int size = patternString.length();
561: final char pattern[] = new char[size];
562: int index = 0;
563:
564: patternString.getChars(0, size, pattern, 0);
565:
566: while (index < size) {
567: if (pattern[index] == '%'
568: && !(index != size - 1 && pattern[index + 1] == '%')) {
569: index += addPatternRun(stack, pattern, index);
570: } else {
571: index += addTextRun(stack, pattern, index);
572: }
573: }
574:
575: final int elementCount = stack.size();
576:
577: m_formatSpecification = new PatternRun[elementCount];
578:
579: for (int i = 0; i < elementCount; i++) {
580: m_formatSpecification[i] = (PatternRun) stack.elementAt(i);
581: }
582: }
583:
584: /**
585: * Set the string description that the format is extracted from.
586: *
587: * @param format the string format
588: * @deprecated Parse format in via constructor rather than use this method
589: */
590: public void setFormat(final String format) {
591: parse(format);
592: }
593: }
|