This is an automated email from the ASF dual-hosted git repository.
reschke pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new 01179a51a6 OAK-11571: commons: add Closer class (similar to Guava
Closer) (#2181)
01179a51a6 is described below
commit 01179a51a619c37fb8817ae2ef54c12add0a47f1
Author: Julian Reschke <[email protected]>
AuthorDate: Mon Mar 17 10:15:17 2025 +0100
OAK-11571: commons: add Closer class (similar to Guava Closer) (#2181)
---
oak-commons/pom.xml | 3 +-
.../apache/jackrabbit/oak/commons/pio/Closer.java | 126 ++++++++++++
.../jackrabbit/oak/commons/pio/package-info.java | 28 +++
.../jackrabbit/oak/commons/pio/CloserTest.java | 213 +++++++++++++++++++++
4 files changed, 369 insertions(+), 1 deletion(-)
diff --git a/oak-commons/pom.xml b/oak-commons/pom.xml
index 385104b03c..83d64d9a07 100644
--- a/oak-commons/pom.xml
+++ b/oak-commons/pom.xml
@@ -56,7 +56,8 @@
org.apache.jackrabbit.oak.commons.log,
org.apache.jackrabbit.oak.commons.sort,
org.apache.jackrabbit.oak.commons.properties,
- org.apache.jackrabbit.oak.commons.jdkcompat
+ org.apache.jackrabbit.oak.commons.jdkcompat,
+ org.apache.jackrabbit.oak.commons.pio
</Export-Package>
</instructions>
</configuration>
diff --git
a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/Closer.java
b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/Closer.java
new file mode 100755
index 0000000000..54705d5a3e
--- /dev/null
+++
b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/Closer.java
@@ -0,0 +1,126 @@
+/*
+ * 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.jackrabbit.oak.commons.pio;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+
+/**
+ * Convenience utility to close a list of {@link Closeable}s in reverse order,
+ * suppressing all but the first exception to occur.
+ * <p>
+ * Inspired by and replacing Guava's Closer.
+ */
+public class Closer implements Closeable {
+
+ private Closer() {
+ // no instances for you
+ }
+
+ // stack of closeables to close, in general will be few
+ private final Deque<Closeable> closeables = new ArrayDeque<>(3);
+
+ // flag set by rethrow method
+ private boolean suppressExceptionsOnClose = false;
+
+ /**
+ * Create instance of Closer.
+ */
+ public static Closer create() {
+ return new Closer();
+ }
+
+ /**
+ * Add a {@link Closeable} to the list.
+ * @param closeable {@link Closeable} object to be added
+ * @return the closeable param
+ */
+ public @Nullable <C extends Closeable> C register(@Nullable C closeable) {
+ if (closeable != null) {
+ closeables.add(closeable);
+ }
+ return closeable;
+ }
+
+ /**
+ * Closes the set of {@link Closeable}s in reverse order.
+ * <p>
+ * Swallows all exceptions except the first that
+ * was thrown.
+ * <p>
+ * If {@link #rethrow} was called before, even the first
+ * exception will be suppressed.
+ */
+ public void close() throws IOException {
+ // keep track of the IOException to throw
+ Throwable toThrow = null;
+
+ // close all in reverse order
+ while (!closeables.isEmpty()) {
+ Closeable closeable = closeables.removeLast();
+ try {
+ closeable.close();
+ } catch (Throwable exception) {
+ // remember the first one that occurred
+ if (toThrow == null) {
+ toThrow = exception;
+ }
+ }
+ }
+
+ // exceptions are suppressed when retrow was called
+ if (!suppressExceptionsOnClose && toThrow != null) {
+ // due to the contract of Closeable, the exception is either
+ // a checked IOException or an unchecked exception
+ if (toThrow instanceof IOException) {
+ throw (IOException) toThrow;
+ } else {
+ throw (RuntimeException) toThrow;
+ }
+ }
+ }
+
+ /**
+ * Sets a flag indicating that this method was called, then rethrows the
+ * given exception (potentially wrapped into {@link Error} or {@link
RuntimeException}).
+ * <p>
+ * {@link #close()} will not throw when this method was called before.
+ * @return never returns
+ * @throws IOException wrapping the input, when needed
+ */
+ public RuntimeException rethrow(@NotNull Throwable throwable) throws
IOException {
+ Objects.requireNonNull(throwable);
+ suppressExceptionsOnClose = true;
+ if (throwable instanceof IOException) {
+ throw (IOException) throwable;
+ } else if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ } else if (throwable instanceof Error) {
+ throw (Error) throwable;
+ } else {
+ throw new RuntimeException(throwable);
+ }
+ }
+}
diff --git
a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/package-info.java
b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/package-info.java
new file mode 100644
index 0000000000..d552c968ab
--- /dev/null
+++
b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/pio/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/**
+ * Internal ("private") utilities related to IO..
+ */
+@Internal(since = "1.0.0")
+@Version("1.0.0")
+package org.apache.jackrabbit.oak.commons.pio;
+import org.apache.jackrabbit.oak.commons.annotations.Internal;
+import org.osgi.annotation.versioning.Version;
+
diff --git
a/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/pio/CloserTest.java
b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/pio/CloserTest.java
new file mode 100644
index 0000000000..4550026cad
--- /dev/null
+++
b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/pio/CloserTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.jackrabbit.oak.commons.pio;
+
+import org.junit.Test;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class CloserTest {
+
+ @Test
+ public void testCloserOrder() throws IOException {
+ // shows closes in reverse order
+
+ int cnt = 2;
+ List<Integer> order = new ArrayList<>();
+
+ Closer closer = Closer.create();
+ for (int i = 0; i < cnt; i++) {
+ Integer val = i;
+ Closeable c = new Closeable() {
+
+ final Integer c = val;
+
+ @Override
+ public void close() {
+ order.add(c);
+ }
+ };
+ closer.register(c);
+ }
+ closer.close();
+ assertEquals(1, (int)order.get(0));
+ assertEquals(0, (int)order.get(1));
+ }
+
+ @Test
+ public void testCloserWithTryWithResources() throws IOException {
+ // check cloesable behavior of Closer
+
+ AtomicBoolean wasClosed = new AtomicBoolean(false);
+
+ try (Closer closer = Closer.create()) {
+ closer.register(() -> wasClosed.set(true));
+ }
+
+ assertTrue("closeable should be closed by try-w-resources",
wasClosed.get());
+ }
+
+ @Test
+ public void testCloseableThrowsRuntimeException() {
+ Closer closer = Closer.create();
+ closer.register(() -> {
+ throw new RuntimeException();
+ });
+ assertThrows(RuntimeException.class, closer::close);
+ }
+
+ @Test
+ public void testWhichThrows() throws IOException {
+ // shows which exception is not suppressed
+
+ int cnt = 2;
+
+ Closer closer = Closer.create();
+ for (int i = 0; i < cnt; i++) {
+ Integer val = i;
+ Closeable c = new Closeable() {
+
+ final Integer c = val;
+
+ @Override
+ public void close() throws IOException {
+ throw new IOException("" + c);
+ }
+ };
+ closer.register(c);
+ }
+
+ try {
+ closer.close();
+ fail("should throw");
+ } catch (IOException ex) {
+ assertEquals("1", ex.getMessage());
+ }
+ }
+
+ @Test
+ public void testRethrowRuntime() {
+ try {
+ Closer closer = Closer.create();
+ try {
+ closer.register(() -> {
+ throw new IOException("checked");
+ });
+ throw new RuntimeException("unchecked");
+ } catch (Throwable t) {
+ throw closer.rethrow(t);
+ } finally {
+ closer.close();
+ }
+ } catch (Exception ex) {
+ assertTrue(
+ "should throw the (wrapped) unchecked exception, but got "
+
+ ex.getMessage(),
+ ex.getMessage().contains("unchecked"));
+ }
+ }
+
+ @Test
+ public void testRethrowChecked() throws IOException {
+ try {
+ Closer closer = Closer.create();
+ try {
+ closer.register(() -> {
+ throw new IOException("checked");
+ });
+ throw new InterruptedException("interrupted");
+ } catch (Throwable t) {
+ throw closer.rethrow(t);
+ } finally {
+ closer.close();
+ }
+ } catch (RuntimeException ex) {
+ assertTrue("should throw the (wrapped) exception",
+ ex.getCause() instanceof InterruptedException);
+ }
+ }
+
+ @Test
+ public void compareClosers() {
+ // when rethrow was called, IOExceptions that happened upon close will
be swallowed
+
+ com.google.common.io.Closer guavaCloser =
com.google.common.io.Closer.create();
+ Closer oakCloser = Closer.create();
+
+ try {
+ throw oakCloser.rethrow(new InterruptedException());
+ } catch (Exception e) {}
+
+ try {
+ throw guavaCloser.rethrow(new InterruptedException());
+ } catch (Exception e) {}
+
+ try {
+ oakCloser.close();
+ } catch (Exception e) {
+ fail("should not throw but got: " + e);
+ }
+
+ try {
+ guavaCloser.close();
+ } catch (Exception e) {
+ fail("should not throw but got: " + e);
+ }
+ }
+
+ @Test
+ public void compareClosers2() {
+ // when rethrow was called, Exceptions that happened upon close will
be swallowed
+
+ com.google.common.io.Closer guavaCloser =
com.google.common.io.Closer.create();
+ Closer oakCloser = Closer.create();
+
+ try {
+ throw oakCloser.rethrow(new InterruptedException());
+ } catch (Exception e) {}
+
+ try {
+ throw guavaCloser.rethrow(new InterruptedException());
+ } catch (Exception e) {}
+
+ try {
+ oakCloser.register(() -> { throw new RuntimeException(); });
+ oakCloser.close();
+ } catch (Exception e) {
+ fail("should not throw but got: " + e);
+ }
+
+ try {
+ guavaCloser.register(() -> { throw new RuntimeException(); });
+ guavaCloser.close();
+ } catch (Exception e) {
+ fail("should not throw but got: " + e);
+ }
+ }
+}