
org.jboss.arquillian.test.spi.ExceptionProxy Maven / Gradle / Ivy
Show all versions of arquillian-test-spi Show documentation
/*
* JBoss, Home of Professional Open Source
* Copyright 2009 Red Hat Inc. and/or its affiliates and other contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.arquillian.test.spi;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Takes an exception class and creates a proxy that can be used to rebuild the
* exception. The problem stems from problems serializing exceptions and
* deserializing them in another application where the exception classes might
* not exist, or they might exist in different version. This proxy also
* propagates the stacktrace and the cause exception to create totally portable
* exceptions. This class creates a serializable proxy of the exception and
* when unserialized can be used to re-create the exception based on the
* following rules :
*
* - If the exception class exists on the client, the original exception is
* created
* - If the exception class exists, but doesn't have a suitable constructor
* then another exception is thrown referencing the original exception
* - If the exception class doesn't exist, one of its superclasses is used
*
*
*
* @author Andy Gibson
*/
public class ExceptionProxy implements Externalizable {
// The serialVersionUID of the ExceptionProxy that existed in Arquillian 1.9.1.Final
private static final long serialVersionUID = 2321010311438950147L;
// This is the className of the exception in the container passed into TestResult#setThrowable(Throwable)
private String className;
// This is the message of the exception in the container passed into TestResult#setThrowable(Throwable)
private String message;
// This is the stack trace of the exception in the container passed into TestResult#setThrowable(Throwable)
private StackTraceElement[] trace;
// This is a proxy to the cause exception in the container, not used post 1.9.1.Final
private ExceptionProxy causeProxy;
// This is the causeProxy#createException() instance
private Throwable cause;
// This only exists if the original container exception could be deserialized in the client
private Throwable original;
// This would exist if the original exception could not be serialized in the container
private Throwable serializationProcessException = null;
// New fields added in 1.9.2.Final
private Version version;
// The list of superclasses of the exception class that was serialized
private List exceptionHierarchy;
public static class Version implements Serializable {
private static final long serialVersionUID = 1L;
int version = 2;
}
public ExceptionProxy() {
version = new Version();
}
public ExceptionProxy(Throwable throwable) {
this.version = new Version();
this.className = throwable.getClass().getName();
this.message = throwable.getMessage();
this.trace = throwable.getStackTrace();
//this.causeProxy = ExceptionProxy.createForException(throwable.getCause());
this.original = throwable;
this.exceptionHierarchy = getExceptionHierarchy(throwable);
}
/**
* Static method to create an exception proxy for the passed in
* {@link Throwable} class. If null is passed in, null is returned as the
* exception proxy
*
* @param throwable
* Exception to proxy
*
* @return An ExceptionProxy representing the exception passed in
*/
public static ExceptionProxy createForException(Throwable throwable) {
if (throwable == null) {
return null;
}
//System.out.println("ExceptionProxy.createForException, throwable=" + throwable);
return new ExceptionProxy(throwable);
}
/**
* Indicates whether this proxy wraps an exception
*
* @return Flag indicating an exception is wrapped.
*/
public boolean hasException() {
return className != null;
}
/**
* Constructs an instance of the proxied exception based on the class name,
* message, stack trace and if applicable, and the cause if the cause could be
* deserialized in the client. Otherwise, this returns an ArquillianProxyException
*
* @return The constructed {@link Throwable} instance if one exists, null otherwise
*/
public Throwable createException() {
if (!hasException()) {
return null;
}
//System.out.println("ExceptionProxy.createException, this=" + this);
if (original != null) {
return original;
}
Throwable throwable = createProxyException(
"Original exception caused: " + (serializationProcessException != null
? serializationProcessException.getClass() + ": " + serializationProcessException.getMessage()
: "Unknown serialization issue"));
return throwable;
}
public ArquillianProxyException createProxyException(String reason) {
ArquillianProxyException exception = new ArquillianProxyException(message, className, reason, getCause());
exception.setStackTrace(trace);
return exception;
}
/**
* Returns the cause of the exception represented by this proxy
*
* @return The cause of this exception
*/
public Throwable getCause() {
// lazy create cause
if (cause == null) {
if (causeProxy != null) {
cause = causeProxy.createException();
}
}
return serializationProcessException;
}
// Accessors to the fields of the ExceptionProxy
public String getClassName() {
return className;
}
public String getMessage() {
return message;
}
public StackTraceElement[] getTrace() {
return trace;
}
public ExceptionProxy getCauseProxy() {
return causeProxy;
}
public Throwable getOriginal() {
return original;
}
public Throwable getSerializationProcessException() {
return serializationProcessException;
}
public Version getVersion() {
return version;
}
public List getExceptionHierarchy() {
return exceptionHierarchy;
}
/**
* Override to provide the full details of the exception being proxied
* @return The full details of the exception being proxied
*/
@Override
public String toString() {
StringBuilder tmp = new StringBuilder();
tmp.append("ExceptionProxy("+className+")");
if(message != null) {
tmp.append("msg: ").append(message);
}
if(causeProxy != null) {
tmp.append("\ncause: ").append(causeProxy);
}
if(trace != null) {
for(StackTraceElement element : trace) {
tmp.append("\n\tat ").append(element);
}
}
if(exceptionHierarchy != null) {
tmp.append("\nexceptionHierarchy: ");
for(String t : exceptionHierarchy) {
tmp.append("\n\t").append(t);
}
}
if(serializationProcessException != null) {
tmp.append("\nserializationProcessException: ").append(serializationProcessException);
}
return tmp.toString();
}
/**
* Custom Serialization logic.
*
* If possible, we try to keep the original Exception form the Container side.
*
* If we can't load the Exception on the client side, return a ArquillianProxyException that keeps the original stack
* trace etc.
*
* We can't use in.readObject() on the Throwable cause, because if a ClassNotFoundException is thrown, the stream is
* marked with the exception
* and that stream is the same stream that is deserializing us, so we will fail outside of our control. Store the
* Throwable cause as a
* serialized byte array instead, so we can deserialize it outside of our own stream.
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Read the first object to see if it is the version object
Object firstObject = in.readObject();
if (firstObject instanceof Version) {
version = (Version) firstObject;
className = (String) in.readObject();
message = (String) in.readObject();
trace = (StackTraceElement[]) in.readObject();
exceptionHierarchy = (List) in.readObject();
// Try to deserialize the original exception
try {
byte[] originalExceptionData = (byte[]) in.readObject();
if (originalExceptionData != null && originalExceptionData.length > 0) {
ByteArrayInputStream originalIn = new ByteArrayInputStream(originalExceptionData);
ObjectInputStream input = new ObjectInputStream(originalIn);
original = (Throwable) input.readObject();
}
} catch (Throwable e) {
this.serializationProcessException = e;
}
// Override with the remote serialization issue cause if exists
Throwable tmpSerializationProcessException = (Throwable) in.readObject();
if (tmpSerializationProcessException != null) {
serializationProcessException = tmpSerializationProcessException;
}
// If we were not able to create original from originalExceptionData, try to create it from exceptionHierarchy
if(original == null) {
original = buildOriginalException();
}
} else {
serializationProcessException = new IOException("Unknown version of ExceptionProxy");
}
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(version);
out.writeObject(className);
out.writeObject(message);
out.writeObject(trace);
out.writeObject(exceptionHierarchy);
byte[] originalBytes = new byte[0];
try {
/* Try to serialize the original exception. Here we do it in a separate try-catch block to avoid
because default serialization will serialize whatever it can and leave non-serializable fields out.
We have to make the write of the root exception atomic.
*/
ByteArrayOutputStream originalOut = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(originalOut);
output.writeObject(original);
output.flush();
originalBytes = originalOut.toByteArray();
} catch (NotSerializableException e) {
// ignore, could not serialize original exception
this.serializationProcessException = e;
}
out.writeObject(originalBytes);
out.writeObject(serializationProcessException);
}
/**
* Get the exception hierarchy for the exception class being proxied.
*
* @return list of exception types in the hierarchy
*/
protected List getExceptionHierarchy(Throwable t) {
List hierarchy = new ArrayList<>();
Class> tclass = t.getClass();
while(Throwable.class.isAssignableFrom(tclass)) {
hierarchy.add(tclass.getName());
tclass = tclass.getSuperclass();
}
return hierarchy;
}
/**
* Build the original exception based on the exception class name. This first
* tries to use a ctor with a message, then a default ctor.
*
* @return the original exception
*/
protected Throwable buildOriginalException() {
Throwable original = null;
for(String tclassName : exceptionHierarchy) {
try {
Class extends Throwable> tclass = Class.forName(tclassName).asSubclass(Throwable.class);
try {
original = tclass.getDeclaredConstructor(String.class).newInstance(message);
original.setStackTrace(trace);
break;
} catch (Exception e) {
try {
original = tclass.getDeclaredConstructor().newInstance();
original.setStackTrace(trace);
break;
} catch (Exception ex) {
// ignore, could not load class on client side, try next base class
}
}
} catch (ClassNotFoundException e) {
// ignore, could not load class on client side, try next base class
}
}
return original;
}
}