Skip to content

Commit 01179a5

Browse files
authored
OAK-11571: commons: add Closer class (similar to Guava Closer) (#2181)
1 parent 7aada40 commit 01179a5

File tree

4 files changed

+369
-1
lines changed

4 files changed

+369
-1
lines changed

oak-commons/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
org.apache.jackrabbit.oak.commons.log,
5757
org.apache.jackrabbit.oak.commons.sort,
5858
org.apache.jackrabbit.oak.commons.properties,
59-
org.apache.jackrabbit.oak.commons.jdkcompat
59+
org.apache.jackrabbit.oak.commons.jdkcompat,
60+
org.apache.jackrabbit.oak.commons.pio
6061
</Export-Package>
6162
</instructions>
6263
</configuration>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.jackrabbit.oak.commons.pio;
20+
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
24+
import java.io.Closeable;
25+
import java.io.IOException;
26+
import java.util.ArrayDeque;
27+
import java.util.Deque;
28+
import java.util.Objects;
29+
30+
/**
31+
* Convenience utility to close a list of {@link Closeable}s in reverse order,
32+
* suppressing all but the first exception to occur.
33+
* <p>
34+
* Inspired by and replacing Guava's Closer.
35+
*/
36+
public class Closer implements Closeable {
37+
38+
private Closer() {
39+
// no instances for you
40+
}
41+
42+
// stack of closeables to close, in general will be few
43+
private final Deque<Closeable> closeables = new ArrayDeque<>(3);
44+
45+
// flag set by rethrow method
46+
private boolean suppressExceptionsOnClose = false;
47+
48+
/**
49+
* Create instance of Closer.
50+
*/
51+
public static Closer create() {
52+
return new Closer();
53+
}
54+
55+
/**
56+
* Add a {@link Closeable} to the list.
57+
* @param closeable {@link Closeable} object to be added
58+
* @return the closeable param
59+
*/
60+
public @Nullable <C extends Closeable> C register(@Nullable C closeable) {
61+
if (closeable != null) {
62+
closeables.add(closeable);
63+
}
64+
return closeable;
65+
}
66+
67+
/**
68+
* Closes the set of {@link Closeable}s in reverse order.
69+
* <p>
70+
* Swallows all exceptions except the first that
71+
* was thrown.
72+
* <p>
73+
* If {@link #rethrow} was called before, even the first
74+
* exception will be suppressed.
75+
*/
76+
public void close() throws IOException {
77+
// keep track of the IOException to throw
78+
Throwable toThrow = null;
79+
80+
// close all in reverse order
81+
while (!closeables.isEmpty()) {
82+
Closeable closeable = closeables.removeLast();
83+
try {
84+
closeable.close();
85+
} catch (Throwable exception) {
86+
// remember the first one that occurred
87+
if (toThrow == null) {
88+
toThrow = exception;
89+
}
90+
}
91+
}
92+
93+
// exceptions are suppressed when retrow was called
94+
if (!suppressExceptionsOnClose && toThrow != null) {
95+
// due to the contract of Closeable, the exception is either
96+
// a checked IOException or an unchecked exception
97+
if (toThrow instanceof IOException) {
98+
throw (IOException) toThrow;
99+
} else {
100+
throw (RuntimeException) toThrow;
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Sets a flag indicating that this method was called, then rethrows the
107+
* given exception (potentially wrapped into {@link Error} or {@link RuntimeException}).
108+
* <p>
109+
* {@link #close()} will not throw when this method was called before.
110+
* @return never returns
111+
* @throws IOException wrapping the input, when needed
112+
*/
113+
public RuntimeException rethrow(@NotNull Throwable throwable) throws IOException {
114+
Objects.requireNonNull(throwable);
115+
suppressExceptionsOnClose = true;
116+
if (throwable instanceof IOException) {
117+
throw (IOException) throwable;
118+
} else if (throwable instanceof RuntimeException) {
119+
throw (RuntimeException) throwable;
120+
} else if (throwable instanceof Error) {
121+
throw (Error) throwable;
122+
} else {
123+
throw new RuntimeException(throwable);
124+
}
125+
}
126+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* Internal ("private") utilities related to IO..
22+
*/
23+
@Internal(since = "1.0.0")
24+
@Version("1.0.0")
25+
package org.apache.jackrabbit.oak.commons.pio;
26+
import org.apache.jackrabbit.oak.commons.annotations.Internal;
27+
import org.osgi.annotation.versioning.Version;
28+
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.jackrabbit.oak.commons.pio;
20+
21+
import org.junit.Test;
22+
23+
import java.io.Closeable;
24+
import java.io.IOException;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.concurrent.atomic.AtomicBoolean;
28+
29+
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertNull;
31+
import static org.junit.Assert.assertThrows;
32+
import static org.junit.Assert.assertTrue;
33+
import static org.junit.Assert.fail;
34+
35+
public class CloserTest {
36+
37+
@Test
38+
public void testCloserOrder() throws IOException {
39+
// shows closes in reverse order
40+
41+
int cnt = 2;
42+
List<Integer> order = new ArrayList<>();
43+
44+
Closer closer = Closer.create();
45+
for (int i = 0; i < cnt; i++) {
46+
Integer val = i;
47+
Closeable c = new Closeable() {
48+
49+
final Integer c = val;
50+
51+
@Override
52+
public void close() {
53+
order.add(c);
54+
}
55+
};
56+
closer.register(c);
57+
}
58+
closer.close();
59+
assertEquals(1, (int)order.get(0));
60+
assertEquals(0, (int)order.get(1));
61+
}
62+
63+
@Test
64+
public void testCloserWithTryWithResources() throws IOException {
65+
// check cloesable behavior of Closer
66+
67+
AtomicBoolean wasClosed = new AtomicBoolean(false);
68+
69+
try (Closer closer = Closer.create()) {
70+
closer.register(() -> wasClosed.set(true));
71+
}
72+
73+
assertTrue("closeable should be closed by try-w-resources", wasClosed.get());
74+
}
75+
76+
@Test
77+
public void testCloseableThrowsRuntimeException() {
78+
Closer closer = Closer.create();
79+
closer.register(() -> {
80+
throw new RuntimeException();
81+
});
82+
assertThrows(RuntimeException.class, closer::close);
83+
}
84+
85+
@Test
86+
public void testWhichThrows() throws IOException {
87+
// shows which exception is not suppressed
88+
89+
int cnt = 2;
90+
91+
Closer closer = Closer.create();
92+
for (int i = 0; i < cnt; i++) {
93+
Integer val = i;
94+
Closeable c = new Closeable() {
95+
96+
final Integer c = val;
97+
98+
@Override
99+
public void close() throws IOException {
100+
throw new IOException("" + c);
101+
}
102+
};
103+
closer.register(c);
104+
}
105+
106+
try {
107+
closer.close();
108+
fail("should throw");
109+
} catch (IOException ex) {
110+
assertEquals("1", ex.getMessage());
111+
}
112+
}
113+
114+
@Test
115+
public void testRethrowRuntime() {
116+
try {
117+
Closer closer = Closer.create();
118+
try {
119+
closer.register(() -> {
120+
throw new IOException("checked");
121+
});
122+
throw new RuntimeException("unchecked");
123+
} catch (Throwable t) {
124+
throw closer.rethrow(t);
125+
} finally {
126+
closer.close();
127+
}
128+
} catch (Exception ex) {
129+
assertTrue(
130+
"should throw the (wrapped) unchecked exception, but got " +
131+
ex.getMessage(),
132+
ex.getMessage().contains("unchecked"));
133+
}
134+
}
135+
136+
@Test
137+
public void testRethrowChecked() throws IOException {
138+
try {
139+
Closer closer = Closer.create();
140+
try {
141+
closer.register(() -> {
142+
throw new IOException("checked");
143+
});
144+
throw new InterruptedException("interrupted");
145+
} catch (Throwable t) {
146+
throw closer.rethrow(t);
147+
} finally {
148+
closer.close();
149+
}
150+
} catch (RuntimeException ex) {
151+
assertTrue("should throw the (wrapped) exception",
152+
ex.getCause() instanceof InterruptedException);
153+
}
154+
}
155+
156+
@Test
157+
public void compareClosers() {
158+
// when rethrow was called, IOExceptions that happened upon close will be swallowed
159+
160+
com.google.common.io.Closer guavaCloser = com.google.common.io.Closer.create();
161+
Closer oakCloser = Closer.create();
162+
163+
try {
164+
throw oakCloser.rethrow(new InterruptedException());
165+
} catch (Exception e) {}
166+
167+
try {
168+
throw guavaCloser.rethrow(new InterruptedException());
169+
} catch (Exception e) {}
170+
171+
try {
172+
oakCloser.close();
173+
} catch (Exception e) {
174+
fail("should not throw but got: " + e);
175+
}
176+
177+
try {
178+
guavaCloser.close();
179+
} catch (Exception e) {
180+
fail("should not throw but got: " + e);
181+
}
182+
}
183+
184+
@Test
185+
public void compareClosers2() {
186+
// when rethrow was called, Exceptions that happened upon close will be swallowed
187+
188+
com.google.common.io.Closer guavaCloser = com.google.common.io.Closer.create();
189+
Closer oakCloser = Closer.create();
190+
191+
try {
192+
throw oakCloser.rethrow(new InterruptedException());
193+
} catch (Exception e) {}
194+
195+
try {
196+
throw guavaCloser.rethrow(new InterruptedException());
197+
} catch (Exception e) {}
198+
199+
try {
200+
oakCloser.register(() -> { throw new RuntimeException(); });
201+
oakCloser.close();
202+
} catch (Exception e) {
203+
fail("should not throw but got: " + e);
204+
}
205+
206+
try {
207+
guavaCloser.register(() -> { throw new RuntimeException(); });
208+
guavaCloser.close();
209+
} catch (Exception e) {
210+
fail("should not throw but got: " + e);
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)