/* * $Id$ * * Copyright 2006, The jCoderZ.org Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the jCoderZ.org Project nor the names of * its contributors may be used to endorse or promote products * derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jcoderz.commons; import java.io.Serializable; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.Map.Entry; import java.util.logging.Logger; import org.jcoderz.commons.util.ThrowableUtil; /** * Implements code common to all Exceptions. * <p> * The two base exceptions {@link org.jcoderz.commons.BaseException} * and {@link org.jcoderz.commons.BaseRuntimeException} and the * {@link org.jcoderz.commons.LogEvent} use a object of this class * as a member and delegate all common calls to this member. * </p> * <p> * This class also implements the special * {@link org.jcoderz.commons.Loggable} that allows to add named * parameters and to get a more detailed logging, with an assigned * error message. * </p> * <p> * The error response id is used to mark log entries with an unique id. This id * is also returned to the client (caller). If the client reports the error * response id it can be used to find a specific log entries more quickly. If * the nested exception has already an error response id, it is re-used for this * exception and will not get a new one. * </p> * Functionality provided by this class is: * <ul> * <li>Create a unique <code>ERROR_RESPONSE_ID</code> parameter for each * instance.</li> * <li>Create a message that contains all parameters for a full informational * toString() output.</li> * <li>Handles nested exceptions so that all information is available and * avoids duplicate information for the JDK1.4 environment which supports nested * exceptions itself.</li> * <li>Holds the constant names for commonly used exception parameters.</li> * </ul> * * @author Andreas Mandel */ public class LoggableImpl implements Serializable, Loggable { /** Name of this class. */ public static final String CLASSNAME = LoggableImpl.class.getName(); /** Logger used for this class. */ public static final Logger logger = Logger.getLogger(CLASSNAME); /** Key used for the log message info parameter object. */ public static final String MESSAGE_INFO_PARAMETER_NAME = "_MESSAGE_INFO"; /** Key used for the error response id added to the loggable. */ public static final String TRACKING_NUMBER_PARAMETER_NAME = "_TRACKING_NUMBER"; /** Key used for the root cause added to the loggable. */ public static final String CAUSE_PARAMETER_NAME = "_CAUSE"; /** Key used for the thread id parameter object. */ public static final String THREAD_ID_PARAMETER_NAME = "_THREAD_ID"; /** Key used for the thread name parameter object. */ public static final String THREAD_NAME_PARAMETER_NAME = "_THREAD_NAME"; /** Key used for the instance id parameter object. */ public static final String INSTANCE_ID_PARAMETER_NAME = "_INSTANCE_ID"; /** Key used for the node id parameter object. */ public static final String NODE_ID_PARAMETER_NAME = "_NODE_ID"; /** Key used for the event time parameter of the loggable. */ public static final String EVENT_TIME_PARAMETER_NAME = "_TIME"; /** Name of the application of the loggable. */ public static final String APPLICATION_NAME_PARAMETER_NAME = "_APPLICATION"; /** Name of the group of the loggable. */ public static final String GROUP_NAME_PARAMETER_NAME = "_GROUP"; /** Context parameter values prefix. */ public static final String CONTEXT_PARAMETER_PREFIX = "CTX~"; /** This nodes id. */ public static final String NODE_ID = getStaticNodeId(); /** Id for this instance. */ public static final String INSTANCE_ID; /** Virtual thread Id generated for this thread. */ public static final ThreadIdHolder THREAD_ID_GENERATOR = new ThreadIdHolder(); static final long serialVersionUID = 1; /** * Maximum number of steps to get the cause of an exception, * until we stop climbing up the cause chain. */ private static final int MAX_EXCEPTION_CHAIN_UP = 20; /** * In the first step use bea specific instance name, which is set as system * property with the following name. * TODO: Make this bea-independent, requires entry in logging.properties, * system property, or something alike. */ private static final String INSTANCE_NAME_PROPERTY = "weblogic.Name"; /** Random generator to create pseudo unique Ids for each loggable. */ private static final Random RANDOM_ID_GENERATOR = new Random(); private static final String DUMMY_INSTANCE_ID = "P" + Integer.toHexString(RANDOM_ID_GENERATOR.nextInt()); private static final String DUMMY_NODE_ID = "127.0.0.1"; /** * list of parameter for this exception The list is not thread save! */ private final Map mParameters = new HashMap(); /** * Remember the ERROR_RESPONSE_ID. Intention is to log this id with the * exception and pass the Id to the recipient. It should be really easy to * find the exception in the log. */ private final String mTrackingNumber; /** The error ID for this loggable */ private final LogMessageInfo mLogMessageInfo; /** The point in time when this event occurred. */ private final long mEventTime; /** The node id. */ private final String mNodeId; /** The id for this instance id. */ private final String mInstanceId; /** The thread id. */ private final long mThreadId; /** The thread name. */ private final String mThreadName; /** * The Throwable that caused this loggable. * Should be equal to mOuter.getCause() */ private Throwable mCause; /** The outer exception, where this loggable belongs to. */ private Loggable mOuter; private String mClassName = null; private String mMethodName = null; static { INSTANCE_ID = getStaticInstanceId(); } /** * Create this loggable provide the 'Loggable' functionality for the * given outer loggable. * @param outer the the outer loggable. * @param errorId the static LogMessageInfo for this Loggable. */ public LoggableImpl (Loggable outer, LogMessageInfo errorId) { this(outer, errorId, THREAD_ID_GENERATOR.getThreadId(), Thread.currentThread().getName(), INSTANCE_ID, NODE_ID); } /** * Create this loggable provide the 'Loggable' functionality for the * given outer loggable with an initial cause. * @param outer the the outer loggable. * @param errorId the static LogMessageInfo for this Loggable. * @param cause the cause of the outer. */ public LoggableImpl (Loggable outer, LogMessageInfo errorId, Throwable cause) { this(outer, errorId, THREAD_ID_GENERATOR.getThreadId(), Thread.currentThread().getName(), INSTANCE_ID, NODE_ID, cause); } /** * Create this loggable provide the 'Loggable' functionality for the * given outer loggable with the given dynamic parameters. * @param outer the the outer loggable. * @param errorId the static LogMessageInfo for this Loggable. * @param threadId the threadId to be set. * @param threadName the threadName to be set. * @param instanceId the instanceId to be set. * @param nodeId the nodeId to be set. */ public LoggableImpl (Loggable outer, LogMessageInfo errorId, long threadId, String threadName, String instanceId, String nodeId) { mEventTime = System.currentTimeMillis(); mTrackingNumber = Integer.toHexString(RANDOM_ID_GENERATOR.nextInt()); mLogMessageInfo = errorId; mThreadId = threadId; mThreadName = threadName; mInstanceId = instanceId; mNodeId = nodeId; mOuter = outer; initInternalParameters(); initThreadContextParameters(); } /** * Create this loggable provide the 'Loggable' functionality for the * given outer loggable with the given dynamic parameters and an * initial cause.. * @param outer the the outer loggable. * @param errorId the static LogMessageInfo for this Loggable. * @param threadId the threadId to be set. * @param threadName the threadName to be set. * @param instanceId the instanceId to be set. * @param nodeId the nodeId to be set. * @param cause the cause of the outer. */ public LoggableImpl (Loggable outer, LogMessageInfo errorId, long threadId, String threadName, String instanceId, String nodeId, Throwable cause) { mEventTime = System.currentTimeMillis(); ThrowableUtil.fixChaining(cause); Throwable thr = cause; int depth = 0; while (thr != null && !(thr instanceof Loggable) && depth < MAX_EXCEPTION_CHAIN_UP) { thr = thr.getCause(); depth++; } if (thr instanceof Loggable) { mTrackingNumber = ((Loggable) thr).getTrackingNumber(); } else { mTrackingNumber = Integer.toHexString(RANDOM_ID_GENERATOR.nextInt()); } mLogMessageInfo = errorId; mThreadId = threadId; mThreadName = threadName; mInstanceId = instanceId; mNodeId = nodeId; mOuter = outer; initCause(cause); initInternalParameters(); initThreadContextParameters(); } /** * Sets the cause of this throwable. * * This method should be called after the call to the * {@link Throwable#initCause(Throwable)} for the case * the super call fails. * * @param cause the cause of this Exception. */ public final void initCause (Throwable cause) { mCause = cause; addParameter(CAUSE_PARAMETER_NAME, cause); ThrowableUtil.fixChaining(cause); ThrowableUtil.collectNestedData(this); } /** * Adds a new named parameter. The parameter is added at the end of the list * of parameters. The same <code>name</code> might occur several times. * * @param name the name of the parameter. * @param value The value of the parameter */ public final void addParameter (String name, Serializable value) { List values = (List) mParameters.get(name); if (values == null) { values = new ArrayList(); mParameters.put(name, values); } values.add(value); } /** {@inheritDoc} */ public List getParameter (String name) { final List values = (List) mParameters.get(name); final List result; if (values != null) { result = Collections.unmodifiableList(values); } else { result = Collections.EMPTY_LIST; } return result; } /** {@inheritDoc} */ public Set getParameterNames () { return Collections.unmodifiableSet(mParameters.keySet()); } /** {@inheritDoc} */ public final LogMessageInfo getLogMessageInfo () { return mLogMessageInfo; } /** {@inheritDoc} */ public final String getTrackingNumber () { return mTrackingNumber; } /** {@inheritDoc} */ public final long getEventTime () { return mEventTime; } /** {@inheritDoc} */ public final String getNodeId () { return mNodeId; } /** {@inheritDoc} */ public final String getInstanceId () { return mInstanceId; } /** {@inheritDoc} */ public final long getThreadId () { return mThreadId; } /** {@inheritDoc} */ public final String getThreadName () { return mThreadName; } /** {@inheritDoc} */ public Throwable getCause () { return mCause; } /** {@inheritDoc} */ public void log () { getSource(); logger.logp(getLogMessageInfo().getLogLevel(), mClassName, mMethodName, getMessage(), mOuter); } /** {@inheritDoc} */ public String getMessage () { return getLogMessageInfo().formatMessage( mParameters, new StringBuffer()).toString(); } /** {@inheritDoc} */ public String getSourceClass () { getSource(); return mClassName; } /** {@inheritDoc} */ public String getSourceMethod () { getSource(); return mMethodName; } /** {@inheritDoc} */ public String toString () { final StringBuffer sb = new StringBuffer(); if (mOuter != null) { sb.append(mOuter.getClass().getName()); } else { sb.append(getClass().getName()); } sb.append(": "); getLogMessageInfo().formatMessage(mParameters, sb); return sb.toString(); } /** {@inheritDoc} */ public String toDetailedString () { final StringBuffer sb = new StringBuffer(); LoggableImpl.appendParameters(sb, this); Throwable cause = null; if (mOuter != null) { cause = mOuter.getCause(); } if (cause == null) { cause = getCause(); } // add parameters of nested chain int depth = 0; while (cause != null && depth < MAX_EXCEPTION_CHAIN_UP) { if (cause instanceof Loggable) { sb.append("\nCaused by: "); LoggableImpl.appendParameters(sb, (Loggable) cause); break; } cause = cause.getCause(); depth++; } cause = null; if (mOuter != null) { cause = mOuter.getCause(); } if (cause == null) { cause = getCause(); } if (cause != null) { sb.append('\n'); sb.append(ThrowableUtil.toString(cause)); } return sb.toString(); } private void initInternalParameters () { addParameter(MESSAGE_INFO_PARAMETER_NAME, mLogMessageInfo); addParameter(TRACKING_NUMBER_PARAMETER_NAME, mTrackingNumber); addParameter(EVENT_TIME_PARAMETER_NAME, new Long(mEventTime)); addParameter(THREAD_ID_PARAMETER_NAME, new Long(mThreadId)); addParameter(THREAD_NAME_PARAMETER_NAME, mThreadName); addParameter(INSTANCE_ID_PARAMETER_NAME, mInstanceId); addParameter(NODE_ID_PARAMETER_NAME, mNodeId); addParameter(APPLICATION_NAME_PARAMETER_NAME, mLogMessageInfo.getAppName()); addParameter(GROUP_NAME_PARAMETER_NAME, mLogMessageInfo.getGroupName()); } private final void initThreadContextParameters () { final Iterator i = LogThreadContext.get().entrySet().iterator(); while (i.hasNext()) { final Entry entry = (Entry) i.next(); addParameter( CONTEXT_PARAMETER_PREFIX + entry.getKey(), String.valueOf(entry.getValue())); } } private final void getSource () { // not analyzed yet. if (mMethodName == null || mClassName == null) { final StackTraceElement[] stack = new Throwable().getStackTrace(); // First, search back to a method in the Logger class. int ix = 0; boolean found = false; while (ix < stack.length) { final StackTraceElement frame = stack[ix]; final String cname = frame.getClassName(); if (cname.equals(CLASSNAME)) { found = true; } else if (found) { break; } ix++; } // Now search for the first frame before the "LoggableImpl" class or // LogMessageInfo class. while (ix < stack.length) { final StackTraceElement frame = stack[ix]; try { final String cname = frame.getClassName(); final Class clazz = Class.forName(cname); if (! (Loggable.class.isAssignableFrom(clazz) || LogMessageInfo.class.isAssignableFrom(clazz))) { // We've found the relevant frame. setMethodAndClass(frame); break; } } catch (ClassNotFoundException e) { setMethodAndClass(frame); break; } ix++; } } } private void setMethodAndClass (final StackTraceElement frame) { mClassName = frame.getClassName(); mMethodName = frame.getMethodName(); final String fileName = frame.getFileName(); if (fileName != null) { final int lineNumber = frame.getLineNumber(); if (lineNumber >= 0) { mMethodName = frame.getMethodName() + "(" + fileName + ":" + lineNumber + ")"; } else { mMethodName = frame.getMethodName() + "(" + fileName + ")"; } } else if (frame.getMethodName().indexOf('(') < 0) { mMethodName = frame.getMethodName() + "()"; } else { mMethodName = frame.getMethodName(); } } private static void appendParameters (StringBuffer sb, Loggable loggable) { sb.append(loggable.toString()); final Object[] params = loggable.getParameterNames().toArray(); Arrays.sort(params); final Iterator/*<String>*/ parameterNames = Arrays.asList(params).iterator(); while (parameterNames.hasNext()) { final String parameterName = (String) parameterNames.next(); sb.append("\n\t"); sb.append(parameterName); sb.append(": \t"); sb.append(loggable.getParameter(parameterName)); } } private static String getStaticNodeId () { String nodeId = DUMMY_NODE_ID; try { nodeId = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { System.err.println("Error retrieving inet address of local host, " + "setting " + DUMMY_NODE_ID + " as node id"); } return nodeId; } private static String getStaticInstanceId () { return System.getProperty(INSTANCE_NAME_PROPERTY, DUMMY_INSTANCE_ID); } private static class ThreadIdHolder extends ThreadLocal { private static final long INITIAL_THREAD_ID = 10L; private static long sNextThreadId = INITIAL_THREAD_ID; protected Object initialValue () { return new Long(sNextThreadId++); } long getThreadId () { return ((Long) get()).longValue(); } } }