浏览代码

SEC-300: Applied Andreas Senft's patch for unwrapping exceptions in ExceptionTranslationFilter to obtain the cause.

Luke Taylor 17 年之前
父节点
当前提交
311add2270

+ 2 - 2
core/src/main/java/org/springframework/security/SpringSecurityException.java

@@ -28,7 +28,7 @@ import org.springframework.core.NestedRuntimeException;
 public abstract class SpringSecurityException extends NestedRuntimeException {
     //~ Constructors ===================================================================================================
 
-/**
+    /**
      * Constructs an <code>SpringSecurityException</code> with the specified
      * message and root cause.
      *
@@ -39,7 +39,7 @@ public abstract class SpringSecurityException extends NestedRuntimeException {
         super(msg, t);
     }
 
-/**
+    /**
      * Constructs an <code>SpringSecurityException</code> with the specified
      * message and no root cause.
      *

+ 57 - 20
core/src/main/java/org/springframework/security/ui/ExceptionTranslationFilter.java

@@ -25,6 +25,8 @@ import org.springframework.security.context.SecurityContextHolder;
 import org.springframework.security.ui.savedrequest.SavedRequest;
 import org.springframework.security.util.PortResolver;
 import org.springframework.security.util.PortResolverImpl;
+import org.springframework.security.util.ThrowableAnalyzer;
+import org.springframework.security.util.ThrowableCauseExtractor;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -92,7 +94,8 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements
 	private AuthenticationEntryPoint authenticationEntryPoint;
 	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
 	private PortResolver portResolver = new PortResolverImpl();
-	private boolean createSessionAllowed = true;
+    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();    
+    private boolean createSessionAllowed = true;
 
 	//~ Methods ========================================================================================================
 
@@ -100,7 +103,8 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements
 		Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint must be specified");
 		Assert.notNull(portResolver, "portResolver must be specified");
 		Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must be specified");
-	}
+        Assert.notNull(throwableAnalyzer, "throwableAnalyzer must be specified");        
+    }
 
 	public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
 			ServletException {
@@ -112,24 +116,31 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements
 				logger.debug("Chain processed normally");
 			}
 		}
-		catch (AuthenticationException ex) {
-			handleException(request, response, chain, ex);
-		}
-		catch (AccessDeniedException ex) {
-			handleException(request, response, chain, ex);
-		}
-		catch (ServletException ex) {
-			if (ex.getRootCause() instanceof AuthenticationException
-					|| ex.getRootCause() instanceof AccessDeniedException) {
-				handleException(request, response, chain, (SpringSecurityException) ex.getRootCause());
-			}
-			else {
-				throw ex;
-			}
-		}
-		catch (IOException ex) {
-			throw ex;
-		}
+        catch (IOException ex) {
+            throw ex;
+        }
+        catch (Exception ex) {
+            // Try to extract a SpringSecurityException from the stacktrace
+            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
+            SpringSecurityException ase = (SpringSecurityException)
+                    this.throwableAnalyzer.getFirstThrowableOfType(SpringSecurityException.class, causeChain);
+
+            if (ase != null) {
+                handleException(request, response, chain, ase);
+            }
+            else {
+                // Rethrow ServletExceptions and RuntimeExceptions as-is
+                if (ex instanceof ServletException) {
+                    throw (ServletException) ex;
+                }
+                else if (ex instanceof RuntimeException) {
+                    throw (RuntimeException) ex;
+                }
+
+                // Wrap other Exceptions. These are not expected to happen
+                throw new RuntimeException(ex);
+            }
+        }
 	}
 
 	public AuthenticationEntryPoint getAuthenticationEntryPoint() {
@@ -235,7 +246,33 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements
 		this.portResolver = portResolver;
 	}
 
+    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
+        this.throwableAnalyzer = throwableAnalyzer;
+    }    
+
     public int getOrder() {
         return FilterChainOrder.EXCEPTION_TRANSLATION_FILTER;
     }
+
+    /**
+     * Default implementation of <code>ThrowableAnalyzer</code> which is capable of also unwrapping
+     * <code>ServletException</code>s.
+     */
+    private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
+        /**
+         * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
+         */
+        protected void initExtractorMap() {
+            super.initExtractorMap();
+
+            registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
+                public Throwable extractCause(Throwable throwable) {
+                    ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
+                    return ((ServletException) throwable).getRootCause();
+                }
+            });
+        }
+
+    }
+
 }

+ 270 - 0
core/src/main/java/org/springframework/security/util/ThrowableAnalyzer.java

@@ -0,0 +1,270 @@
+package org.springframework.security.util;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Handler for analyzing {@link Throwable} instances.
+ *
+ * Can be subclassed to customize its behavior.
+ * 
+ * @author Andreas Senft
+ * @since 2.0
+ * @version $Id$
+ */
+public class ThrowableAnalyzer {
+
+    /**
+     * Default extractor for {@link Throwable} instances.
+     * 
+     * @see Throwable#getCause()
+     */
+    public static final ThrowableCauseExtractor DEFAULT_EXTRACTOR
+        = new ThrowableCauseExtractor() {
+            public Throwable extractCause(Throwable throwable) {
+                return throwable.getCause();
+            }
+        };
+    
+    /**
+     * Default extractor for {@link InvocationTargetException} instances.
+     * 
+     * @see InvocationTargetException#getTargetException()
+     */
+    public static final ThrowableCauseExtractor INVOCATIONTARGET_EXTRACTOR 
+        = new ThrowableCauseExtractor() {
+            public Throwable extractCause(Throwable throwable) {
+                verifyThrowableHierarchy(throwable, InvocationTargetException.class);
+                return ((InvocationTargetException) throwable).getTargetException();
+            }
+        };
+
+    /**
+     * Comparator to order classes ascending according to their hierarchy relation.
+     * If two classes have a hierarchical relation, the "higher" class is considered 
+     * to be greater by this comparator.<br>
+     * For hierarchically unrelated classes their fully qualified name will be compared. 
+     */
+    private static final Comparator CLASS_HIERARCHY_COMPARATOR = new Comparator() {
+
+        public int compare(Object o1, Object o2) {
+            Class class1 = (Class) o1;
+            Class class2 = (Class) o2;
+            
+            if (class1.isAssignableFrom(class2)) {
+                return 1;
+            } else if (class2.isAssignableFrom(class1)) {
+                return -1;
+            } else {
+                return class1.getName().compareTo(class2.getName());
+            }
+        }
+        
+    };
+        
+
+    /**
+     * Map of registered cause extractors.
+     * key: Class<Throwable>; value: ThrowableCauseExctractor
+     */
+    private final Map extractorMap;
+    
+    
+    /**
+     * Creates a new <code>ThrowableAnalyzer</code> instance.
+     */
+    public ThrowableAnalyzer() {
+        this.extractorMap = new TreeMap(CLASS_HIERARCHY_COMPARATOR);
+        
+        initExtractorMap();
+    }
+    
+    /**
+     * Registers a <code>ThrowableCauseExtractor</code> for the specified type.
+     * <i>Can be used in subclasses overriding {@link #initExtractorMap()}.</i>
+     * 
+     * @param throwableType the type (has to be a subclass of <code>Throwable</code>)
+     * @param extractor the associated <code>ThrowableCauseExtractor</code> (not <code>null</code>)
+     * 
+     * @throws IllegalArgumentException if one of the arguments is invalid
+     */
+    protected final void registerExtractor(Class throwableType, ThrowableCauseExtractor extractor) {
+        verifyThrowableType(throwableType);
+
+        if (extractor == null) {
+            throw new IllegalArgumentException("Invalid extractor: null");
+        }
+
+        this.extractorMap.put(throwableType, extractor);
+    }
+
+    /**
+     * Initializes associations between <code>Throwable</code>s and <code>ThrowableCauseExtractor</code>s.
+     * The default implementation performs the following registrations:
+     * <li>{@link #DEFAULT_EXTRACTOR} for {@link Throwable}</li>
+     * <li>{@link #INVOCATIONTARGET_EXTRACTOR} for {@link InvocationTargetException}</li>
+     * <br>
+     * Subclasses overriding this method are encouraged to invoke the super method to perform the
+     * default registrations. They can register additional extractors as required.
+     * <p>
+     * Note: An extractor registered for a specific type is applicable for that type <i>and all subtypes thereof</i>.
+     * However, extractors registered to more specific types are guaranteed to be resolved first.
+     * So in the default case InvocationTargetExceptions will be handled by {@link #INVOCATIONTARGET_EXTRACTOR}
+     * while all other throwables are handled by {@link #DEFAULT_EXTRACTOR}.
+     * 
+     * @see #registerExtractor(Class, ThrowableCauseExtractor)
+     */
+    protected void initExtractorMap() {
+        registerExtractor(InvocationTargetException.class, INVOCATIONTARGET_EXTRACTOR);
+        registerExtractor(Throwable.class, DEFAULT_EXTRACTOR);
+    }
+    
+    /**
+     * Returns an array containing the classes for which extractors are registered.
+     * The order of the classes is the order in which comparisons will occur for
+     * resolving a matching extractor.
+     * 
+     * @return the types for which extractors are registered
+     */
+    final Class[] getRegisteredTypes() {
+        List typeList = new ArrayList(this.extractorMap.keySet());
+        return (Class[]) typeList.toArray(new Class[typeList.size()]);
+    }
+    
+    /**
+     * Determines the cause chain of the provided <code>Throwable</code>.
+     * The returned array contains all throwables extracted from the stacktrace, using the registered
+     * {@link ThrowableCauseExtractor extractors}. The elements of the array are ordered:
+     * The first element is the passed in throwable itself. The following elements
+     * appear in their order downward the stacktrace.
+     * <p>
+     * Note: If no {@link ThrowableCauseExtractor} is registered for this instance 
+     * then the returned array will always only contain the passed in throwable.
+     * 
+     * @param throwable the <code>Throwable</code> to analyze
+     * @return an array of all determined throwables from the stacktrace
+     * 
+     * @throws IllegalArgumentException if the throwable is <code>null</code>
+     * 
+     * @see #initExtractorMap()
+     */
+    public final Throwable[] determineCauseChain(Throwable throwable) {
+        if (throwable == null) {
+            throw new IllegalArgumentException("Invalid throwable: null");
+        }
+        
+        List chain = new ArrayList();
+        Throwable currentThrowable = throwable;
+        
+        while (currentThrowable != null) {
+            chain.add(currentThrowable);
+            currentThrowable = extractCause(currentThrowable);
+        }
+        
+        return (Throwable[]) chain.toArray(new Throwable[chain.size()]);
+    }
+    
+    /**
+     * Extracts the cause of the given throwable using an appropriate extractor.
+     * 
+     * @param throwable the <code>Throwable</code> (not <code>null</code>
+     * @return the cause, may be <code>null</code> if none could be resolved
+     */
+    private Throwable extractCause(Throwable throwable) {
+        for (Iterator iter = this.extractorMap.entrySet().iterator(); iter.hasNext(); ) {
+            Map.Entry entry = (Map.Entry) iter.next();
+            
+            Class throwableType = (Class) entry.getKey();
+            if (throwableType.isInstance(throwable)) {
+                ThrowableCauseExtractor extractor = (ThrowableCauseExtractor) entry.getValue();
+                return extractor.extractCause(throwable);
+            }
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Returns the first throwable from the passed in array that is assignable to the provided type.
+     * A returned instance is safe to be cast to the specified type.
+     * <p>
+     * If the passed in array is null or empty this method returns <code>null</code>.
+     * 
+     * @param throwableType the type to look for
+     * @param chain the array (will be processed in element order)
+     * @return the found <code>Throwable</code>, <code>null</code> if not found
+     * 
+     * @throws IllegalArgumentException if the provided type is <code>null</code> 
+     * or no subclass of <code>Throwable</code>
+     */
+    public final Throwable getFirstThrowableOfType(Class throwableType, Throwable[] chain) {
+        verifyThrowableType(throwableType);
+        
+        if (chain != null) {
+            for (int i = 0; i < chain.length; ++i) {
+                Throwable t = chain[i];
+                
+                if ((t != null) && throwableType.isInstance(t)) {
+                    return t;
+                }
+            }
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Convenience method for verifying that the passed in class refers to a valid 
+     * subclass of <code>Throwable</code>.
+     * 
+     * @param throwableType the type to check
+     * 
+     * @throws IllegalArgumentException if <code>typeToCheck</code> is either <code>null</code>
+     * or not assignable to <code>expectedBaseType</code>
+     */
+    private static void verifyThrowableType(Class throwableType) {
+        if (throwableType == null) {
+            throw new IllegalArgumentException("Invalid type: null");
+        }
+        if (!Throwable.class.isAssignableFrom(throwableType)) {
+            throw new IllegalArgumentException("Invalid type: '" 
+                    + throwableType.getName() 
+                    + "'. Has to be a subclass of '" + Throwable.class.getName() + "'");
+        }
+    }
+    
+    /**
+     * Verifies that the provided throwable is a valid subclass of the provided type (or of the type itself).
+     * If <code>expectdBaseType</code> is <code>null</code>, no check will be performed.
+     * <p>
+     * Can be used for verification purposes in implementations 
+     * of {@link ThrowableCauseExtractor extractors}.
+     * 
+     * @param throwable the <code>Throwable</code> to check
+     * @param expectedBaseType the type to check against
+     * 
+     * @throws IllegalArgumentException if <code>throwable</code> is either <code>null</code>
+     * or its type is not assignable to <code>expectedBaseType</code>
+     */
+    public static final void verifyThrowableHierarchy(Throwable throwable, Class expectedBaseType) {
+        if (expectedBaseType == null) {
+            return;
+        }
+        
+        if (throwable == null) {
+            throw new IllegalArgumentException("Invalid throwable: null");
+        }
+        Class throwableType = throwable.getClass();
+        
+        if (!expectedBaseType.isAssignableFrom(throwableType)) {
+            throw new IllegalArgumentException("Invalid type: '" 
+                    + throwableType.getName() 
+                    + "'. Has to be a subclass of '" + expectedBaseType.getName() + "'");
+        }
+    }
+}

+ 25 - 0
core/src/main/java/org/springframework/security/util/ThrowableCauseExtractor.java

@@ -0,0 +1,25 @@
+package org.springframework.security.util;
+
+
+/**
+ * Interface for handlers extracting the cause out of a specific {@link Throwable} type.
+ * 
+ * @author Andreas Senft
+ * @since 2.0
+ * @version $Id$
+ * 
+ * @see ThrowableAnalyzer
+ */
+public interface ThrowableCauseExtractor {
+
+    /**
+     * Extracts the cause from the provided <code>Throwable</code>.
+     * 
+     * @param throwable the <code>Throwable</code>
+     * @return the extracted cause (maybe <code>null</code>)
+     * 
+     * @throws IllegalArgumentException if <code>throwable</code> is <code>null</code> 
+     * or otherwise considered invalid for the implementation
+     */
+    Throwable extractCause(Throwable throwable);
+}

+ 1 - 13
core/src/test/java/org/springframework/security/ui/ExceptionTranslationFilterTests.java

@@ -46,19 +46,8 @@ import javax.servlet.ServletResponse;
  * benalex $
  */
 public class ExceptionTranslationFilterTests extends TestCase {
-	//~ Constructors ===================================================================================================
-
-	public ExceptionTranslationFilterTests() {
-		super();
-	}
-
-	public ExceptionTranslationFilterTests(String arg0) {
-		super(arg0);
-	}
-
 	//~ Methods ========================================================================================================
 
-
 	protected void tearDown() throws Exception {
 		super.tearDown();
 		SecurityContextHolder.clearContext();
@@ -312,8 +301,7 @@ public class ExceptionTranslationFilterTests extends TestCase {
 		}
 	}
 
-	// ~ Inner Classes
-	// ==================================================================================================
+	// ~ Inner Classes =================================================================================================
 
 	private class MockFilterChain implements FilterChain {
 		private boolean throwAccessDenied;

+ 274 - 0
core/src/test/java/org/springframework/security/util/ThrowableAnalyzerTests.java

@@ -0,0 +1,274 @@
+package org.springframework.security.util;
+
+import java.lang.reflect.InvocationTargetException;
+
+import junit.framework.TestCase;
+
+/**
+ * Testcases for {@link ThrowableAnalyzer}.
+ *
+ * @author Andreas Senft
+ */
+public class ThrowableAnalyzerTests extends TestCase {
+
+    /**
+     * Exception for testing purposes. The cause is not retrievable by {@link #getCause()}.
+     */
+    public static final class NonStandardException extends Exception {
+
+        private Throwable cause;
+
+        public NonStandardException(String message, Throwable cause) {
+            super(message);
+            this.cause = cause;
+        }
+
+        public Throwable resolveCause() {
+            return this.cause;
+        }
+    }
+
+    /**
+     * <code>ThrowableCauseExtractor</code> for handling <code>NonStandardException</code> instances.
+     */
+    public static final class NonStandardExceptionCauseExtractor implements ThrowableCauseExtractor {
+
+        public Throwable extractCause(Throwable throwable) {
+            ThrowableAnalyzer.verifyThrowableHierarchy(throwable, NonStandardException.class);
+            return ((NonStandardException) throwable).resolveCause();
+        }
+
+    }
+
+
+    /**
+     * An array of nested throwables for testing.
+     * The cause of element 0 is element 1, the cause of element 1 is element 2 and so on.
+     */
+    private Throwable[] testTrace;
+
+    /**
+     * Plain <code>ThrowableAnalyzer</code>.
+     */
+    private ThrowableAnalyzer standardAnalyzer;
+
+    /**
+     * Enhanced <code>ThrowableAnalyzer</code> capable to process
+     * <code>NonStandardException</code>s.
+     */
+    private ThrowableAnalyzer nonstandardAnalyzer;
+
+    /**
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+       // Set up test trace
+       this.testTrace = new Throwable[7];
+       this.testTrace[6] = new IllegalArgumentException("Test_6");
+       this.testTrace[5] = new Throwable("Test_5",this.testTrace[6]);
+       this.testTrace[4] = new InvocationTargetException(this.testTrace[5], "Test_4");
+       this.testTrace[3] = new Exception("Test_3", this.testTrace[4]);
+       this.testTrace[2] = new NonStandardException("Test_2", this.testTrace[3]);
+       this.testTrace[1] = new RuntimeException("Test_1", this.testTrace[2]);
+       this.testTrace[0] = new Exception("Test_0", this.testTrace[1]);
+
+       // Set up standard analyzer
+       this.standardAnalyzer = new ThrowableAnalyzer();
+
+       // Set up nonstandard analyzer
+       this.nonstandardAnalyzer = new ThrowableAnalyzer() {
+           /**
+            * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
+            */
+           @Override
+           protected void initExtractorMap() {
+               super.initExtractorMap();
+               // register extractor for NonStandardException
+               registerExtractor(NonStandardException.class, new NonStandardExceptionCauseExtractor());
+           }
+       };
+    }
+
+
+    /**
+     * @see junit.framework.TestCase#tearDown()
+     */
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+
+    public void testRegisterExtractorWithInvalidClass() {
+        try {
+            new ThrowableAnalyzer() {
+
+                /**
+                 * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
+                 */
+                @Override
+                protected void initExtractorMap() {
+                    // Object is no subclass of Throwable
+                    super.registerExtractor(Object.class, DEFAULT_EXTRACTOR);
+                }
+            };
+
+            fail("IllegalArgumentExpected");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+    }
+
+    public void testRegisterExtractorWithInvalidExtractor() {
+        try {
+            new ThrowableAnalyzer() {
+
+                /**
+                 * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
+                 */
+                @Override
+                protected void initExtractorMap() {
+                    // null is no valid extractor
+                    super.registerExtractor(Exception.class, null);
+                }
+            };
+
+            fail("IllegalArgumentExpected");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+    }
+
+    public void testGetRegisteredTypes() {
+
+        Class[] registeredTypes = this.nonstandardAnalyzer.getRegisteredTypes();
+
+        for (int i = 0; i < registeredTypes.length; ++i) {
+            Class clazz = registeredTypes[i];
+
+            // The most specific types have to occur first.
+            for (int j = 0; j < i; ++j) {
+                Class prevClazz = registeredTypes[j];
+
+                assertFalse("Unexpected order of registered classes: "
+                        + prevClazz + " is assignable from " + clazz, prevClazz.isAssignableFrom(clazz));
+            }
+        }
+    }
+
+    public void testDetermineCauseChainWithNoExtractors() {
+        ThrowableAnalyzer analyzer = new ThrowableAnalyzer() {
+
+            /**
+             * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
+             */
+            @Override
+            protected void initExtractorMap() {
+                // skip default initialization
+            }
+        };
+
+        assertEquals("Unexpected number of registered types", 0, analyzer.getRegisteredTypes().length);
+
+        Throwable t = this.testTrace[0];
+        Throwable[] chain = analyzer.determineCauseChain(t);
+        // Without extractors only the root throwable is available
+        assertEquals("Unexpected chain size", 1, chain.length);
+        assertEquals("Unexpected chain entry", t, chain[0]);
+    }
+
+    public void testDetermineCauseChainWithDefaultExtractors() {
+        ThrowableAnalyzer analyzer = this.standardAnalyzer;
+
+        assertEquals("Unexpected number of registered types", 2, analyzer.getRegisteredTypes().length);
+
+        Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]);
+
+        // Element at index 2 is a NonStandardException which cannot be analyzed further by default
+        assertEquals("Unexpected chain size", 3, chain.length);
+        for (int i = 0; i < 3; ++i) {
+            assertEquals("Unexpected chain entry: " + i, this.testTrace[i], chain[i]);
+        }
+    }
+
+    public void testDetermineCauseChainWithCustomExtractors() {
+        ThrowableAnalyzer analyzer = this.nonstandardAnalyzer;
+
+        Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]);
+
+        assertEquals("Unexpected chain size", this.testTrace.length, chain.length);
+        for (int i = 0; i < chain.length; ++i) {
+            assertEquals("Unexpected chain entry: " + i, this.testTrace[i], chain[i]);
+        }
+    }
+
+    public void testGetFirstThrowableOfTypeWithSuccess1() {
+        ThrowableAnalyzer analyzer = this.nonstandardAnalyzer;
+
+        Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]);
+
+        Throwable result = analyzer.getFirstThrowableOfType(Exception.class, chain);
+
+        assertNotNull("null not expected", result);
+        assertEquals("Unexpected throwable found", this.testTrace[0], result);
+    }
+
+    public void testGetFirstThrowableOfTypeWithSuccess2() {
+        ThrowableAnalyzer analyzer = this.nonstandardAnalyzer;
+
+        Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]);
+
+        Throwable result = analyzer.getFirstThrowableOfType(NonStandardException.class, chain);
+
+        assertNotNull("null not expected", result);
+        assertEquals("Unexpected throwable found", this.testTrace[2], result);
+    }
+
+    public void testGetFirstThrowableOfTypeWithFailure() {
+        ThrowableAnalyzer analyzer = this.nonstandardAnalyzer;
+
+        Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]);
+
+        // IllegalStateException not in trace
+        Throwable result = analyzer.getFirstThrowableOfType(IllegalStateException.class, chain);
+
+        assertNull("null expected", result);
+    }
+
+    public void testVerifyThrowableHierarchyWithExactType() {
+
+        Throwable throwable = new IllegalStateException("Test");
+        ThrowableAnalyzer.verifyThrowableHierarchy(throwable, IllegalStateException.class);
+        // No exception expected
+    }
+
+    public void testVerifyThrowableHierarchyWithCompatibleType() {
+
+        Throwable throwable = new IllegalStateException("Test");
+        ThrowableAnalyzer.verifyThrowableHierarchy(throwable, Exception.class);
+        // No exception expected
+    }
+
+    public void testVerifyThrowableHierarchyWithNull() {
+        try {
+            ThrowableAnalyzer.verifyThrowableHierarchy(null, Throwable.class);
+            fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+    }
+
+    public void testVerifyThrowableHierarchyWithNonmatchingType() {
+
+        Throwable throwable = new IllegalStateException("Test");
+        try {
+            ThrowableAnalyzer.verifyThrowableHierarchy(throwable, InvocationTargetException.class);
+            fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+    }
+}