From a10519e0b87dfa4bf9c6330664ec079f1fdca636 Mon Sep 17 00:00:00 2001 From: A Anand Date: Sun, 4 Jan 2026 11:55:59 +0530 Subject: [PATCH] feature: #85 | Added try with resources capability --- .gitignore | 3 +- .../zeplinko/commons/lang/ext/core/Try.java | 120 ++++++ .../commons/lang/ext/core/TryTest.java | 352 ++++++++++++++++++ 3 files changed, 474 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fc3f89c..97d3650 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store +.agent diff --git a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java index a1cd506..1ef5633 100644 --- a/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java +++ b/src/main/java/org/zeplinko/commons/lang/ext/core/Try.java @@ -2,6 +2,11 @@ import jakarta.annotation.Nonnull; import org.zeplinko.commons.lang.ext.annotations.Preview; +import org.zeplinko.commons.lang.ext.util.function.ThrowingBiFunction; +import org.zeplinko.commons.lang.ext.util.function.ThrowingConsumer; +import org.zeplinko.commons.lang.ext.util.function.ThrowingFunction; +import org.zeplinko.commons.lang.ext.util.function.ThrowingRunnable; +import org.zeplinko.commons.lang.ext.util.function.ThrowingSupplier; import java.util.Arrays; import java.util.Objects; @@ -124,6 +129,121 @@ public static Try to(@Nonnull Callable callable) { } } + /** + * Executes a runnable that may throw a checked exception. + * + * @param runnable The runnable to execute. + * @return A {@code Try} representing success or failure. + */ + @Preview + public static Try run(@Nonnull ThrowingRunnable runnable) { + Objects.requireNonNull(runnable); + try { + runnable.run(); + return Try.success(Empty.getInstance()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Try.failure(e); + } catch (Exception e) { + return Try.failure(e); + } + } + + /** + * Executes a block of code with a resource that is automatically closed, + * capturing any exceptions into a {@code Try}. + * + * @param resourceSupplier A supplier that creates the resource. + * @param action The function to execute with the resource. + * @param The type of the resource (must be AutoCloseable). + * @param The type of the result. + * @return A {@code Try} containing the result or any exception thrown. + */ + @Preview + public static Try withResources( + @Nonnull ThrowingSupplier resourceSupplier, + @Nonnull ThrowingFunction action + ) { + Objects.requireNonNull(resourceSupplier); + Objects.requireNonNull(action); + try (R resource = resourceSupplier.get()) { + return Try.success(action.apply(resource)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Try.failure(e); + } catch (Exception e) { + return Try.failure(e); + } + } + + /** + * Executes a block of code with two resources that are automatically closed, + * capturing any exceptions into a {@code Try}. + *

+ * Resources are closed in reverse order of acquisition: the second resource is + * closed first, followed by the first resource. If closing a resource throws an + * exception, it is added as a suppressed exception if the action also threw. + *

+ * + * @param resourceSupplier1 A supplier that creates the first resource. + * @param resourceSupplier2 A supplier that creates the second resource. + * @param action The function to execute with the resources. + * @param The type of the first resource (must be + * AutoCloseable). + * @param The type of the second resource (must be + * AutoCloseable). + * @param The type of the result. + * @return A {@code Try} containing the result or any exception thrown. + */ + @Preview + public static Try withResources( + @Nonnull ThrowingSupplier resourceSupplier1, + @Nonnull ThrowingSupplier resourceSupplier2, + @Nonnull ThrowingBiFunction action + ) { + Objects.requireNonNull(resourceSupplier1); + Objects.requireNonNull(resourceSupplier2); + Objects.requireNonNull(action); + try ( + R1 r1 = resourceSupplier1.get(); + R2 r2 = resourceSupplier2.get() + ) { + return Try.success(action.apply(r1, r2)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Try.failure(e); + } catch (Exception e) { + return Try.failure(e); + } + } + + /** + * Executes a side effect operation with a resource that is automatically + * closed. + * + * @param resourceSupplier A supplier that creates the resource. + * @param action The consumer to execute with the resource. + * @param The type of the resource (must be AutoCloseable). + * @return A {@code Try} representing success or failure. + */ + @Preview + public static Try consumeResource( + @Nonnull ThrowingSupplier resourceSupplier, + @Nonnull ThrowingConsumer action + ) { + Objects.requireNonNull(resourceSupplier); + Objects.requireNonNull(action); + try (R resource = resourceSupplier.get()) { + action.accept(resource); + return Try.success(Empty.getInstance()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Try.failure(e); + } catch (Exception e) { + return Try.failure(e); + } + } + /** * {@inheritDoc} */ diff --git a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java index 89321ef..db291e6 100644 --- a/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java +++ b/src/test/java/org/zeplinko/commons/lang/ext/core/TryTest.java @@ -1187,4 +1187,356 @@ void test_givenTry_whenToStringIsInvoked_thenRespectiveOutcomeIsReturned( String actualToString = tryObject.toString(); Assertions.assertEquals(expectedToString, actualToString); } + + @Test + void test_whenRunIsCalledWithNullRunnable_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows(NullPointerException.class, () -> Try.run(null)); + assertNotNull(exception); + } + + @Test + void test_whenRunIsCalledWithRunnableThatSucceeds_thenSuccessTryWithEmptyIsReturned() { + List sideEffect = new ArrayList<>(); + + Try result = Try.run(() -> sideEffect.add("executed")); + + assertTrue(result.isSuccess()); + assertSame(Empty.getInstance(), result.getData()); + assertEquals(1, sideEffect.size()); + assertEquals("executed", sideEffect.get(0)); + } + + @Test + void test_whenRunIsCalledWithRunnableThatThrowsException_thenFailureTryIsReturned() { + RuntimeException expectedError = new RuntimeException("run failed"); + + Try result = Try.run(() -> { + throw expectedError; + }); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + } + + @Test + void test_whenRunIsCalledWithRunnableThatThrowsInterruptedException_thenFailureTryIsReturned() { + InterruptedException expectedError = new InterruptedException("interrupted"); + + Try result = Try.run(() -> { + throw expectedError; + }); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(Thread.currentThread().isInterrupted()); + Thread.interrupted(); + } + + @Test + void test_whenWithResourcesIsCalledWithNullSupplier_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.withResources(null, r -> "result") + ); + assertNotNull(exception); + } + + @Test + void test_whenWithResourcesIsCalledWithNullAction_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.withResources(() -> new TestResource(), null) + ); + assertNotNull(exception); + } + + @Test + void test_whenWithResourcesSucceeds_thenSuccessTryIsReturnedAndResourceIsClosed() { + TestResource resource = new TestResource(); + + Try result = Try.withResources(() -> resource, r -> "success"); + + assertTrue(result.isSuccess()); + assertEquals("success", result.getData()); + assertTrue(resource.isClosed()); + } + + @Test + void test_whenWithResourcesSupplierThrowsException_thenFailureTryIsReturned() { + RuntimeException expectedError = new RuntimeException("supplier failed"); + + Try result = Try.withResources( + () -> { + throw expectedError; + }, + r -> "success" + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + } + + @Test + void test_whenWithResourcesActionThrowsException_thenFailureTryIsReturnedAndResourceIsClosed() { + TestResource resource = new TestResource(); + RuntimeException expectedError = new RuntimeException("action failed"); + + Try result = Try.withResources( + () -> resource, + r -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource.isClosed()); + } + + @Test + void test_whenWithResourcesActionThrowsInterruptedException_thenFailureTryIsReturned() { + TestResource resource = new TestResource(); + InterruptedException expectedError = new InterruptedException("interrupted"); + + Try result = Try.withResources( + () -> resource, + r -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource.isClosed()); + assertTrue(Thread.currentThread().isInterrupted()); + Thread.interrupted(); + } + + @Test + void test_whenWithResourcesTwoIsCalledWithNullFirstSupplier_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.withResources(null, () -> new TestResource(), (r1, r2) -> "result") + ); + assertNotNull(exception); + } + + @Test + void test_whenWithResourcesTwoIsCalledWithNullSecondSupplier_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.withResources(() -> new TestResource(), null, (r1, r2) -> "result") + ); + assertNotNull(exception); + } + + @Test + void test_whenWithResourcesTwoIsCalledWithNullAction_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.withResources(() -> new TestResource(), () -> new TestResource(), null) + ); + assertNotNull(exception); + } + + @Test + void test_whenWithResourcesTwoSucceeds_thenSuccessTryIsReturnedAndBothResourcesAreClosed() { + TestResource resource1 = new TestResource(); + TestResource resource2 = new TestResource(); + + Try result = Try.withResources( + () -> resource1, + () -> resource2, + (r1, r2) -> "success" + ); + + assertTrue(result.isSuccess()); + assertEquals("success", result.getData()); + assertTrue(resource1.isClosed()); + assertTrue(resource2.isClosed()); + } + + @Test + void test_whenWithResourcesTwoFirstSupplierThrows_thenFailureTryIsReturned() { + RuntimeException expectedError = new RuntimeException("first supplier failed"); + + Try result = Try.withResources( + () -> { + throw expectedError; + }, + () -> new TestResource(), + (r1, r2) -> "success" + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + } + + @Test + void test_whenWithResourcesTwoSecondSupplierThrows_thenFailureTryIsReturnedAndFirstResourceIsClosed() { + TestResource resource1 = new TestResource(); + RuntimeException expectedError = new RuntimeException("second supplier failed"); + + Try result = Try.withResources( + () -> resource1, + () -> { + throw expectedError; + }, + (r1, r2) -> "success" + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource1.isClosed()); + } + + @Test + void test_whenWithResourcesTwoActionThrows_thenFailureTryIsReturnedAndBothResourcesAreClosed() { + TestResource resource1 = new TestResource(); + TestResource resource2 = new TestResource(); + RuntimeException expectedError = new RuntimeException("action failed"); + + Try result = Try.withResources( + () -> resource1, + () -> resource2, + (r1, r2) -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource1.isClosed()); + assertTrue(resource2.isClosed()); + } + + @Test + void test_whenWithResourcesTwoActionThrowsInterruptedException_thenFailureTryIsReturned() { + TestResource resource1 = new TestResource(); + TestResource resource2 = new TestResource(); + InterruptedException expectedError = new InterruptedException("interrupted"); + + Try result = Try.withResources( + () -> resource1, + () -> resource2, + (r1, r2) -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource1.isClosed()); + assertTrue(resource2.isClosed()); + assertTrue(Thread.currentThread().isInterrupted()); + Thread.interrupted(); + } + + @Test + void test_whenConsumeResourceIsCalledWithNullSupplier_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.consumeResource(null, r -> { + }) + ); + assertNotNull(exception); + } + + @Test + void test_whenConsumeResourceIsCalledWithNullAction_thenExceptionIsThrown() { + @SuppressWarnings("DataFlowIssue") + NullPointerException exception = assertThrows( + NullPointerException.class, + () -> Try.consumeResource(() -> new TestResource(), null) + ); + assertNotNull(exception); + } + + @Test + void test_whenConsumeResourceSucceeds_thenSuccessTryWithEmptyIsReturnedAndResourceIsClosed() { + TestResource resource = new TestResource(); + List sideEffect = new ArrayList<>(); + + Try result = Try.consumeResource( + () -> resource, + r -> sideEffect.add("consumed") + ); + + assertTrue(result.isSuccess()); + assertSame(Empty.getInstance(), result.getData()); + assertTrue(resource.isClosed()); + assertEquals(1, sideEffect.size()); + } + + @Test + void test_whenConsumeResourceSupplierThrows_thenFailureTryIsReturned() { + RuntimeException expectedError = new RuntimeException("supplier failed"); + + Try result = Try.consumeResource( + () -> { + throw expectedError; + }, + r -> { + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + } + + @Test + void test_whenConsumeResourceActionThrows_thenFailureTryIsReturnedAndResourceIsClosed() { + TestResource resource = new TestResource(); + RuntimeException expectedError = new RuntimeException("action failed"); + + Try result = Try.consumeResource( + () -> resource, + r -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource.isClosed()); + } + + @Test + void test_whenConsumeResourceActionThrowsInterruptedException_thenFailureTryIsReturned() { + TestResource resource = new TestResource(); + InterruptedException expectedError = new InterruptedException("interrupted"); + + Try result = Try.consumeResource( + () -> resource, + r -> { + throw expectedError; + } + ); + + assertTrue(result.isFailure()); + assertSame(expectedError, result.getError()); + assertTrue(resource.isClosed()); + assertTrue(Thread.currentThread().isInterrupted()); + Thread.interrupted(); + } + + private static class TestResource implements AutoCloseable { + private boolean closed = false; + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + } }