/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2002
* Copyright by ESO (in the framework of the ALMA collaboration),
* All rights reserved
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
package alma.acs.exceptions;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.omg.CORBA.UserException;
import alma.ACSErr.ACSException;
import alma.ACSErr.ErrorTrace;
import alma.ACSErr.NameValue;
import alma.ACSErr.Severity;
import alma.acs.util.UTCUtility;
import alma.acs.classloading.ContextFinder;
/**
* Base class for any checked exceptions defined in ALMA Java code.
* Allows these Java exceptions to be interoperable with the ACS error system,
* i.e. that they can be converted to and from {@link ErrorTrace} or
* {@link ACSException} objects.
* See {@link #AcsJException(ErrorTrace)}, {@link #getErrorTrace}, {@link #getACSException}.
* <p>
* Not to be confused with <code>alma.ACSErr.ACSException</code>,
* which is a CORBA user exception with an <code>alma.ACSErr.ErrorTrace</code> inside.
* <code>ACSException</code> is declared as <code>final</code>,
* so that it's not an option to make our <code>AcsJException</code>
* a subclass of it. (todo check if that's due to OMG spec or JacORB).
* <p>
* For the conversion to and from the CORBA exception types,
* two <code>ErrorTrace</code> properties (i.e. {@link NameValue} objects
* referenced from {@link ErrorTrace#data}) are added:
* <ul>
* <li> <code>javaex.class</code> to store the exception class for later reconstruction
* <li> <code>javaex.msg</code> to store the message text provided in the
* various constructors of <code>Throwable</code> and subclasses.<br>
* TODO check why there is no such field available already in ErrorTrace,
* is there really no unique way of transporting text messages in addition
* to error types and codes??
* </ul>
* <p>
*
* @author hsommer Jun 18, 2003 1:07:38 PM
*/
public abstract class AcsJException extends Exception
{
private static String s_thisHost;
// process name or PID, container name if applicable
private static String s_thisProcess;
// host and process (possibly converted from remote process!)
protected String m_host;
protected String m_process;
// location data (needed if converted back from ErrorTrace,
// in which case we don't have VM StackTrace available)
protected int m_line;
protected String m_method;
protected String m_file;
protected String m_sourceObject;
// thread name or its ID
protected String m_threadName;
protected Severity m_severity;
// additional name-value pairs
protected Properties m_properties;
protected long m_timeMilli;
private boolean m_initialized = false;
public AcsJException()
{
super();
init();
}
public AcsJException(String message)
{
super(message);
init();
}
/**
* Constructor from another exception, with a message.
* Subclass ctors call this ctor even if the message is null,
* to avoid the Throwable#ctor taking the cause.toString() as the message.
* <p>
* If <code>cause</code> is a CORBA exception generated by the ACS error system,
* then its ErrorTrace is automatically converted to a chain of AcsJ-style exceptions.
*
* @param cause the causing exception
* @see CorbaExceptionConverter#convertHiddenErrorTrace(Throwable)
*/
public AcsJException(String message, Throwable cause)
{
super(message); // cause will be set later once we know of what type it is
cause = CorbaExceptionConverter.convertHiddenErrorTrace(cause);
init();
initCause(cause);
}
public AcsJException(ErrorTrace etCause)
{
super();
init();
Throwable cause = CorbaExceptionConverter.recursiveGetThrowable(etCause);
initCause(cause);
}
/**
* Creates a new exception, with the chain of causing exceptions
* derived from <code>etCause</code>.
*
* @param message
* @param etCause
*/
public AcsJException(String message, ErrorTrace etCause)
{
super(message);
init();
Throwable cause = CorbaExceptionConverter.recursiveGetThrowable(etCause);
initCause(cause);
}
/**
* Initializes both static (if null) and non-static member variables.
* Only to be called once by all of the ctors.
* <p>
* todo: for caused-by exceptions that are constructed from their ErrorTrace
* representation, this method does not really need to be called;
* perhaps a new package-private ctor should be added which does not
* call init()
*/
private void init()
{
if (m_initialized)
{
return;
}
if (s_thisHost == null)
{
try
{
s_thisHost = InetAddress.getLocalHost().getHostName();
}
catch (UnknownHostException e)
{
s_thisHost = "unknown";
}
}
if (s_thisProcess == null)
{
s_thisProcess = ContextFinder.getProcessName();
}
m_host = s_thisHost;
m_process = s_thisProcess;
m_sourceObject = ContextFinder.getSourceObject();
m_timeMilli = System.currentTimeMillis();
m_threadName = Thread.currentThread().getName();
setSeverity();
m_properties = new Properties();
// Need to extract this info here (and not later when converting to ErrorTrace)
// so that ErrorTrace can be filled with data from m_line etc.;
// when converting AcsJExceptions that were previously constructed from an ErrorTrace
// only these m_line etc fields will correctly show the original location, unlike any StackTrace
// created then as part of the conversion.
StackTraceElement[] stTrElems = getStackTrace();
if (stTrElems != null && stTrElems[0] != null)
{
StackTraceElement stTrEl = stTrElems[0];
m_line = stTrEl.getLineNumber();
m_method = stTrEl.getMethodName();
m_file = stTrEl.getFileName();
}
else
{
m_file = m_method = "unknown";
m_line = -1;
}
m_initialized = true;
}
public String toString(){
String strProperties = "";
for (Iterator iter = m_properties.keySet().iterator(); iter.hasNext();)
{
String key = (String) iter.next();
if (!key.equals(CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_CLASS) &&
!key.equals(CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_MESSAGE)) {
String value = m_properties.getProperty(key);
strProperties+=key+"='"+value+"' ";
}
}
return super.toString() + ((strProperties.length()>0) ? ( " [ " + strProperties) + "] " : "");
}
/**
* If we have some better name than "java", for example the unique name of
* the java container.
* @param name
*/
public static void setProcessName(String name)
{
s_thisProcess = name;
}
/**
* Forces a subclass to choose an ACS error type.
* @return the error type
*/
protected abstract int getErrorType();
/**
* Forces a subclass to choose an ACS error code.
* @return the error code
*/
protected abstract int getErrorCode();
/**
* Returns the short description which is a hardcoded text particular for a (type, code) combination.
* The subclass that represents the exception code will have to override this method.
* This method is not <code>abstract</code> because the exception class that corresponds to an error type
* (but not an error code) would otherwise have to overload it with returning an empty string.
* @returns an empty string, unless overloaded for an error code.
*/
public String getShortDescription() {
return "";
}
/**
* Creates a CORBA <code>UserException</code> from this exception.
* <p>
* Subclasses must return their associated <code>UserException</code>
* with an <code>ErrorTrace</code> member.
* <p>
* By convention, subclasses must also implement another method that returns
* the correct subtype of <code>UserException</code>.
* No problem with a code generator of course...
*/
public abstract UserException toCorbaException();
/**
* Creates an AcsJCompletion that contains this exception.
* @return A completion object for which {@link AcsJCompletion#getAcsJException()} will yield this exception.
*/
public AcsJCompletion toAcsJCompletion() {
return new AcsJCompletion(this);
}
/**
* Sets the default severity level to {@link Severity#Error}.
* May be overridden by a subclass to set the default severity for that exception,
* or may be called from application code to set it for a specific exception instance.
*/
public void setSeverity()
{// todo add parameter
m_severity = Severity.Error;
}
/**
* Returns the Severity level currently associated with this exception instance.
* @return Severity
*/
public Severity getSeverity()
{
return m_severity;
}
/**
* Allows extra information to be attached to the exception.
* @return the previous value of the specified key in this property
* list, or <code>null</code> if it did not have one.
*/
public Object setProperty(String key, String value)
{
return m_properties.setProperty(key, value);
}
/**
* @see #setProperty
*/
public String getProperty(String key)
{
return m_properties.getProperty(key);
}
/**
* To be used by {@link #getErrorTrace()} or whoever else
* cares about a <code>NameValue[]</code>.
*/
public NameValue[] getNameValueArray()
{
NameValue[] nameValArray = new NameValue[m_properties.size()];
int count = 0;
for (Iterator iter = m_properties.keySet().iterator(); iter.hasNext();)
{
String key = (String) iter.next();
String value = m_properties.getProperty(key);
NameValue nameVal = new NameValue(key, value);
nameValArray[count] = nameVal;
count++;
}
return nameValArray;
}
/**
* Gets the timestamp when this exception was created.
*
* @return timestamp in milliseconds (Java-UTC time, 1970)
*/
public long getTimestampMillis()
{
return m_timeMilli;
}
/**
* Converts this exception (and all "caused-by" throwables it might have)
* to an <code>ErrorTrace</code> that links to
* its caused-by <code>ErrorTrace</code> objects.
* <p>
* To be used whenever the Corba wall is hit and the Java exception must
* be converted to an ACS-Corba-exception.
*/
public ErrorTrace getErrorTrace()
{
if (!m_initialized) {
throw new IllegalStateException("This exception has not been initialized. " +
"Probably a (generated) exception subclass did not call the superclass constructors!");
}
ErrorTrace et = recursiveGetErrorTrace(this, m_timeMilli);
return et;
}
/**
* Translates a <code>Throwable</code> to an <code>ErrorTrace</code>.
* <p>
* If <code>thr</code> is a subclass of <code>AcsJException</code>,
* then the various data fields of the <code>ErrorTrace</code> object
* are properly filled. Otherwise defaults are used.
* <p>
* If <code>thr</code> has other <code>Throwable</code>s chained up
* (see {@link Throwable#getCause}) then these will also be translated,
* and the resulting <code>ErrorTrace</code> objects will be linked together.
*
* @param thr
* @return ErrorTrace
*/
private static ErrorTrace recursiveGetErrorTrace(Throwable thr, long defaultTimeMilli)
{
ErrorTrace et = createSingleErrorTrace(thr, defaultTimeMilli);
// if present, chain up the underlying exception(s)
Throwable cause = thr.getCause();
if (cause != null) {
ErrorTrace causeErrorTrace = recursiveGetErrorTrace(cause, --defaultTimeMilli);
if (et.previousError == null || et.previousError.length != 1) {
et.previousError = new ErrorTrace[1];
}
et.previousError[0] = causeErrorTrace;
}
else {
et.previousError = new ErrorTrace[0];
}
return et;
}
/**
* Creates an <code>ErrorTrace</code> object that represents this exception.
* Linked exceptions are not considered, so the returned <code>ErrorTrace.previousError</code> is left as <code>null</code>.
*/
protected ErrorTrace createSingleErrorTrace() {
ErrorTrace et = new ErrorTrace();
et.host = m_host;
et.process = m_process;
et.sourceObject = m_sourceObject;
et.lineNum = m_line;
et.routine = m_method;
et.file = m_file;
et.errorType = getErrorType();
et.errorCode = getErrorCode();
et.severity = getSeverity();
et.thread = m_threadName;
et.timeStamp = UTCUtility.utcJavaToOmg(m_timeMilli);
et.data = getNameValueArray();
et.shortDescription = getShortDescription();
addExceptionProperties(this, et);
return et;
}
/**
* Converts a Throwable that may not be an AcsJException to an <code>ErrorTrace</code>.
* <p>
* For example, <code>thr</code> could be a <code>NullPointerException</code> thrown in the same process and thus not yet converted
* to <code>DefaultAcsJException</code>, but wrapped by some subclass of AcsJException which contains the NPE as its cause.
*
* @param defaultTimeMilli Java-time (1970-based) used to estimate a time stamp if none is available,
* in order to avoid something from October 1582 in the new ErrorTrace.
* Typically the timestamp from the wrapping AcsJException.
* @see #createSingleErrorTrace()
*/
public static ErrorTrace createSingleErrorTrace(Throwable thr, long defaultTimeMilli) {
ErrorTrace et = null;
if (thr instanceof AcsJException) {
et = ((AcsJException)thr).createSingleErrorTrace();
return et;
}
et = new ErrorTrace();
StackTraceElement[] stTrElems = thr.getStackTrace();
if (stTrElems != null && stTrElems[0] != null) {
StackTraceElement stTrEl = stTrElems[0];
et.file = stTrEl.getFileName();
et.routine = stTrEl.getMethodName();
et.lineNum = stTrEl.getLineNumber();
}
else {
// getStackTrace() only make its best-effort,
// we might end up without the data...
et.file = et.routine = "unknown";
et.lineNum = -1;
}
// the non-ACS exception we are converting does not contain data for the following fields of ErrorTrace,
// so we have to use defaults or smart guessing
et.host = s_thisHost;
et.process = s_thisProcess;
et.sourceObject = ContextFinder.getSourceObject();
et.thread = "NA";
et.timeStamp = UTCUtility.utcJavaToOmg(defaultTimeMilli);
et.errorType = 9; // = ACSErrTypeJavaNative.value; this code is hardcoded otherwise we'll have problem with bulding and duplication of code
et.errorCode = -1;
et.shortDescription = thr.getClass().getName();
et.severity = Severity.Error;
et.data = new NameValue[0];
addExceptionProperties(thr, et);
return et;
}
private static void addExceptionProperties(Throwable thr, ErrorTrace et) {
// add Java exception information as properties
String classname = thr.getClass().getName();
ErrorTraceManipulator.setProperty(et, CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_CLASS, classname);
if (thr.getMessage() != null) {
ErrorTraceManipulator.setProperty(et, CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_MESSAGE, thr.getMessage());
}
}
/**
* Logs this exception and all causing exceptions.
* For each such exception, a separate log entry is created, in accordance with the ACS error logging specification.
*
* @param logger the JDK logger to be used for logging this exception.
*/
public void log(Logger logger)
{
Throwable thr = this;
// the common ID by which all log messages for the various ErrorTrace objects can later be assembled again
String stackID = logger.getName() + System.currentTimeMillis();
// stackLevel is a running integer counting down from the latest Throwable to the initial Throwable (index=0)
for (int stackLevel = getTraceDepth()-1; stackLevel >= 0; stackLevel--) {
ErrorTrace et = createSingleErrorTrace(thr, m_timeMilli - 1);
LogRecord logRec = createSingleErrorTraceLogRecord(et, stackID, stackLevel);
logger.log(logRec);
if (stackLevel > 0) {
thr = thr.getCause();
}
}
}
/**
* Creates a {@link LogRecord} with the data from the given {@link ErrorTrace} object.
* Some data such as thread name, line of code, user-defined properties etc which do not correspond to any
* field of <code>LogRecord</code> are stored inside a <code>Map</code> that is set as a parameter of the <code>LogRecord</code>.
* Later when the ACS logging system will process this <code>LogRecord</code>, it will recognize the special data
* and turn them into the XML attributes of elements like <Debug>
* @param et the ErrorTrace input object
* @param stackID the stackID that must be the same for all linked <code>ErrorTrace</code> objects.
* @param stackLevel 0-based index for LogRecords from an ErrorTrace chain.
* @return the LogRecord that contains the data from the input parameters.
*/
LogRecord createSingleErrorTraceLogRecord(ErrorTrace et, String stackID, int stackLevel) {
Level logLevel = ErrorTraceLogLevels.mapErrorLevelToLogLevel(et.severity);
String message = et.shortDescription.trim();
message += " (type=" + et.errorType + ", code=" + et.errorCode + ")";
String usermessage = ErrorTraceManipulator.getProperty(et, CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_MESSAGE);
if (usermessage != null && usermessage.trim().length() > 0) {
message += " :: " + usermessage.trim();
}
// need to explicitly construct a log record with the historical data (normal Logger methods would not allow setting these fields)
LogRecord logRec = new LogRecord(logLevel, message);
logRec.setMillis(UTCUtility.utcOmgToJava(et.timeStamp));
logRec.setSourceClassName(et.file);
logRec.setSourceMethodName(et.routine);
logRec.setLoggerName(et.sourceObject); // the SourceObject will be derived from the logger name in AcsXMLLOgFormatter
// other fields are encoded in a map and will be extracted by the AcsXMLLOgFormatter before sending the log record over the wire
// Due to build order problems we can't call LogParameterUtil#createPropertiesMap(), but instead have to repeat that code here;
// the same is true for the property names which would be available as String constants of LogParameterUtil.
Map<String, Object> specialLogProperties = new HashMap<String, Object>();
specialLogProperties.put("isAcsPropertiesMap", Boolean.TRUE);
specialLogProperties.put("Line", new Long(et.lineNum));
specialLogProperties.put("ThreadName", et.thread);
specialLogProperties.put("HostName", et.host);
// logProperties.put("Context", "???");
specialLogProperties.put("StackId", stackID);
specialLogProperties.put("StackLevel", new Long(stackLevel));
specialLogProperties.put("ProcessName", et.process);
specialLogProperties.put("SourceObject", et.sourceObject);
// non-standard properties from ErrorTrace
Map etProperties = ErrorTraceManipulator.getProperties(et);
etProperties.remove(CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_CLASS);
etProperties.remove(CorbaExceptionConverter.PROPERTY_JAVAEXCEPTION_MESSAGE);
// set both maps as log parameters. By design there can't be any other parameters which would be lost here
logRec.setParameters(new Object[] {specialLogProperties, etProperties} );
return logRec;
}
/**
* Gets the total number of linked ("caused-by") exceptions including this one.
* <p>
* Not to be confused with the length of {@link Throwable#getStackTrace()}, which
* refers to the call stack for the current exception, and not to linked causing exception.
*/
public int getTraceDepth() {
int depth = 1;
Throwable cause = getCause();
while (cause != null) {
depth++;
cause = cause.getCause();
}
return depth;
}
/**
* @return Returns the m_file.
*/
public String getFile()
{
return m_file;
}
/**
* @return Returns the m_host.
*/
public String getHost()
{
return m_host;
}
/**
* @return Returns the m_line.
*/
public int getLine()
{
return m_line;
}
/**
* @return Returns the m_method.
*/
public String getMethod()
{
return m_method;
}
/**
* @return Returns the m_process.
*/
public String getProcess()
{
return m_process;
}
/**
* @return Returns the m_threadName.
*/
public String getThreadName()
{
return m_threadName;
}
/**
* @return Returns the m_sourceObject.
*/
public String getSourceObject()
{
return m_sourceObject;
}
}