Skip to content

Commit 0d8768b

Browse files
authored
Add FlushShieldOutputStream (#841)
* [IO-856] Try test on all OSs for GitHub CI * Add FlushShieldOutputStream
1 parent 76feb70 commit 0d8768b

2 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
* https://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+
package org.apache.commons.io.output;
21+
22+
import java.io.FilterOutputStream;
23+
import java.io.IOException;
24+
import java.io.OutputStream;
25+
26+
import org.apache.commons.io.build.AbstractStreamBuilder;
27+
28+
/**
29+
* Re-implements {@link FilterOutputStream#flush()} to do nothing.
30+
*
31+
* @since 2.22.0
32+
*/
33+
public final class FlushShieldOutputStream extends ProxyOutputStream {
34+
35+
// @formatter:off
36+
/**
37+
* Builds a new {@link FlushShieldOutputStream}.
38+
*
39+
* <p>
40+
* Using File IO:
41+
* </p>
42+
* <pre>{@code
43+
* FlushShieldOutputStream s = FlushShieldOutputStream.builder()
44+
* .setPath("over/there.out")
45+
* .setBufferSize(8192)
46+
* .get();
47+
* }
48+
* </pre>
49+
* <p>
50+
* Using NIO Path:
51+
* </p>
52+
* <pre>{@code
53+
* FlushShieldOutputStream s = FlushShieldOutputStream.builder()
54+
* .setPath("over/there.out")
55+
* .setBufferSize(8192)
56+
* .get();
57+
* }
58+
* </pre>
59+
*
60+
* @see #get()
61+
* @since 2.13.0
62+
*/
63+
// @formatter:on
64+
public static class Builder extends AbstractStreamBuilder<FlushShieldOutputStream, Builder> {
65+
66+
/**
67+
* Constructs a new builder of {@link FlushShieldOutputStream}.
68+
*/
69+
public Builder() {
70+
// empty
71+
}
72+
73+
/**
74+
* Builds a new {@link FlushShieldOutputStream}.
75+
*
76+
* @return a new instance.
77+
* @throws IllegalStateException if the {@code origin} is {@code null}.
78+
* @throws UnsupportedOperationException if the origin cannot be converted to an {@link OutputStream}.
79+
* @throws IOException if an I/O error occurs converting to an {@link OutputStream} using {@link #getOutputStream()}.
80+
* @see #getOutputStream()
81+
* @see #getBufferSize()
82+
* @see #getUnchecked()
83+
*/
84+
@Override
85+
public FlushShieldOutputStream get() throws IOException {
86+
return new FlushShieldOutputStream(this);
87+
}
88+
89+
}
90+
91+
/**
92+
* Constructs a new builder of {@link FlushShieldOutputStream}.
93+
*
94+
* @return a new builder of {@link FlushShieldOutputStream}.
95+
*/
96+
public static Builder builder() {
97+
return new Builder();
98+
}
99+
100+
@SuppressWarnings("resource") // caller closes.
101+
private FlushShieldOutputStream(final Builder builder) throws IOException {
102+
super(builder.getOutputStream());
103+
}
104+
105+
/**
106+
* Constructs a {@code FlushShieldOutputStream} filter for the specified underlying output stream.
107+
*
108+
* @param out the underlying output stream to be assigned to the field {@code this.out} for later use, or {@code null} if this instance is to be created
109+
* without an underlying stream.
110+
*/
111+
public FlushShieldOutputStream(final OutputStream out) {
112+
super(out);
113+
}
114+
115+
@Override
116+
public void flush() throws IOException {
117+
// shield: do nothing.
118+
}
119+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.io.output;
19+
20+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertFalse;
23+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
24+
import static org.junit.jupiter.api.Assertions.assertNotNull;
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
26+
27+
import java.io.IOException;
28+
import java.util.concurrent.atomic.AtomicBoolean;
29+
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
33+
/**
34+
* Tests {@link FlushShieldOutputStream}.
35+
*/
36+
class FlushShieldOutputStreamTest {
37+
38+
/** Tracks whether the underlying stream's close() was called. */
39+
private AtomicBoolean closed;
40+
41+
/** Tracks whether the underlying stream's flush() was called. */
42+
private AtomicBoolean flushed;
43+
44+
/** The stream under test. */
45+
private FlushShieldOutputStream shielded;
46+
47+
/** The underlying byte-array-backed stream used as the delegate. */
48+
private ByteArrayOutputStream target;
49+
50+
@BeforeEach
51+
void setUp() {
52+
flushed = new AtomicBoolean();
53+
closed = new AtomicBoolean();
54+
target = new ByteArrayOutputStream() {
55+
56+
@Override
57+
public void close() throws IOException {
58+
closed.set(true);
59+
super.close();
60+
}
61+
62+
@Override
63+
public void flush() throws IOException {
64+
flushed.set(true);
65+
super.flush();
66+
}
67+
};
68+
shielded = new FlushShieldOutputStream(target);
69+
}
70+
71+
@Test
72+
void testBuilderGet() throws IOException {
73+
assertNotNull(FlushShieldOutputStream.builder().setOutputStream(target).get());
74+
}
75+
76+
@Test
77+
void testBuilderWithOutputStream() throws IOException {
78+
try (FlushShieldOutputStream built = FlushShieldOutputStream.builder().setOutputStream(target).get()) {
79+
assertNotNull(built);
80+
assertInstanceOf(FlushShieldOutputStream.class, built);
81+
// flush must be shielded for builder-created instances too
82+
built.flush();
83+
assertFalse(flushed.get(), "flush() via builder instance must NOT reach the underlying stream.");
84+
// writes must still work
85+
built.write('B');
86+
assertEquals(1, target.size());
87+
assertEquals('B', target.toByteArray()[0]);
88+
}
89+
}
90+
91+
@Test
92+
void testCloseReachesUnderlying() throws IOException {
93+
assertFalse(closed.get(), "close should not have been called yet.");
94+
shielded.close();
95+
assertTrue(closed.get(), "close() must reach the underlying stream.");
96+
}
97+
98+
@Test
99+
void testFlushCanBeCalledMultipleTimes() throws IOException {
100+
shielded.flush();
101+
shielded.flush();
102+
shielded.flush();
103+
assertFalse(flushed.get(), "repeated flush() calls must NOT reach the underlying stream.");
104+
}
105+
106+
@Test
107+
void testFlushDoesNotDelegateToUnderlying() throws IOException {
108+
assertFalse(flushed.get(), "flush should not have been called yet.");
109+
shielded.flush();
110+
assertFalse(flushed.get(), "flush() must NOT reach the underlying stream.");
111+
}
112+
113+
@Test
114+
void testWriteByteArray() throws IOException {
115+
final byte[] data = { 'H', 'i' };
116+
shielded.write(data);
117+
assertEquals(2, target.size());
118+
assertArrayEquals(data, target.toByteArray());
119+
}
120+
121+
@Test
122+
void testWriteByteArrayWithOffset() throws IOException {
123+
final byte[] data = { 'X', 'Y', 'Z' };
124+
shielded.write(data, 1, 2);
125+
assertEquals(2, target.size());
126+
assertArrayEquals(new byte[] { 'Y', 'Z' }, target.toByteArray());
127+
}
128+
129+
@Test
130+
void testWriteInt() throws IOException {
131+
shielded.write('A');
132+
assertEquals(1, target.size());
133+
assertEquals('A', target.toByteArray()[0]);
134+
}
135+
}

0 commit comments

Comments
 (0)