1
0
mirror of synced 2026-08-05 09:47:05 +00:00

SEC-1132: Moved ThrowableAnalyzer code to web module as it is only used in ExceptionTranslationFilter

This commit is contained in:
Luke Taylor
2009-04-25 07:03:15 +00:00
parent 1ac0ea9d3f
commit a76cbee4bc
4 changed files with 12 additions and 9 deletions
@@ -1,243 +0,0 @@
package org.springframework.security.util;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.springframework.util.Assert;
/**
* 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<? extends Throwable>> CLASS_HIERARCHY_COMPARATOR =
new Comparator<Class<? extends Throwable>>() {
public int compare(Class<? extends Throwable> class1, Class<? extends Throwable> class2) {
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<Class<? extends Throwable>, ThrowableCauseExtractor> extractorMap;
/**
* Creates a new <code>ThrowableAnalyzer</code> instance.
*/
public ThrowableAnalyzer() {
this.extractorMap = new TreeMap<Class<? extends Throwable>, ThrowableCauseExtractor>(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<? extends Throwable> throwableType, ThrowableCauseExtractor extractor) {
Assert.notNull(extractor, "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
*/
@SuppressWarnings("unchecked")
final Class<? extends Throwable>[] getRegisteredTypes() {
Set<Class<? extends Throwable>> typeList = this.extractorMap.keySet();
return 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<Throwable> chain = new ArrayList<Throwable>();
Throwable currentThrowable = throwable;
while (currentThrowable != null) {
chain.add(currentThrowable);
currentThrowable = extractCause(currentThrowable);
}
return 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 (Map.Entry<Class<? extends Throwable>, ThrowableCauseExtractor> entry : extractorMap.entrySet()) {
Class<? extends Throwable> throwableType = 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<? extends Throwable> throwableType, Throwable[] chain) {
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;
}
/**
* 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<? extends Throwable> expectedBaseType) {
if (expectedBaseType == null) {
return;
}
if (throwable == null) {
throw new IllegalArgumentException("Invalid throwable: null");
}
Class<? extends Throwable> throwableType = throwable.getClass();
if (!expectedBaseType.isAssignableFrom(throwableType)) {
throw new IllegalArgumentException("Invalid type: '"
+ throwableType.getName()
+ "'. Has to be a subclass of '" + expectedBaseType.getName() + "'");
}
}
}
@@ -1,25 +0,0 @@
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,254 +0,0 @@
package org.springframework.security.util;
import java.lang.reflect.InvocationTargetException;
import junit.framework.TestCase;
/**
* Test cases for {@link ThrowableAnalyzer}.
*
* @author Andreas Senft
*/
@SuppressWarnings("unchecked")
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 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
}
}
}