diff --git a/src/main/java/org/apache/commons/io/output/FlushShieldOutputStream.java b/src/main/java/org/apache/commons/io/output/FlushShieldOutputStream.java new file mode 100644 index 00000000000..995cbde632d --- /dev/null +++ b/src/main/java/org/apache/commons/io/output/FlushShieldOutputStream.java @@ -0,0 +1,119 @@ +/* + * 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 + * + * https://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.commons.io.output; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.build.AbstractStreamBuilder; + +/** + * Re-implements {@link FilterOutputStream#flush()} to do nothing. + * + * @since 2.22.0 + */ +public final class FlushShieldOutputStream extends ProxyOutputStream { + + // @formatter:off + /** + * Builds a new {@link FlushShieldOutputStream}. + * + *

+ * Using File IO: + *

+ *
{@code
+     * FlushShieldOutputStream s = FlushShieldOutputStream.builder()
+     *   .setPath("over/there.out")
+     *   .setBufferSize(8192)
+     *   .get();
+     * }
+     * 
+ *

+ * Using NIO Path: + *

+ *
{@code
+     * FlushShieldOutputStream s = FlushShieldOutputStream.builder()
+     *   .setPath("over/there.out")
+     *   .setBufferSize(8192)
+     *   .get();
+     * }
+     * 
+ * + * @see #get() + * @since 2.13.0 + */ + // @formatter:on + public static class Builder extends AbstractStreamBuilder { + + /** + * Constructs a new builder of {@link FlushShieldOutputStream}. + */ + public Builder() { + // empty + } + + /** + * Builds a new {@link FlushShieldOutputStream}. + * + * @return a new instance. + * @throws IllegalStateException if the {@code origin} is {@code null}. + * @throws UnsupportedOperationException if the origin cannot be converted to an {@link OutputStream}. + * @throws IOException if an I/O error occurs converting to an {@link OutputStream} using {@link #getOutputStream()}. + * @see #getOutputStream() + * @see #getBufferSize() + * @see #getUnchecked() + */ + @Override + public FlushShieldOutputStream get() throws IOException { + return new FlushShieldOutputStream(this); + } + + } + + /** + * Constructs a new builder of {@link FlushShieldOutputStream}. + * + * @return a new builder of {@link FlushShieldOutputStream}. + */ + public static Builder builder() { + return new Builder(); + } + + @SuppressWarnings("resource") // caller closes. + private FlushShieldOutputStream(final Builder builder) throws IOException { + super(builder.getOutputStream()); + } + + /** + * Constructs a {@code FlushShieldOutputStream} filter for the specified underlying output stream. + * + * @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 + * without an underlying stream. + */ + public FlushShieldOutputStream(final OutputStream out) { + super(out); + } + + @Override + public void flush() throws IOException { + // shield: do nothing. + } +} diff --git a/src/test/java/org/apache/commons/io/output/FlushShieldOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/FlushShieldOutputStreamTest.java new file mode 100644 index 00000000000..3f840d05d08 --- /dev/null +++ b/src/test/java/org/apache/commons/io/output/FlushShieldOutputStreamTest.java @@ -0,0 +1,135 @@ +/* + * 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 + * + * https://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.commons.io.output; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FlushShieldOutputStream}. + */ +class FlushShieldOutputStreamTest { + + /** Tracks whether the underlying stream's close() was called. */ + private AtomicBoolean closed; + + /** Tracks whether the underlying stream's flush() was called. */ + private AtomicBoolean flushed; + + /** The stream under test. */ + private FlushShieldOutputStream shielded; + + /** The underlying byte-array-backed stream used as the delegate. */ + private ByteArrayOutputStream target; + + @BeforeEach + void setUp() { + flushed = new AtomicBoolean(); + closed = new AtomicBoolean(); + target = new ByteArrayOutputStream() { + + @Override + public void close() throws IOException { + closed.set(true); + super.close(); + } + + @Override + public void flush() throws IOException { + flushed.set(true); + super.flush(); + } + }; + shielded = new FlushShieldOutputStream(target); + } + + @Test + void testBuilderGet() throws IOException { + assertNotNull(FlushShieldOutputStream.builder().setOutputStream(target).get()); + } + + @Test + void testBuilderWithOutputStream() throws IOException { + try (FlushShieldOutputStream built = FlushShieldOutputStream.builder().setOutputStream(target).get()) { + assertNotNull(built); + assertInstanceOf(FlushShieldOutputStream.class, built); + // flush must be shielded for builder-created instances too + built.flush(); + assertFalse(flushed.get(), "flush() via builder instance must NOT reach the underlying stream."); + // writes must still work + built.write('B'); + assertEquals(1, target.size()); + assertEquals('B', target.toByteArray()[0]); + } + } + + @Test + void testCloseReachesUnderlying() throws IOException { + assertFalse(closed.get(), "close should not have been called yet."); + shielded.close(); + assertTrue(closed.get(), "close() must reach the underlying stream."); + } + + @Test + void testFlushCanBeCalledMultipleTimes() throws IOException { + shielded.flush(); + shielded.flush(); + shielded.flush(); + assertFalse(flushed.get(), "repeated flush() calls must NOT reach the underlying stream."); + } + + @Test + void testFlushDoesNotDelegateToUnderlying() throws IOException { + assertFalse(flushed.get(), "flush should not have been called yet."); + shielded.flush(); + assertFalse(flushed.get(), "flush() must NOT reach the underlying stream."); + } + + @Test + void testWriteByteArray() throws IOException { + final byte[] data = { 'H', 'i' }; + shielded.write(data); + assertEquals(2, target.size()); + assertArrayEquals(data, target.toByteArray()); + } + + @Test + void testWriteByteArrayWithOffset() throws IOException { + final byte[] data = { 'X', 'Y', 'Z' }; + shielded.write(data, 1, 2); + assertEquals(2, target.size()); + assertArrayEquals(new byte[] { 'Y', 'Z' }, target.toByteArray()); + } + + @Test + void testWriteInt() throws IOException { + shielded.write('A'); + assertEquals(1, target.size()); + assertEquals('A', target.toByteArray()[0]); + } +}