/* * 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.lang.reflect.InvocationTargetException; /** * 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. </p> 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 : * <ul> * <li>If the exception class exists on the client, the original exception is * created</li> * <li>If the exception class exists, but doesn't have a suitable constructor * then another exception is thrown referencing the original exception</li> * <li>If the exception class exists, but is not throwable, another exception is * thrown referencing the original exception</li> * <li>If the exception class doesn't exist, another exception is raised instead * </li> * </ul> * * @author <a href="mailto:contact@andygibson.net">Andy Gibson</a> */ public class ExceptionProxy implements Externalizable { private static final long serialVersionUID = 2321010311438950147L; private String className; private String message; private StackTraceElement[] trace; private ExceptionProxy causeProxy; private Throwable cause; private Throwable original; private Throwable serializationProcessException = null; public ExceptionProxy() { } public ExceptionProxy(Throwable throwable) { this.className = throwable.getClass().getName(); this.message = throwable.getMessage(); this.trace = throwable.getStackTrace(); this.causeProxy = ExceptionProxy.createForException(throwable.getCause()); this.original = 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; } 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, the cause. * * @return The constructed {@link Throwable} instance */ public Throwable createException() { if (!hasException()) { return null; } 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 cause; } /** * Custom Serialization logic. * <p> * If possible, we try to keep the original Exception form the Container side. * <p> * If we can't load the Exception on the client side, return a ArquillianProxyException that keeps the original stack * trace etc. * <p> * 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 { className = (String) in.readObject(); message = (String) in.readObject(); trace = (StackTraceElement[]) in.readObject(); causeProxy = (ExceptionProxy) in.readObject(); /* * Attempt to deserialize the original Exception. It might fail due to ClassNotFoundExceptions, ignore and move on */ byte[] originalExceptionData = (byte[]) in.readObject(); if (originalExceptionData != null && originalExceptionData.length > 0) { try { ByteArrayInputStream originalIn = new ByteArrayInputStream(originalExceptionData); ObjectInputStream input = new ObjectInputStream(originalIn); // // Uncomment to run ExceptionProxySerializationTestCase // { // @Override // protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException // { // return Class.forName(desc.getName(), false, Thread.currentThread().getContextClassLoader()); // } // }; original = (Throwable) input.readObject(); if (causeProxy != null) { // reset the cause, so we can de-serialize them individual Throwable cause = causeProxy.createException(); if (original instanceof InvocationTargetException) { SecurityActions.setFieldValue(InvocationTargetException.class, original, "target", cause); } else { SecurityActions.setFieldValue(Throwable.class, original, "cause", cause); } } } catch (Throwable e) // Possible ClassNotFoundExcpetion / NoClassDefFoundError { // ignore, could not load class on client side, move on and create a fake 'proxy' later serializationProcessException = e; } } // Override with the remote serialization issue cause if exists Throwable tmpSerializationProcessException = (Throwable) in.readObject(); if (tmpSerializationProcessException != null) { serializationProcessException = tmpSerializationProcessException; } } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(className); out.writeObject(message); out.writeObject(trace); out.writeObject(causeProxy); byte[] originalBytes = new byte[0]; if (original != null) { try { // reset the cause, so we can serialize the exception chain individual SecurityActions.setFieldValue(Throwable.class, original, "cause", null); } catch (Exception e) { // move on, try to serialize anyway } try { ByteArrayOutputStream originalOut = new ByteArrayOutputStream(); ObjectOutputStream output = new ObjectOutputStream(originalOut); output.writeObject(original); output.flush(); originalBytes = originalOut.toByteArray(); } catch (NotSerializableException e) { // in case some class breaks Serialization contract serializationProcessException = e; } } out.writeObject(originalBytes); out.writeObject(serializationProcessException); } @Override public String toString() { return super.toString() + String.format("[class=%s, message=%s],cause = %s", className, message, causeProxy); } }