/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.aries.blueprint.utils;

import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.aries.blueprint.container.GenericType;
import org.apache.aries.blueprint.di.ExecutionContext;
import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
import org.osgi.framework.BundleReference;
import org.osgi.service.blueprint.container.ComponentDefinitionException;

/**
 * TODO: javadoc
 *
 * @version $Rev$, $Date$
 */
public class ReflectionUtils {

    private static Map<Class<?>, WeakReference<Method[]>> publicMethods = Collections.synchronizedMap(new WeakHashMap<Class<?>, WeakReference<Method[]>>());
    private static Map<Class<?>, PropertyDescriptor[][]> beanInfos = Collections.synchronizedMap(new WeakHashMap<Class<?>, PropertyDescriptor[][]>());

    public static boolean hasDefaultConstructor(Class type) {
        if (!Modifier.isPublic(type.getModifiers())) {
            return false;
        }
        if (Modifier.isAbstract(type.getModifiers())) {
            return false;
        }
        Constructor[] constructors = type.getConstructors();
        for (Constructor constructor : constructors) {
            if (Modifier.isPublic(constructor.getModifiers()) &&
                    constructor.getParameterTypes().length == 0) {
                return true;
            }
        }
        return false;
    }
    
    public static Set<String> getImplementedInterfaces(Set<String> classes, Class clazz) {
        if (clazz != null && clazz != Object.class) {
            for (Class itf : clazz.getInterfaces()) {
                if (Modifier.isPublic(itf.getModifiers())) {
                    classes.add(itf.getName());
                }
                getImplementedInterfaces(classes, itf);
            }
            getImplementedInterfaces(classes, clazz.getSuperclass());
        }
        return classes;
    }
    
    public static Set<Class<?>> getImplementedInterfacesAsClasses(Set<Class<?>> classes, Class<?> clazz) {
        if (clazz != null && clazz != Object.class) {
            for (Class<?> itf : clazz.getInterfaces()) {
                if (Modifier.isPublic(itf.getModifiers())) {
                    classes.add(itf);
                }
                getImplementedInterfacesAsClasses(classes, itf);
            }
            getImplementedInterfacesAsClasses(classes, clazz.getSuperclass());
        }
        return classes;
    }

    public static Set<String> getSuperClasses(Set<String> classes, Class clazz) {
        if (clazz != null && clazz != Object.class) {
            if (Modifier.isPublic(clazz.getModifiers())) {
                classes.add(clazz.getName());
            }
            getSuperClasses(classes, clazz.getSuperclass());
        }
        return classes;
    }

    public static Method getLifecycleMethod(Class clazz, String name) {
        if (name != null) {
            for (Method method : getPublicMethods(clazz)) {
                if (method.getName().equals(name)
                        && method.getParameterTypes().length == 0
                        && Void.TYPE.equals(method.getReturnType())) {
                    return method;
                }
            }
        }
        return null;
    }

    public static Method[] getPublicMethods(Class clazz) {
        WeakReference<Method[]> ref = publicMethods.get(clazz);
        Method[] methods = ref != null ? ref.get() : null;
        if (methods == null) {
            ArrayList<Method> array = new ArrayList<Method>();
            doGetPublicMethods(clazz, array);
            methods = array.toArray(new Method[array.size()]);
            publicMethods.put(clazz, new WeakReference<Method[]>(methods));
        }
        return methods;
    }

    private static void doGetPublicMethods(Class clazz, ArrayList<Method> methods) {
        Class parent = clazz.getSuperclass();
        if (parent != null) {
            doGetPublicMethods(parent, methods);
        }
        for (Class interf : clazz.getInterfaces()) {
            doGetPublicMethods(interf, methods);
        }
        if (Modifier.isPublic(clazz.getModifiers())) {
            for (Method mth : clazz.getMethods()) {
                removeByNameAndSignature(methods, mth);
                methods.add(mth);
            }
        }
    }

    private static void removeByNameAndSignature(ArrayList<Method> methods, Method toRemove) {
        for (int i = 0; i < methods.size(); i++) {
            Method m = methods.get(i);
            if (m != null &&
                    m.getReturnType() == toRemove.getReturnType() &&
                    m.getName() == toRemove.getName() &&
                    arrayContentsEq(m.getParameterTypes(),
                            toRemove.getParameterTypes())) {
                methods.remove(i--);
            }
        }
    }

    private static boolean arrayContentsEq(Object[] a1, Object[] a2) {
        if (a1 == null) {
            return a2 == null || a2.length == 0;
        }
        if (a2 == null) {
            return a1.length == 0;
        }
        if (a1.length != a2.length) {
            return false;
        }
        for (int i = 0; i < a1.length; i++) {
            if (a1[i] != a2[i]) {
                return false;
            }
        }
        return true;
    }

    public static List<Method> findCompatibleMethods(Class clazz, String name, Class[] paramTypes) {
        List<Method> methods = new ArrayList<Method>();
        for (Method method : getPublicMethods(clazz)) {
            Class[] methodParams = method.getParameterTypes();
            if (name.equals(method.getName()) && Void.TYPE.equals(method.getReturnType()) && methodParams.length == paramTypes.length && !method.isBridge()) {
                boolean assignable = true;
                for (int i = 0; i < paramTypes.length && assignable; i++) {
                    assignable &= paramTypes[i] == null || methodParams[i].isAssignableFrom(paramTypes[i]);
                }
                if (assignable) {
                    methods.add(method);
                }
            }
        }
        return methods;
    }

    public static PropertyDescriptor[] getPropertyDescriptors(Class clazz, boolean allowFieldInjection) {
        PropertyDescriptor[][] properties = beanInfos.get(clazz);
        int index = allowFieldInjection ? 0 : 1;
        
        if (properties == null) {
            properties = new PropertyDescriptor[2][];
            beanInfos.put(clazz, properties);
        }
        
        if (properties[index] == null) {
            Set<String> propertyNames = new HashSet<String>();
            Map<String,Method> getters = new HashMap<String, Method>();
            Map<String,List<Method>> setters = new HashMap<String, List<Method>>();
            Set<String> illegalProperties = new HashSet<String>();
            
            for (Method method : getPublicMethods(clazz)) {
                if (Modifier.isStatic(method.getModifiers()) || method.isBridge()) continue;
                
                String name = method.getName();
                Class<?> argTypes[] = method.getParameterTypes();
                Class<?> resultType = method.getReturnType();
                
                if (name.length() > 3 && name.startsWith("set") && resultType == Void.TYPE && argTypes.length == 1) {
                    name = decapitalize(name.substring(3));
                    if (!!!setters.containsKey(name)) setters.put(name, new ArrayList<Method>());
                    setters.get(name).add(method);
                    propertyNames.add(name);
                } else if (name.length() > 3 && name.startsWith("get") && resultType != Void.TYPE && argTypes.length == 0) {
                    name = decapitalize(name.substring(3));

                    if (getters.containsKey(name)) illegalProperties.add(name);
                    else propertyNames.add(name);
                    
                    getters.put(name, method);                    
                } else if (name.length() > 2 && name.startsWith("is") && argTypes.length == 0 && resultType == boolean.class) {
                    name = decapitalize(name.substring(2));

                    if (getters.containsKey(name)) illegalProperties.add(name);
                    else propertyNames.add(name);
                    
                    getters.put(name, method);                    
                }
                
            }

            Map<String, PropertyDescriptor> props = new HashMap<String, PropertyDescriptor>();
            for (String propName : propertyNames) {
                props.put(propName,
                        new MethodPropertyDescriptor(propName, getters.get(propName), setters.get(propName)));
            }            
            
            if (allowFieldInjection) {
                for (Class cl = clazz; cl != null && cl != Object.class; cl = cl.getSuperclass()) {
                    for (Field field : cl.getDeclaredFields()) {
                        if (!!!Modifier.isStatic(field.getModifiers())) {
                            String name = decapitalize(field.getName());
                            PropertyDescriptor desc = props.get(name);
                            if (desc == null) {
                                props.put(name, new FieldPropertyDescriptor(name, field));
                            } else if (desc instanceof MethodPropertyDescriptor) {
                                props.put(name,
                                        new JointPropertyDescriptor((MethodPropertyDescriptor) desc,
                                                new FieldPropertyDescriptor(name, field)));
                            } else {
                                illegalProperties.add(name);
                            }
                        }
                    }
                }
            }
            
            List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();
            for (PropertyDescriptor prop : props.values()) {
                if (!!!illegalProperties.contains(prop.getName())) result.add(prop);
            }
            
            properties[index] = result.toArray(new PropertyDescriptor[result.size()]); 
        }
        return properties[index];
    }

    private static String decapitalize(String name) {
        if (name == null || name.length() == 0) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                Character.isUpperCase(name.charAt(0))) {
            return name;
        }
        char chars[] = name.toCharArray();
        chars[0] = Character.toLowerCase(chars[0]);
        return new String(chars);
    }

    public static Object invoke(AccessControlContext acc, final Method method, final Object instance, final Object... args) throws Exception {
        if (acc == null) {
            return method.invoke(instance, args);
        } else {
            try {
                return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                    public Object run() throws Exception {
                        return method.invoke(instance, args);
                    }            
                }, acc);
            } catch (PrivilegedActionException e) {
                throw e.getException();
            }
        }
    }
    
    public static Object newInstance(AccessControlContext acc, final Class clazz) throws Exception {
        if (acc == null) {
            return clazz.newInstance();
        } else {
            try {
                return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                    public Object run() throws Exception {
                        return clazz.newInstance();
                    }            
                }, acc);
            } catch (PrivilegedActionException e) {
                throw e.getException();
            }
        }
    }
    
    public static Object newInstance(AccessControlContext acc, final Constructor constructor, final Object... args) throws Exception {
        if (acc == null) {
            return constructor.newInstance(args);
        } else {
            try {
                return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                    public Object run() throws Exception {
                        return constructor.newInstance(args);
                    }            
                }, acc);
            } catch (PrivilegedActionException e) {
                throw e.getException();
            }
        }
    }
    
    public static abstract class PropertyDescriptor {
        private final String name;
        
        public PropertyDescriptor(String name) {
            this.name = name;
        }
        
        public String getName() {
            return name;
        }
        
        public abstract boolean allowsGet();
        public abstract boolean allowsSet();
        
        protected abstract Object internalGet(ExtendedBlueprintContainer container, Object instance) throws Exception;
        protected abstract void internalSet(ExtendedBlueprintContainer container, Object instance, Object value) throws Exception;        
        
        public Object get(final Object instance, final ExtendedBlueprintContainer container) throws Exception {            
            if (container.getAccessControlContext() == null) {
                return internalGet(container, instance);
            } else {
                try {
                    return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        public Object run() throws Exception {
                            return internalGet(container, instance);
                        }            
                    }, container.getAccessControlContext());
                } catch (PrivilegedActionException e) {
                    throw e.getException();
                }
            }
        }

        public void set(final Object instance, final Object value, final ExtendedBlueprintContainer container) throws Exception {
            if (container.getAccessControlContext() == null) {
                internalSet(container, instance, value);
            } else {
                try {
                    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        public Object run() throws Exception {
                            internalSet(container, instance, value);
                            return null;
                        }            
                    }, container.getAccessControlContext());
                } catch (PrivilegedActionException e) {
                    throw e.getException();
                }
            }            
        }
        
        protected Object convert(Object obj, Type type) throws Exception {
            return ExecutionContext.Holder.getContext().convert(obj, new GenericType(type));
        }
    }
    
    private static class JointPropertyDescriptor extends PropertyDescriptor {
        private final MethodPropertyDescriptor mpd;
        private final FieldPropertyDescriptor fpd;
        
        public JointPropertyDescriptor(MethodPropertyDescriptor mpd, FieldPropertyDescriptor fpd) {
            super(mpd.getName());
            this.mpd = mpd;
            this.fpd = fpd;
        }

        @Override
        public boolean allowsGet() {
            return mpd.allowsGet() || fpd.allowsGet();
        }

        @Override
        public boolean allowsSet() {
            return mpd.allowsSet() || fpd.allowsSet();
        }

        @Override
        protected Object internalGet(ExtendedBlueprintContainer container, Object instance) throws Exception {
            if (mpd.allowsGet()) return mpd.internalGet(container, instance);
            else if (fpd.allowsGet()) return fpd.internalGet(container, instance);
            else throw new UnsupportedOperationException();
        }

        @Override
        protected void internalSet(ExtendedBlueprintContainer container, Object instance, Object value) throws Exception {
            if (mpd.allowsSet()) mpd.internalSet(container, instance, value);
            else if (fpd.allowsSet()) fpd.internalSet(container, instance, value);
            else throw new UnsupportedOperationException();
        }
    }
    
    private static class FieldPropertyDescriptor extends PropertyDescriptor {
        // instead of holding on to the java.lang.reflect.Field objects we retrieve it every time. The reason is that PropertyDescriptors are 
        // used as values in a WeakHashMap with the class corresponding to the field as the key
        private final String fieldName;
        private final WeakReference<Class<?>> declaringClass;
        
        public FieldPropertyDescriptor(String name, Field field) {
            super(name);
            this.fieldName = field.getName();
            this.declaringClass = new WeakReference(field.getDeclaringClass());
        }

        public boolean allowsGet() {
            return true;
        }

        public boolean allowsSet() {
            return true;
        }
        
        private Field getField(ExtendedBlueprintContainer container) throws ClassNotFoundException, NoSuchFieldException {
            if (declaringClass.get() == null) throw new ClassNotFoundException("Declaring class was garbage collected");
            
            return declaringClass.get().getDeclaredField(fieldName);
        }

        protected Object internalGet(final ExtendedBlueprintContainer container, final Object instance) throws Exception {
            if (useContainersPermission(container)) {
                try {
                    return AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        public Object run() throws Exception {
                            return doInternalGet(container, instance);
                        }                        
                    });
                } catch (PrivilegedActionException pae) {
                    Exception e = pae.getException();
                    if (e instanceof IllegalAccessException) throw (IllegalAccessException) e;
                    else throw (RuntimeException) e;
                }
            } else {
                return doInternalGet(container, instance);
            }
        }
        
        private Object doInternalGet(ExtendedBlueprintContainer container, Object instance) throws Exception {
            Field field = getField(container);
            boolean isAccessible = field.isAccessible();
            field.setAccessible(true);
            try {
                return field.get(instance);
            } finally {
                field.setAccessible(isAccessible);
            }
        }

        protected void internalSet(final ExtendedBlueprintContainer container, final Object instance, final Object value) throws Exception {
            try {
                Boolean wasSet = AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
                    public Boolean run() throws Exception {
                      if (useContainersPermission(container)) {
                        doInternalSet(container, instance, value);
                        return Boolean.TRUE;
                      }
                      return Boolean.FALSE;
                    }                        
                });
                if(!!!wasSet) {
                  doInternalSet(container, instance, value);
                }
            } catch (PrivilegedActionException pae) {
                throw pae.getException();
            }
        }
        
        private void doInternalSet(ExtendedBlueprintContainer container, Object instance, Object value) throws Exception {
            Field field = getField(container);
            final Object convertedValue = convert(value, field.getGenericType());
            boolean isAccessible = field.isAccessible();
            field.setAccessible(true);
            try {
                field.set(instance, convertedValue);
            } finally {
                field.setAccessible(isAccessible);
            }
        }
        
        /**
         * Determine whether the field access (in particular the call to {@link Field#setAccessible(boolean)} should be done with the Blueprint extender's
         * permissions, rather than the joint (more restrictive) permissions of the extender plus the Blueprint bundle.
         * 
         * We currently only allow this for classes that originate from inside the Blueprint bundle. Otherwise this would open a potential security hole.
         * @param container
         * @return
         */
        private boolean useContainersPermission(ExtendedBlueprintContainer container) throws ClassNotFoundException {
            if (declaringClass.get() == null) throw new ClassNotFoundException("Declaring class was garbage collected");
            ClassLoader loader = declaringClass.get().getClassLoader();
            
            if (loader == null) return false;
            
            if (loader instanceof BundleReference) {
                BundleReference ref = (BundleReference) loader;
                return ref.getBundle().equals(container.getBundleContext().getBundle());                
            }
            
            return false;
        }
    }
    
    private static class MethodDescriptor {
        private final String methodName;
        private final WeakReference<Class<?>> declaringClass;
        private final List<WeakReference<Class<?>>> argClasses;
        
        public MethodDescriptor(Method method) {
            methodName = method.getName();
            declaringClass = new WeakReference<Class<?>>(method.getDeclaringClass());
            
            List<WeakReference<Class<?>>> accumulator = new ArrayList<WeakReference<Class<?>>>();
            for (Class<?> c : method.getParameterTypes()) {
                accumulator.add(new WeakReference<Class<?>>(c));
            }
            argClasses = Collections.unmodifiableList(accumulator);
        }
        
        public Method getMethod(ExtendedBlueprintContainer container) throws ClassNotFoundException, NoSuchMethodException {
            Class<?>[] argumentClasses = new Class<?>[argClasses.size()];
            for (int i=0; i<argClasses.size(); i++) {
                argumentClasses[i] = argClasses.get(i).get();
                if (argumentClasses[i] == null) throw new ClassNotFoundException("Argument class was garbage collected");
            }
            
            if (declaringClass.get() == null) throw new ClassNotFoundException("Declaring class was garbage collected");
            
            return declaringClass.get().getMethod(methodName, argumentClasses);
        }
        
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(declaringClass.get()).append(".").append(methodName).append("(");
            
            boolean first = true;
            for (WeakReference<Class<?>> wcl : argClasses) {
                if (!!!first) builder.append(",");
                else first = false;
                
                builder.append(wcl.get());
            }

            builder.append(")");
            return builder.toString();
        }
    }
    
    private static class MethodPropertyDescriptor extends PropertyDescriptor {
        // instead of holding on to the java.lang.reflect.Method objects we retrieve it every time. The reason is that PropertyDescriptors are 
        // used as values in a WeakHashMap with the class corresponding to the methods as the key
        private final MethodDescriptor getter;
        private final Collection<MethodDescriptor> setters;

        private MethodPropertyDescriptor(String name, Method getter, Collection<Method> setters) {
            super(name);
            this.getter = (getter != null) ? new MethodDescriptor(getter) : null;
            
            if (setters != null) {
                Collection<MethodDescriptor> accumulator = new ArrayList<MethodDescriptor>();
                for (Method s : setters) accumulator.add(new MethodDescriptor(s));
                this.setters = Collections.unmodifiableCollection(accumulator);
            } else {
                this.setters = Collections.emptyList();
            }
        }
        
        public boolean allowsGet() {
            return getter != null;
        }
        
        public boolean allowsSet() {
            return !!!setters.isEmpty();
        }
        
        protected Object internalGet(ExtendedBlueprintContainer container, Object instance) 
                throws Exception {
            if (getter != null) {
                return getter.getMethod(container).invoke(instance);
            } else {
                throw new UnsupportedOperationException();
            }
        }
        
        protected void internalSet(ExtendedBlueprintContainer container, Object instance, Object value) throws Exception {
            
            Method setterMethod = findSetter(container, value);

            if (setterMethod != null) {
                setterMethod.invoke(instance, convert(value, setterMethod.getGenericParameterTypes()[0]));
            } else {
                throw new ComponentDefinitionException(
                        "No converter available to convert value "+value+" into a form applicable for the " + 
                        "setters of property "+getName());
            }
        }
        
        private Method findSetter(ExtendedBlueprintContainer container, Object value) throws Exception {
            Class<?> valueType = (value == null) ? null : value.getClass();
            
            Method getterMethod = (getter != null) ? getter.getMethod(container) : null;
            Collection<Method> setterMethods = getSetters(container);
            
            Method result = findMethodByClass(getterMethod, setterMethods, valueType);
            
            if (result == null) result = findMethodWithConversion(setterMethods, value);
                        
            return result;
        }
        
        private Collection<Method> getSetters(ExtendedBlueprintContainer container) throws Exception {
            Collection<Method> result = new ArrayList<Method>();
            for (MethodDescriptor md : setters) result.add(md.getMethod(container));
            
            return result;
        }
        
        private Method findMethodByClass(Method getterMethod, Collection<Method> setterMethods, Class<?> arg)
                throws ComponentDefinitionException {
            Method result = null;

            if (!hasSameTypeSetter(getterMethod, setterMethods)) {
                throw new ComponentDefinitionException(
                        "At least one Setter method has to match the type of the Getter method for property "
                                + getName());
            }

            if (setterMethods.size() == 1) {
                return setterMethods.iterator().next();
            }
            
            for (Method m : setterMethods) {
                Class<?> paramType = m.getParameterTypes()[0];

                if ((arg == null && Object.class.isAssignableFrom(paramType))
                        || (arg != null && paramType.isAssignableFrom(arg))) {

                    // pick the method that has the more specific parameter if
                    // any
                    if (result != null) {
                        Class<?> oldParamType = result.getParameterTypes()[0];
                        if (paramType.isAssignableFrom(oldParamType)) {
                            // do nothing, result is correct
                        } else if (oldParamType.isAssignableFrom(paramType)) {
                            result = m;
                        } else {
                            throw new ComponentDefinitionException(
                                    "Ambiguous setter method for property "
                                            + getName()
                                            + ". More than one method matches the  parameter type "
                                            + arg);
                        }
                    } else {
                        result = m;
                    }
                }
            }

            return result;
        }
        
        // ensure there is a setter that matches the type of the getter
        private boolean hasSameTypeSetter(Method getterMethod, Collection<Method> setterMethods) {
            if (getterMethod == null) {
                return true;
            }

            Iterator<Method> it = setterMethods.iterator();
            while (it.hasNext()) {
                Method m = it.next();
                if (m.getParameterTypes()[0].equals(getterMethod.getReturnType())) {
                    return true;
                }
            }
            return false;
        }

        private Method findMethodWithConversion(Collection<Method> setterMethods, Object value) throws Exception {
            ExecutionContext ctx = ExecutionContext.Holder.getContext();
            List<Method> matchingMethods = new ArrayList<Method>();
            for (Method m : setterMethods) {
                Type paramType = m.getGenericParameterTypes()[0];
                if (ctx.canConvert(value, new GenericType(paramType))) matchingMethods.add(m);
            }
            
            if (matchingMethods.isEmpty()) return null;
            else if (matchingMethods.size() == 1) return matchingMethods.get(0);
            else throw new ComponentDefinitionException(
                    "Ambiguous setter method for property "+ getName() + 
                    ". More than one method matches the parameter "+value+" after applying conversion.");
        }
        
        public String toString() {
            return "PropertyDescriptor <name: "+getName()+", getter: "+getter+", setter: "+setters;
        }
    }

    public static Throwable getRealCause(Throwable t) {
        if (t instanceof InvocationTargetException && t.getCause() != null) {
            return t.getCause();
        }
        return t;
    }

}
