diff --git a/oak-commons/pom.xml b/oak-commons/pom.xml index 385104b03ce..83d64d9a07a 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 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 00000000000..54705d5a3ef --- /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. + *

+ * 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 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 register(@Nullable C closeable) { + if (closeable != null) { + closeables.add(closeable); + } + return closeable; + } + + /** + * Closes the set of {@link Closeable}s in reverse order. + *

+ * Swallows all exceptions except the first that + * was thrown. + *

+ * 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}). + *

+ * {@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 00000000000..d552c968ab0 --- /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 00000000000..4550026cada --- /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 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); + } + } +}