diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd757bb..06b7af82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v5.1.21 +------ +* Add methods `allMultiCauses` and `anyMultiCause` to retrieve all causes and the first non-MultiException cause, respectively. + v5.1.20 ------ * Upgrade bytebuddy and asm version for JDK 17 and JDK 21 support diff --git a/gradle.properties b/gradle.properties index b2832557..68abaf48 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=5.1.20 +version=5.1.21 group=com.linkedin.parseq org.gradle.parallel=true diff --git a/subprojects/parseq/src/main/java/com/linkedin/parseq/Exceptions.java b/subprojects/parseq/src/main/java/com/linkedin/parseq/Exceptions.java index 8568f68f..0957b349 100644 --- a/subprojects/parseq/src/main/java/com/linkedin/parseq/Exceptions.java +++ b/subprojects/parseq/src/main/java/com/linkedin/parseq/Exceptions.java @@ -2,8 +2,17 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; import java.util.NoSuchElementException; +import java.util.Stack; import java.util.concurrent.TimeoutException; @@ -72,4 +81,85 @@ public static Exception timeoutException(String desc) { } } + /** + * Returns whether the given exception is a {@link MultiException}. + */ + public static boolean isMultiple(final Throwable e) { + return e instanceof MultiException; + } + + /** + * Returns all causes of the given exception. + * If the exception is a {@link MultiException}, it will return a list of all causes. + * If the exception is not a {@link MultiException} and not null, it will return a list with the exception; otherwise, it will return an empty list. + * + * @param e the exception to search, nullable + * @return a list of all causes, or an empty list if none found, never null + */ + public static List allMultiCauses(final Throwable e) { + if (e == null) { + return Collections.emptyList(); + } + if (!isMultiple(e)) { + return Collections.singletonList(e); + } + + Deque meStack = new ArrayDeque<>(); + meStack.push((MultiException) e); + List causes = new ArrayList<>(); + + while (!meStack.isEmpty()) { + MultiException me = meStack.pop(); + if (me.getCauses() == null) { + continue; + } + + for (Throwable cause : me.getCauses()) { + if (cause == null) { + continue; + } + if (isMultiple(cause)) { + meStack.push((MultiException) cause); + } else { + causes.add(cause); + } + } + } + return causes; + } + + /** + * Finds the first non-MultiException cause of the given exception. + * + * @param e the exception to search, nullable + * @return the first non-MultiException cause, or null if none found, nullable + */ + public static Throwable anyMultiCause(final Throwable e) { + if (!isMultiple(e)) { + return e; + } + + Deque meStack = new ArrayDeque<>(); + meStack.push((MultiException) e); + + while (!meStack.isEmpty()) { + MultiException curMe = meStack.pop(); + Collection causes = curMe.getCauses(); + if (causes == null) { + continue; + } + + for (Throwable subCause : causes) { + if (subCause == null) { + continue; + } + if (isMultiple(subCause)) { + meStack.push((MultiException) subCause); + } else { + return subCause; + } + } + } + return null; + } } diff --git a/subprojects/parseq/src/test/java/com/linkedin/parseq/TestExceptions.java b/subprojects/parseq/src/test/java/com/linkedin/parseq/TestExceptions.java new file mode 100644 index 00000000..02747025 --- /dev/null +++ b/subprojects/parseq/src/test/java/com/linkedin/parseq/TestExceptions.java @@ -0,0 +1,167 @@ +package com.linkedin.parseq; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import org.testng.annotations.Test; + +/** + * test {@link Exceptions} + * + * @author wiilei + */ +public class TestExceptions { + @Test + public void testIsMultiException() { + assertTrue(Exceptions.isMultiple(new MultiException(Collections.emptyList()))); + assertFalse(Exceptions.isMultiple(new Exception())); + assertFalse(Exceptions.isMultiple(null)); + } + + @Test + public void testAllMultiCauses_withNull() { + Collection realCauses = Exceptions.allMultiCauses(null); + + assertNotNull(realCauses, "Real causes is null"); + assertEquals(realCauses.size(), 0, "The size of real causes is not correct"); + } + + /** + * test single layer multi exception, ME1[E1, E2] + */ + @Test + public void testAllMultiCauses_singleLayerMultiExceptionNonEmpty() { + Exception e1 = new Exception("E1"); + Exception e2 = new Exception("E2"); + MultiException me1 = new MultiException(Arrays.asList(e1, e2)); + + Collection realCauses = Exceptions.allMultiCauses(me1); + + assertEquals(realCauses.size(), 2, "The size of real causes is not correct"); + assertTrue(realCauses.contains(e1), "Real causes does not contain Exception 1"); + assertTrue(realCauses.contains(e2), "Real causes does not contain Exception 2"); + } + + /** + * test single layer multi exception, ME1[] + */ + @Test + public void testAllMultiCauses_singleLayerMultiExceptionEmpty() { + MultiException me1 = new MultiException(Collections.emptyList()); + Collection realCauses = Exceptions.allMultiCauses(me1); + assertEquals(realCauses.size(), 0, "The size of real causes is not correct"); + } + + /** + * test nested head multi exception, ME1[ ME2[E1, E2], E3 ] + */ + @Test + public void testAllMultiCauses_nestedHeadMultiException() { + Exception e1 = new Exception("Exception 1"); + Exception e2 = new Exception("Exception 2"); + Exception e3 = new Exception("Exception 3"); + MultiException me2 = new MultiException(Arrays.asList(e1, e2)); + MultiException me1 = new MultiException(Arrays.asList(me2, e3)); + + Collection realCauses = Exceptions.allMultiCauses(me1); + + assertEquals(realCauses.size(), 3, "The size of real causes is not correct"); + assertTrue(realCauses.contains(e1), "Real causes does not contain Exception 1"); + assertTrue(realCauses.contains(e2), "Real causes does not contain Exception 2"); + assertTrue(realCauses.contains(e3), "Real causes does not contain Exception 3"); + } + + /** + * test nested single multi exception with empty, ME1[ ME2[] ] + */ + @Test + public void testAllMultiCauses_nestedSingleMultiExceptionWithEmpty() { + MultiException me2 = new MultiException(Collections.emptyList()); + MultiException me1 = new MultiException(Collections.singletonList(me2)); + + Collection realCauses = Exceptions.allMultiCauses(me1); + + assertEquals(realCauses.size(), 0, "The size of real causes is not correct"); + } + + /** + * test nested single multi exception with tail non-empty, ME1[ ME2[E1] ] + */ + @Test + public void testAllMultiCauses_nestedSingleMultiExceptionWithTailNonEmpty() { + Exception e1 = new Exception("Exception 1"); + MultiException me2 = new MultiException(Collections.singletonList(e1)); + MultiException me1 = new MultiException(Collections.singletonList(me2)); + + Collection realCauses = Exceptions.allMultiCauses(me1); + + assertEquals(realCauses.size(), 1, "The size of real causes is not correct"); + assertTrue(realCauses.contains(e1), "Real causes does not contain Exception 1"); + } + + /** + * test nested multi exception with tail non-empty, ME1[ ME2[E1], E1, ME3[E2] ] + */ + @Test + public void testAllMultiCauses_nestedMultiExceptionWithTailNonEmpty() { + Exception e1 = new Exception("Exception 1"); + Exception e2 = new Exception("Exception 2"); + + MultiException me2 = new MultiException(Collections.emptyList()); + MultiException me3 = new MultiException(Collections.singletonList(e2)); + MultiException me1 = new MultiException(Arrays.asList(me2, e1, me3)); + + Collection realCauses = Exceptions.allMultiCauses(me1); + + assertEquals(realCauses.size(), 2, "The size of real causes is not correct"); + assertTrue(realCauses.contains(e1), "Real causes does not contain Exception 1"); + assertTrue(realCauses.contains(e2), "Real causes does not contain Exception 2"); + } + + /** + * test takes the first non-MultiException cause of the given exception, ME1[ ME2[E1], E1, ME3[E2] ] + */ + @Test + public void testAnyMultiCause_nestedMultiExceptionWithTailNonEmpty() { + Exception e1 = new Exception("Exception 1"); + Exception e2 = new Exception("Exception 2"); + + MultiException me2 = new MultiException(Collections.emptyList()); + MultiException me3 = new MultiException(Collections.singletonList(e2)); + MultiException me1 = new MultiException(Arrays.asList(me2, e1, me3)); + + Throwable anyCause = Exceptions.anyMultiCause(me1); + assertEquals(anyCause, e1, "The first non-MultiException cause is not correct"); + } + + /** + * test takes the first non-MultiException cause of the given exception, ME1[ ME2[], E1, ME3[E2] ] + */ + @Test + public void testAnyMultiCause_nestedSingleMultiExceptionWithTailNonEmpty() { + Exception e1 = new Exception("Exception 1"); + Exception e2 = new Exception("Exception 1"); + MultiException me2 = new MultiException(Collections.emptyList()); + MultiException me3 = new MultiException(Collections.singletonList(e2)); + MultiException me1 = new MultiException(Arrays.asList(me2, e1, me3)); + + Throwable anyCause = Exceptions.anyMultiCause(me1); + assertEquals(anyCause, e1, "The first non-MultiException cause is not correct"); + } + + @Test + public void testAnyMultiCause_singleLayerMultiExceptionNonEmpty() { + Throwable anyCause = Exceptions.anyMultiCause(null); + assertNull(anyCause, "The first non-MultiException cause is not correct"); + + Exception e1 = new Exception("Exception 1"); + anyCause = Exceptions.anyMultiCause(e1); + assertEquals(anyCause, e1, "The first non-MultiException cause is not correct"); + } +}