diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java index ffd766a13..28da62b5a 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java @@ -53,6 +53,7 @@ import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; @@ -101,7 +102,11 @@ import org.apache.openjpa.meta.ValueStrategies; import org.apache.openjpa.util.GeneralException; import org.apache.openjpa.util.InternalException; import org.apache.openjpa.util.InvalidStateException; +import org.apache.openjpa.util.LockException; +import org.apache.openjpa.util.ObjectExistsException; +import org.apache.openjpa.util.ObjectNotFoundException; import org.apache.openjpa.util.OpenJPAException; +import org.apache.openjpa.util.OptimisticException; import org.apache.openjpa.util.ReferentialIntegrityException; import org.apache.openjpa.util.Serialization; import org.apache.openjpa.util.StoreException; @@ -158,16 +163,6 @@ public class DBDictionary private static final String ZERO_TIMESTAMP_STR = "'" + new Timestamp(0) + "'"; - public static final List EMPTY_STRING_LIST = Arrays.asList(new String[]{}); - public static final List[] SQL_STATE_CODES = - {EMPTY_STRING_LIST, // 0: Default - Arrays.asList(new String[]{"41000"}), // 1: LOCK - EMPTY_STRING_LIST, // 2: OBJECT_NOT_FOUND - EMPTY_STRING_LIST, // 3: OPTIMISTIC - Arrays.asList(new String[]{"23000"}), // 4: REFERENTIAL_INTEGRITY - EMPTY_STRING_LIST // 5: OBJECT_EXISTS - }; - private static final Localizer _loc = Localizer.forPackage (DBDictionary.class); @@ -364,6 +359,9 @@ public class DBDictionary // any positive number = batch limit public int batchLimit = NO_BATCH; + public final Map> sqlStateCodes = + new HashMap>(); + public DBDictionary() { fixedSizeTypeNameSet.addAll(Arrays.asList(new String[]{ "BIGINT", "BIT", "BLOB", "CLOB", "DATE", "DECIMAL", "DISTINCT", @@ -4109,8 +4107,32 @@ public class DBDictionary if (selectWords != null) selectWordSet.addAll(Arrays.asList(Strings.split(selectWords .toUpperCase(), ",", 0))); + + // initialize the error codes + SQLErrorCodeReader codeReader = new SQLErrorCodeReader(); + String rsrc = "sql-error-state-codes.xml"; + InputStream stream = getClass().getResourceAsStream(rsrc); + String dictionaryClassName = getClass().getName(); + if (stream == null) { // User supplied dictionary but no error codes xml + stream = DBDictionary.class.getResourceAsStream(rsrc); // use default + dictionaryClassName = getClass().getSuperclass().getName(); + } + codeReader.parse(stream, dictionaryClassName, this); } - + + public void addErrorCode(int errorType, String errorCode) { + if (errorCode == null || errorCode.trim().length() == 0) + return; + Set codes = sqlStateCodes.get(errorType); + if (codes == null) { + codes = new HashSet(); + codes.add(errorCode.trim()); + sqlStateCodes.put(errorType, codes); + } else { + codes.add(errorCode.trim()); + } + } + ////////////////////////////////////// // ConnectionDecorator implementation ////////////////////////////////////// @@ -4119,7 +4141,7 @@ public class DBDictionary * Decorate the given connection if needed. Some databases require special * handling for JDBC bugs. This implementation issues any * {@link #initializationSQL} that has been set for the dictionary but - * does not decoreate the connection. + * does not decorate the connection. */ public Connection decorate(Connection conn) throws SQLException { @@ -4170,7 +4192,7 @@ public class DBDictionary public OpenJPAException newStoreException(String msg, SQLException[] causes, Object failed) { if (causes != null && causes.length > 0) { - OpenJPAException ret = SQLExceptions.narrow(msg, causes[0], this); + OpenJPAException ret = narrow(msg, causes[0]); ret.setFailedObject(failed).setNestedThrowables(causes); return ret; } @@ -4179,26 +4201,36 @@ public class DBDictionary } /** - * Gets the list of String, each represents an error that can help - * to narrow down a SQL exception to specific type of StoreException.
- * For example, error code "23000" represents referential - * integrity violation and hence can be narrowed down to - * {@link ReferentialIntegrityException} rather than more general - * {@link StoreException}.
- * JDBC Drivers are not uniform in return values of SQLState for the same - * error and hence each database specific Dictionary can specialize.
- * - * - * @return an unmodifiable list of Strings representing supposedly - * uniform SQL States for a given type of StoreException. - * Default behavior is to return an empty list. + * Gets the subtype of StoreException by matching the given SQLException's + * error state code to the list of error codes supplied by the dictionary. + * Returns -1 if no matching code can be found. */ - public List/**/ getSQLStates(int exceptionType) { - if (exceptionType>=0 && exceptionType erroStates = sqlStateCodes.get(type); + if (erroStates != null && erroStates.contains(errorState)) { + errorType = type; + break; + } + } + switch (errorType) { + case StoreException.LOCK: + return new LockException(msg); + case StoreException.OBJECT_EXISTS: + return new ObjectExistsException(msg); + case StoreException.OBJECT_NOT_FOUND: + return new ObjectNotFoundException(msg); + case StoreException.OPTIMISTIC: + return new OptimisticException(msg); + case StoreException.REFERENTIAL_INTEGRITY: + return new ReferentialIntegrityException(msg); + default: + return new StoreException(msg); + } } - + /** * Closes the specified {@link DataSource} and releases any * resources associated with it. diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java index f79307f88..8453fe020 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DerbyDictionary.java @@ -100,26 +100,4 @@ public class DerbyDictionary } } } - - /** - * Adds extra SQLState code that Derby JDBC Driver uses. In JDBC 4.0, - * SQLState will follow either XOPEN or SQL 2003 convention. A compliant - * driver can be queries via DatabaseMetaData.getSQLStateType() to detect - * the convention type.
- * This method is overwritten to highlight that a) the SQL State is ideally - * uniform across JDBC Drivers but not practically and b) the overwritten - * method must crate a new list to return as the super classes list is - * unmodifable. - */ - public List getSQLStates(int exceptionType) { - List original = super.getSQLStates(exceptionType); - if (exceptionType == StoreException.LOCK) { - // Can not add new codes to unmodifable list of the super class - List newStates = new ArrayList(original); - newStates.add("40XL1"); - return newStates; - } - return original; - } - } diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLErrorCodeReader.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLErrorCodeReader.java new file mode 100644 index 000000000..dec3e23f9 --- /dev/null +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLErrorCodeReader.java @@ -0,0 +1,136 @@ +package org.apache.openjpa.jdbc.sql; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; + +import org.apache.commons.lang.StringUtils; +import org.apache.openjpa.jdbc.conf.JDBCConfiguration; +import org.apache.openjpa.jdbc.sql.DBDictionary; +import org.apache.openjpa.lib.log.Log; +import org.apache.openjpa.lib.util.Localizer; +import org.apache.openjpa.lib.xml.XMLFactory; +import org.apache.openjpa.util.StoreException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Parses XML content of SQL Error State codes to populate errro codes for + * a given Database Dictionary. + * + * @author Pinaki Poddar + * + */ +public class SQLErrorCodeReader { + private Log log = null; + public static final String ERROR_CODE_DELIMITER = ","; + public static final Map storeErrorTypes = + new HashMap(); + static { + storeErrorTypes.put("lock", StoreException.LOCK); + storeErrorTypes.put("object-exists", StoreException.OBJECT_EXISTS); + storeErrorTypes + .put("object-not-found", StoreException.OBJECT_NOT_FOUND); + storeErrorTypes.put("optimistic", StoreException.OPTIMISTIC); + storeErrorTypes.put("referential-integrity", + StoreException.REFERENTIAL_INTEGRITY); + + } + + private static final Localizer _loc = + Localizer.forPackage(SQLErrorCodeReader.class); + + public List getDictionaries(InputStream in) { + List result = new ArrayList(); + DocumentBuilder builder = XMLFactory.getDOMParser(false, false); + try { + Document doc = builder.parse(in); + Element root = doc.getDocumentElement(); + NodeList nodes = root.getElementsByTagName("dictionary"); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node dictionary = attrs.getNamedItem("class"); + if (dictionary != null) { + result.add(dictionary.getNodeValue()); + } + } + } catch (Throwable e) { + if (log.isWarnEnabled()) { + log.error(_loc.get("error-code-parse-error")); + } + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + return result; + } + + /** + * Parses given stream of XML content for error codes of the given database + * dictionary name. Populates the given dictionary with the error codes. + * + */ + public void parse(InputStream in, String dictName, DBDictionary dict) { + if (in == null || dict == null) + return; + log = dict.conf.getLog(JDBCConfiguration.LOG_JDBC); + DocumentBuilder builder = XMLFactory.getDOMParser(false, false); + try { + Document doc = builder.parse(in); + Element root = doc.getDocumentElement(); + NodeList nodes = root.getElementsByTagName("dictionary"); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + NamedNodeMap attrs = node.getAttributes(); + Node dictionary = attrs.getNamedItem("class"); + if (dictionary != null + && dictionary.getNodeValue().equals(dictName)) { + readErrorCodes(node, dict); + } + } + } catch (Throwable e) { + if (log.isWarnEnabled()) { + log.error(_loc.get("error-code-parse-error")); + } + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + + static void readErrorCodes(Node node, DBDictionary dict) { + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + short nodeType = child.getNodeType(); + if (nodeType == Node.ELEMENT_NODE) { + String errorType = child.getNodeName(); + if (storeErrorTypes.containsKey(errorType)) { + String errorCodes = child.getTextContent(); + if (!StringUtils.isEmpty(errorCodes)) { + String[] codes = errorCodes.split(ERROR_CODE_DELIMITER); + for (String code : codes) { + dict.addErrorCode(storeErrorTypes.get(errorType), + code.trim()); + } + } + } + } + } + } +} diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLExceptions.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLExceptions.java index 134f02bdc..cfd6934e8 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLExceptions.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/SQLExceptions.java @@ -117,32 +117,4 @@ public class SQLExceptions { } return (SQLException[]) errs.toArray(new SQLException[errs.size()]); } - - /** - * Narrows the given SQLException to a specific type of - * {@link StoreException#getSubtype() StoreException} by analyzing the - * SQLState code supplied by SQLException. Each database-specific - * {@link DBDictionary dictionary} can supply a set of error codes that will - * map to a specific specific type of StoreException via - * {@link DBDictionary#getSQLStates(int) getSQLStates()} method. - * The default behavior is to return generic {@link StoreException - * StoreException}. - */ - public static OpenJPAException narrow(String msg, SQLException se, - DBDictionary dict) { - String e = se.getSQLState(); - if (dict.getSQLStates(StoreException.LOCK).contains(e)) - return new LockException(msg); - else if (dict.getSQLStates(StoreException.OBJECT_EXISTS).contains(e)) - return new ObjectExistsException(msg); - else if (dict.getSQLStates(StoreException.OBJECT_NOT_FOUND).contains(e)) - return new ObjectNotFoundException(msg); - else if (dict.getSQLStates(StoreException.OPTIMISTIC).contains(e)) - return new OptimisticException(msg); - else if (dict.getSQLStates(StoreException.REFERENTIAL_INTEGRITY) - .contains(e)) - return new ReferentialIntegrityException(msg); - else - return new StoreException(msg); - } } diff --git a/openjpa-jdbc/src/main/resources/org/apache/openjpa/jdbc/sql/sql-error-state-codes.xml b/openjpa-jdbc/src/main/resources/org/apache/openjpa/jdbc/sql/sql-error-state-codes.xml new file mode 100644 index 000000000..5a5327723 --- /dev/null +++ b/openjpa-jdbc/src/main/resources/org/apache/openjpa/jdbc/sql/sql-error-state-codes.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + -911,-913 + -407,-530,-531,-532,-543,-544,-545,-603,-667,-803 + + + + + + + 40001 + 22001,22005,23502,23503,23513,X0Y32 + 23505 + + 40XL1,40001 + + + + 1205 + 544,2601,2627,8114,8115 + 1205 + + + + 1205 + 423,511,515,530,547,2601,2615,2714 + + + 1205 + + + + 40001 + 22001,22005,23502,23503,23513,X0Y32 + 23505,456c + + 40XL1,40001 + + + + 40001 + 22001,22005,23502,23503,23513,X0Y32 + 23505,456c + + 40XL1,40001 + + + + 40001 + 22001,22005,23502,23503,23513,X0Y32 + 23505,456c + + 40XL1,40001 + + + + 40001 + 22001,22005,23502,23503,23513,X0Y32 + 23505,456c + + 40XL1,40001 + + + + 22003,22012,22025,23000,23001 + + + + -9 + + + + -239,-268,-692,-11030 + + + + + + + + + + + + + 1213 + 630,839,840,893,1062,1169,1215,1216,1217,1451,1452,1557 + 23000 + + 41000,1205,1213 + + + + + 1,1400,1722,2291,2292 + + + + + + + + 22001,22005,23502,23503,23513,X0Y32 + + + + + + + 55P03,40P01 + 23000,23502,23503,23505,23514 + + + 55P03 + + + \ No newline at end of file diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/TestDataCacheBehavesIdentical.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/TestDataCacheBehavesIdentical.java index a2b151193..0e2b7566a 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/TestDataCacheBehavesIdentical.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/TestDataCacheBehavesIdentical.java @@ -30,7 +30,7 @@ import org.apache.openjpa.persistence.StoreCacheImpl; import org.apache.openjpa.persistence.cache.common.apps.BidirectionalOne2OneOwned; import org.apache.openjpa.persistence.cache.common.apps.BidirectionalOne2OneOwner; import org.apache.openjpa.persistence.common.utils.AbstractTestCase; -import org.apache.openjpa.persistence.exception.PObject; +import org.apache.openjpa.persistence.datacache.common.apps.PObject; /** * Tests various application behavior with or without DataCache. diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/common/apps/PObject.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/common/apps/PObject.java new file mode 100644 index 000000000..8d9bf7c9c --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/datacache/common/apps/PObject.java @@ -0,0 +1,57 @@ +/* + * 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.openjpa.persistence.datacache.common.apps; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Version; + +/** + * A Simple entity for testing. Has a version field for testing optimistic + * concurrent usage. + * + * @author Pinaki Poddar + * + */ +@Entity +public class PObject { + @Id + @GeneratedValue + private long id; + private String name; + @Version + private int version; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public int getVersion() { + return version; + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/PObject.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/PObject.java index f079f9a9b..9239d14ec 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/PObject.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/PObject.java @@ -33,7 +33,6 @@ import javax.persistence.Version; @Entity public class PObject { @Id - @GeneratedValue private long id; private String name; @Version @@ -47,6 +46,10 @@ public class PObject { this.name = name; } + public void setId(long id) { + this.id = id; + } + public long getId() { return id; } diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/TestException.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/TestException.java index 733a93876..33ebe5606 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/TestException.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/exception/TestException.java @@ -18,17 +18,32 @@ */ package org.apache.openjpa.persistence.exception; +import java.io.InputStream; import java.sql.SQLException; +import java.util.List; +import javax.persistence.EntityExistsException; import javax.persistence.EntityManager; +import javax.persistence.EntityNotFoundException; import javax.persistence.OptimisticLockException; +import javax.persistence.TransactionRequiredException; + +import org.apache.openjpa.jdbc.sql.DBDictionary; +import org.apache.openjpa.jdbc.sql.SQLErrorCodeReader; import org.apache.openjpa.persistence.test.SingleEMFTestCase; /** * Tests proper JPA exceptions are raised by the implementation. + * Actual runtime type of the raised exception is a subclass of JPA-defined + * exception. + * The raised exception may nest the expected exception. + * + * @author Pinaki Poddar */ public class TestException extends SingleEMFTestCase { - public void setUp() { + private static long ID_COUNTER = System.currentTimeMillis(); + + public void setUp() { super.setUp(PObject.class, CLEAR_TABLES); } @@ -36,16 +51,17 @@ public class TestException extends SingleEMFTestCase { * Tests that when Optimistic transaction consistency is violated, the * exception thrown is an instance of javax.persistence.OptimisticException. */ - public void testThrowsJPADefinedOptimisticException() { + public void testThrowsOptimisticException() { EntityManager em1 = emf.createEntityManager(); EntityManager em2 = emf.createEntityManager(); assertNotEquals(em1, em2); em1.getTransaction().begin(); PObject pc = new PObject(); + long id = ++ID_COUNTER; + pc.setId(id); em1.persist(pc); em1.getTransaction().commit(); - Object id = pc.getId(); em1.clear(); em1.getTransaction().begin(); @@ -61,47 +77,121 @@ public class TestException extends SingleEMFTestCase { try { pc2.setName("Modified in TXN2"); em2.flush(); - fail("Expected optimistic exception on flush"); + fail("Expected " + OptimisticLockException.class); } catch (Throwable t) { - if (!isExpectedException(t, OptimisticLockException.class)) { - print(t); - fail(t.getCause().getClass() + " is not " + - OptimisticLockException.class); - } + assertException(t, OptimisticLockException.class); } em1.getTransaction().commit(); try { em2.getTransaction().commit(); - fail("Expected optimistic exception on commit"); + fail("Expected " + OptimisticLockException.class); } catch (Throwable t) { - if (!isExpectedException(t, OptimisticLockException.class)) { - print(t); - fail(t.getCause().getClass() + " is not " + - OptimisticLockException.class); + assertException(t, OptimisticLockException.class); + } + } + + public void testThrowsEntityExistsException() { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + PObject pc = new PObject(); + long id = ++ID_COUNTER; + pc.setId(id); + em.persist(pc); + em.getTransaction().commit(); + em.clear(); + + em.getTransaction().begin(); + PObject pc2 = new PObject(); + pc2.setId(id); + em.persist(pc2); + try { + em.getTransaction().commit(); + fail("Expected " + EntityExistsException.class); + } catch (Throwable t) { + assertException(t, EntityExistsException.class); + } + } + + public void testThrowsEntityNotFoundException() { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + PObject pc = new PObject(); + long id = ++ID_COUNTER; + pc.setId(id); + em.persist(pc); + em.getTransaction().commit(); + + EntityManager em2 = emf.createEntityManager(); + em2.getTransaction().begin(); + PObject pc2 = em2.find(PObject.class, id); + assertNotNull(pc2); + em2.remove(pc2); + em2.getTransaction().commit(); + + try { + em.refresh(pc); + fail("Expected " + EntityNotFoundException.class); + } catch (Throwable t) { + assertException(t, EntityNotFoundException.class); + } + } + + public void testErrorCodeConfigurationHasAllKnownDictionaries() { + SQLErrorCodeReader reader = new SQLErrorCodeReader(); + InputStream in = DBDictionary.class.getResourceAsStream + ("sql-error-state-codes.xml"); + assertNotNull(in); + List names = reader.getDictionaries(in); + assertTrue(names.size()>=18); + for (String name:names) { + try { + Class.forName(name, false, Thread.currentThread() + .getContextClassLoader()); + } catch (Throwable t) { + fail("DB dictionary " + name + " can not be loaded"); + t.printStackTrace(); } } } - boolean isExpectedException(Throwable t, Class expectedType) { - if (t == null) return false; - if (expectedType.isAssignableFrom(t.getClass())) - return true; - if (t.getCause()==t) return false; - return isExpectedException(t.getCause(), expectedType); + /** + * Asserts that the given expected type of the exception is equal to or a + * subclass of the given throwable or any of its nested exception. + * Otherwise fails assertion and prints the given throwable and its nested + * exception on the console. + */ + void assertException(Throwable t, Class expectedType) { + if (!isExpectedException(t, expectedType)) { + t.printStackTrace(); + print(t, 0); + fail(t + " or its cause is not instanceof " + expectedType); + } } - void print(Throwable t) { - print(t, 0); + /** + * Affirms if the given expected type of the exception is equal to or a + * subclass of the given throwable or any of its nested exception. + */ + boolean isExpectedException(Throwable t, Class expectedType) { + if (t == null) + return false; + if (expectedType.isAssignableFrom(t.getClass())) + return true; + return isExpectedException(t.getCause(), expectedType); } void print(Throwable t, int tab) { if (t == null) return; for (int i=0; i