diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/Neo4jException.java b/driver/src/main/java/org/neo4j/driver/exceptions/Neo4jException.java index 6033e19e5e..af09b9deea 100644 --- a/driver/src/main/java/org/neo4j/driver/exceptions/Neo4jException.java +++ b/driver/src/main/java/org/neo4j/driver/exceptions/Neo4jException.java @@ -40,26 +40,31 @@ public class Neo4jException extends RuntimeException { private final String code; /** * The GQLSTATUS as defined by the GQL standard. + * * @since 5.26.0 */ private final String gqlStatus; /** * The GQLSTATUS description. + * * @since 5.26.0 */ private final String statusDescription; /** * The diagnostic record. + * * @since 5.26.0 */ private final Map diagnosticRecord; /** * The GQLSTATUS error classification. + * * @since 5.26.0 */ private final GqlStatusErrorClassification classification; /** * The GQLSTATUS error classification as raw String. + * * @since 5.26.0 */ private final String rawClassification; @@ -116,12 +121,13 @@ public Neo4jException(String code, String message, Throwable cause) { /** * Creates a new instance. - * @param gqlStatus the GQLSTATUS as defined by the GQL standard + * + * @param gqlStatus the GQLSTATUS as defined by the GQL standard * @param statusDescription the status description - * @param code the code - * @param message the message - * @param diagnosticRecord the diagnostic record - * @param cause the cause + * @param code the code + * @param message the message + * @param diagnosticRecord the diagnostic record + * @param cause the cause * @since 5.26.0 */ @Preview(name = "GQL-error") @@ -235,19 +241,56 @@ public Optional rawClassification() { */ @Preview(name = "GQL-error") public Optional gqlCause() { - return findFirstGqlCause(this, Neo4jException.class); + return Optional.ofNullable(findFirstGqlCause(this)); + } + + /** + * Returns whether there is an error with the given GQLSTATUS in this GQL error chain, beginning the search from + * this exception. + * + * @param gqlStatus the GQLSTATUS + * @return {@literal true} if yes or {@literal false} otherwise + * @since 5.28.8 + */ + @Preview(name = "GQL-error") + public boolean containsGqlStatus(String gqlStatus) { + return findByGqlStatus(this, gqlStatus) != null; + } + + /** + * Finds the first {@link Neo4jException} that has the given GQLSTATUS in this GQL error chain, beginning the search + * from this exception. + * + * @param gqlStatus the GQLSTATUS + * @return an {@link Optional} of {@link Neo4jException} or {@link Optional#empty()} otherwise + * @since 5.28.8 + */ + @Preview(name = "GQL-error") + public Optional findByGqlStatus(String gqlStatus) { + return Optional.ofNullable(findByGqlStatus(this, gqlStatus)); } @SuppressWarnings("DuplicatedCode") - private static Optional findFirstGqlCause(Throwable throwable, Class targetCls) { + private static Neo4jException findFirstGqlCause(Throwable throwable) { var cause = throwable.getCause(); - if (cause == null) { - return Optional.empty(); - } - if (targetCls.isAssignableFrom(cause.getClass())) { - return Optional.of(targetCls.cast(cause)); + if (cause instanceof Neo4jException neo4jException) { + return neo4jException; } else { - return Optional.empty(); + return null; + } + } + + private static Neo4jException findByGqlStatus(Neo4jException neo4jException, String gqlStatus) { + Objects.requireNonNull(gqlStatus); + Neo4jException result = null; + var gqlError = neo4jException; + while (gqlError != null) { + if (gqlError.gqlStatus().equals(gqlStatus)) { + result = gqlError; + break; + } + gqlError = findFirstGqlCause(gqlError); } + return result; } } diff --git a/driver/src/test/java/org/neo4j/driver/exceptions/Neo4jExceptionTest.java b/driver/src/test/java/org/neo4j/driver/exceptions/Neo4jExceptionTest.java index 1c18d179af..2ff98ac7bb 100644 --- a/driver/src/test/java/org/neo4j/driver/exceptions/Neo4jExceptionTest.java +++ b/driver/src/test/java/org/neo4j/driver/exceptions/Neo4jExceptionTest.java @@ -17,6 +17,7 @@ package org.neo4j.driver.exceptions; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collections; import java.util.Map; @@ -84,6 +85,71 @@ void shouldFindGqlCauseOnNonInterruptedChainOnly() { assertError(exception2, exception3, null); } + @ParameterizedTest + @MethodSource("gqlErrorsArgs") + void shouldFindByGqlStatus(Neo4jException exception, int statusIndex, boolean shouldBeFound) { + // given + var status = "status" + statusIndex; + + // when + var exceptionWithStatus = exception.findByGqlStatus(status); + + // then + if (shouldBeFound) { + assertTrue(exceptionWithStatus.isPresent()); + assertEquals(getByIndex(exception, statusIndex), exceptionWithStatus.get()); + } else { + assertTrue(exceptionWithStatus.isEmpty()); + } + } + + @ParameterizedTest + @MethodSource("gqlErrorsArgs") + void shouldReturnIfErrorContainsGqlStatus(Neo4jException exception, int statusIndex, boolean shouldBeFound) { + // given + var status = "status" + statusIndex; + + // when + var contains = exception.containsGqlStatus(status); + + // then + assertEquals(shouldBeFound, contains); + } + + static Stream gqlErrorsArgs() { + return Stream.of( + Arguments.of(buildGqlErrors(1, 1), 0, true), + Arguments.of(buildGqlErrors(1, 1), -1, false), + Arguments.of(buildGqlErrors(20, 20), 0, true), + Arguments.of(buildGqlErrors(20, 20), 10, true), + Arguments.of(buildGqlErrors(20, 20), 19, true), + Arguments.of(buildGqlErrors(20, 20), -1, false), + Arguments.of(buildGqlErrors(20, 15), 0, true), + Arguments.of(buildGqlErrors(20, 15), 14, true), + Arguments.of(buildGqlErrors(20, 15), -1, false)); + } + + @SuppressWarnings("DataFlowIssue") + static Neo4jException buildGqlErrors(int totalSize, int gqlErrorsSize) { + Exception exception = null; + for (var i = totalSize - 1; i >= gqlErrorsSize; i--) { + exception = new IllegalStateException("illegal state" + i, exception); + } + for (var i = gqlErrorsSize - 1; i >= 0; i--) { + exception = new Neo4jException( + "status" + i, "description" + i, "code", "message", Collections.emptyMap(), exception); + } + return (Neo4jException) exception; + } + + static Throwable getByIndex(Throwable exception, int depth) { + var result = exception; + for (var i = 0; i < depth; i++) { + result = result.getCause(); + } + return result; + } + private void assertError(Neo4jException exception, Throwable expectedCause, Neo4jException expectedGqlCause) { assertEquals(expectedCause, exception.getCause()); assertEquals(expectedGqlCause, exception.gqlCause().orElse(null));