diff --git a/src/main/java/org/apache/commons/io/channels/FilterByteChannel.java b/src/main/java/org/apache/commons/io/channels/FilterByteChannel.java new file mode 100644 index 00000000000..4bd17f2d0ce --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterByteChannel.java @@ -0,0 +1,106 @@ +/* + * 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.channels; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.io.input.ProxyReader; +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.io.output.ProxyWriter; + +/** + * A {@link ByteChannel} filter which delegates to the wrapped {@link ByteChannel}. + * + * @param the {@link ByteChannel} type. + * @see FilterInputStream + * @see FilterOutputStream + * @see FilterReader + * @see FilterWritableByteChannel + * @see ProxyInputStream + * @see ProxyOutputStream + * @see ProxyReader + * @see ProxyWriter + * @since 2.22.0 + */ +public class FilterByteChannel extends FilterChannel implements ByteChannel { + + /** + * Builds instances of {@link FilterByteChannel} for subclasses. + * + * @param The {@link FilterByteChannel} type. + * @param The {@link ByteChannel} type wrapped by the FilterChannel. + * @param The builder type. + */ + public abstract static class AbstractBuilder, C extends ByteChannel, B extends AbstractBuilder> + extends FilterChannel.AbstractBuilder { + + /** + * Constructs a new builder for {@link FilterByteChannel}. + */ + protected AbstractBuilder() { + // empty + } + } + + /** + * Builds instances of {@link FilterByteChannel}. + */ + public static class Builder extends AbstractBuilder, ByteChannel, Builder> { + + /** + * Builds instances of {@link FilterByteChannel}. + */ + protected Builder() { + // empty + } + + @Override + public FilterByteChannel get() throws IOException { + return new FilterByteChannel<>(this); + } + } + + /** + * Creates a new {@link Builder}. + * + * @return a new {@link Builder}. + */ + public static Builder forByteChannel() { + return new Builder(); + } + + FilterByteChannel(final AbstractBuilder builder) throws IOException { + super(builder); + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + return channel.read(dst); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + return channel.write(src); + } +} diff --git a/src/main/java/org/apache/commons/io/channels/FilterChannel.java b/src/main/java/org/apache/commons/io/channels/FilterChannel.java new file mode 100644 index 00000000000..10ac54e01ef --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterChannel.java @@ -0,0 +1,125 @@ +/* + * 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.channels; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.nio.channels.Channel; + +import org.apache.commons.io.build.AbstractStreamBuilder; +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.io.input.ProxyReader; +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.io.output.ProxyWriter; + +/** + * A {@link Channel} filter which delegates to the wrapped {@link Channel}. + * + * @param the {@link Channel} type. + * @see FilterInputStream + * @see FilterOutputStream + * @see FilterReader + * @see FilterWritableByteChannel + * @see ProxyInputStream + * @see ProxyOutputStream + * @see ProxyReader + * @see ProxyWriter + * @since 2.22.0 + */ +public class FilterChannel implements Channel { + + /** + * Builds instances of {@link FilterChannel} for subclasses. + * + * @param The {@link FilterChannel} type. + * @param The {@link Channel} type wrapped by the FilterChannel. + * @param The builder type. + */ + public abstract static class AbstractBuilder, C extends Channel, B extends AbstractBuilder> + extends AbstractStreamBuilder> { + + /** + * Constructs instance for subclasses. + */ + protected AbstractBuilder() { + // empty + } + } + + /** + * Builds instances of {@link FilterChannel}. + */ + public static class Builder extends AbstractBuilder, Channel, Builder> { + + /** + * Builds instances of {@link FilterChannel}. + */ + protected Builder() { + // empty + } + + @Override + public FilterChannel get() throws IOException { + return new FilterChannel<>(this); + } + } + + /** + * Creates a new {@link Builder}. + * + * @return a new {@link Builder}. + */ + public static Builder forChannel() { + return new Builder(); + } + + final C channel; + + /** + * @param builder + * @throws IOException if an I/O error occurs. + */ + @SuppressWarnings("unchecked") + FilterChannel(final AbstractBuilder builder) throws IOException { + channel = (C) builder.getChannel(Channel.class); + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + /** + * Unwraps this instance by returning the underlying {@link Channel} of type {@code C}. + *

+ * Use with caution. + *

+ * + * @return the underlying channel of type {@code C}. + */ + public C unwrap() { + return channel; + } +} diff --git a/src/main/java/org/apache/commons/io/channels/FilterReadableByteChannel.java b/src/main/java/org/apache/commons/io/channels/FilterReadableByteChannel.java new file mode 100644 index 00000000000..d29ce815c3a --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterReadableByteChannel.java @@ -0,0 +1,101 @@ +/* + * 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.channels; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.io.input.ProxyReader; +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.io.output.ProxyWriter; + +/** + * A {@link ReadableByteChannel} filter which delegates to the wrapped {@link ReadableByteChannel}. + * + * @param the {@link ReadableByteChannel} type. + * @see FilterInputStream + * @see FilterOutputStream + * @see FilterReader + * @see FilterWritableByteChannel + * @see ProxyInputStream + * @see ProxyOutputStream + * @see ProxyReader + * @see ProxyWriter + * @since 2.22.0 + */ +public class FilterReadableByteChannel extends FilterChannel implements ReadableByteChannel { + + /** + * Builds instances of {@link FilterReadableByteChannel} for subclasses. + * + * @param The {@link FilterReadableByteChannel} type. + * @param The {@link ReadableByteChannel} type wrapped by the FilterChannel. + * @param The builder type. + */ + public abstract static class AbstractBuilder, C extends ReadableByteChannel, B extends AbstractBuilder> + extends FilterChannel.AbstractBuilder { + + /** + * Constructs a new builder for {@link FilterReadableByteChannel}. + */ + public AbstractBuilder() { + // empty + } + } + + /** + * Builds instances of {@link FilterByteChannel}. + */ + public static class Builder extends AbstractBuilder, ReadableByteChannel, Builder> { + + /** + * Builds instances of {@link FilterByteChannel}. + */ + protected Builder() { + // empty + } + + @Override + public FilterReadableByteChannel get() throws IOException { + return new FilterReadableByteChannel<>(this); + } + } + + /** + * Creates a new {@link Builder}. + * + * @return a new {@link Builder}. + */ + public static Builder forReadableByteChannel() { + return new Builder(); + } + + FilterReadableByteChannel(final Builder builder) throws IOException { + super(builder); + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + return channel.read(dst); + } +} diff --git a/src/main/java/org/apache/commons/io/channels/FilterSeekableByteChannel.java b/src/main/java/org/apache/commons/io/channels/FilterSeekableByteChannel.java new file mode 100644 index 00000000000..b36d0b471a9 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterSeekableByteChannel.java @@ -0,0 +1,115 @@ +/* + * 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.channels; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; + +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.io.input.ProxyReader; +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.io.output.ProxyWriter; + +/** + * A {@link SeekableByteChannel} filter which delegates to the wrapped {@link SeekableByteChannel}. + * + * @param the {@link SeekableByteChannel} type. + * @see FilterInputStream + * @see FilterOutputStream + * @see FilterReader + * @see FilterWritableByteChannel + * @see ProxyInputStream + * @see ProxyOutputStream + * @see ProxyReader + * @see ProxyWriter + * @since 2.22.0 + */ +public class FilterSeekableByteChannel extends FilterByteChannel implements SeekableByteChannel { + + /** + * Builds instances of {@link FilterSeekableByteChannel} for subclasses. + * + * @param The {@link FilterSeekableByteChannel} type. + * @param The {@link SeekableByteChannel} type wrapped by the FilterChannel. + * @param The builder type. + */ + public abstract static class AbstractBuilder, C extends SeekableByteChannel, B extends AbstractBuilder> + extends FilterByteChannel.AbstractBuilder { + + /** + * Constructs a new builder for {@link FilterSeekableByteChannel}. + */ + public AbstractBuilder() { + // empty + } + } + + /** + * Builds instances of {@link FilterSeekableByteChannel}. + */ + public static class Builder extends AbstractBuilder, SeekableByteChannel, Builder> { + + /** + * Builds instances of {@link FilterSeekableByteChannel}. + */ + protected Builder() { + // empty + } + + @Override + public FilterSeekableByteChannel get() throws IOException { + return new FilterSeekableByteChannel<>(this); + } + } + + /** + * Creates a new {@link Builder}. + * + * @return a new {@link Builder}. + */ + public static Builder forSeekableByteChannel() { + return new Builder(); + } + + FilterSeekableByteChannel(final Builder builder) throws IOException { + super(builder); + } + + @Override + public long position() throws IOException { + return channel.position(); + } + + @Override + public SeekableByteChannel position(final long newPosition) throws IOException { + return channel.position(newPosition); + } + + @Override + public long size() throws IOException { + return channel.size(); + } + + @Override + public SeekableByteChannel truncate(final long size) throws IOException { + return channel.truncate(size); + } +} diff --git a/src/main/java/org/apache/commons/io/channels/FilterWritableByteChannel.java b/src/main/java/org/apache/commons/io/channels/FilterWritableByteChannel.java new file mode 100644 index 00000000000..da2f3381758 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterWritableByteChannel.java @@ -0,0 +1,101 @@ +/* + * 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.channels; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.FilterReader; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.io.input.ProxyReader; +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.io.output.ProxyWriter; + +/** + * A {@link WritableByteChannel} filter which delegates to the wrapped {@link WritableByteChannel}. + * + * @param the {@link WritableByteChannel} type. + * @see FilterInputStream + * @see FilterOutputStream + * @see FilterReader + * @see FilterWritableByteChannel + * @see ProxyInputStream + * @see ProxyOutputStream + * @see ProxyReader + * @see ProxyWriter + * @since 2.22.0 + */ +public class FilterWritableByteChannel extends FilterChannel implements WritableByteChannel { + + /** + * Builds instances of {@link FilterWritableByteChannel} for subclasses. + * + * @param The {@link FilterWritableByteChannel} type. + * @param The {@link WritableByteChannel} type wrapped by the FilterChannel. + * @param The builder type. + */ + public abstract static class AbstractBuilder, C extends WritableByteChannel, B extends AbstractBuilder> + extends FilterChannel.AbstractBuilder { + + /** + * Constructs a new builder for {@link FilterWritableByteChannel}. + */ + public AbstractBuilder() { + // empty + } + } + + /** + * Builds instances of {@link FilterByteChannel}. + */ + public static class Builder extends AbstractBuilder, WritableByteChannel, Builder> { + + /** + * Builds instances of {@link FilterByteChannel}. + */ + protected Builder() { + // empty + } + + @Override + public FilterWritableByteChannel get() throws IOException { + return new FilterWritableByteChannel<>(this); + } + } + + /** + * Creates a new {@link Builder}. + * + * @return a new {@link Builder}. + */ + public static Builder forWritableByteChannel() { + return new Builder(); + } + + FilterWritableByteChannel(final Builder builder) throws IOException { + super(builder); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + return channel.write(src); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FilterByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterByteChannelTest.java new file mode 100644 index 00000000000..0994b726d14 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterByteChannelTest.java @@ -0,0 +1,143 @@ +/* + * 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.channels; + +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterByteChannel}. + */ +class FilterByteChannelTest { + + private FilterByteChannel buildFilterByteChannel(final ByteChannel channel) throws IOException { + return FilterByteChannel.forByteChannel().setChannel(channel).get(); + } + + @Test + void testBuilderRequiresChannel() { + assertThrows(IllegalStateException.class, () -> FilterByteChannel.forByteChannel().get()); + } + + @Test + void testClose() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testImplementsByteChannel() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertInstanceOf(ByteChannel.class, filterChannel); + } + + @Test + void testIsOpenAfterClose() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + assertFalse(filterChannel.isOpen()); + } + + @Test + void testIsOpenDelegatesToChannel() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertTrue(filterChannel.isOpen()); + assertFalse(filterChannel.isOpen()); + verify(channel, times(2)).isOpen(); + } + + @Test + void testReadDelegatesToChannel() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(8); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertEquals(8, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadPropagatesIOException() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenThrow(new IOException("read error")); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertThrows(IOException.class, () -> filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadReturnsMinusOneAtEndOfStream() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(-1); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertEquals(-1, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testUnwrapReturnsWrappedChannel() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertSame(channel, filterChannel.unwrap()); + } + + @Test + void testWriteDelegatesToChannel() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenReturn(16); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertEquals(16, filterChannel.write(buffer)); + verify(channel).write(buffer); + } + + @Test + void testWritePropagatesIOException() throws IOException { + final ByteChannel channel = mock(ByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenThrow(new IOException("write error")); + final FilterByteChannel filterChannel = buildFilterByteChannel(channel); + assertThrows(IOException.class, () -> filterChannel.write(buffer)); + verify(channel).write(buffer); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FilterChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterChannelTest.java new file mode 100644 index 00000000000..68bea2a717f --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterChannelTest.java @@ -0,0 +1,93 @@ +/* +* 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.channels; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.channels.Channel; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterChannel}. + */ +class FilterChannelTest { + + private FilterChannel buildFilterChannel(final Channel channel) throws IOException { + return FilterChannel.forChannel().setChannel(channel).get(); + } + + @Test + void testBuilderRequiresChannel() { + assertThrows(IllegalStateException.class, () -> FilterChannel.forChannel().get()); + } + + @Test + void testClose() throws IOException { + final Channel channel = mock(Channel.class); + final FilterChannel filterChannel = buildFilterChannel(channel); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testCloseClosesUnderlyingChannel() throws IOException { + final Channel channel = mock(Channel.class); + when(channel.isOpen()).thenReturn(true); + final FilterChannel filterChannel = buildFilterChannel(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testIsOpenAfterClose() throws IOException { + final Channel channel = mock(Channel.class); + // Simulate the channel reporting open=true then open=false after close + when(channel.isOpen()).thenReturn(true).thenReturn(false); + final FilterChannel filterChannel = buildFilterChannel(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + assertFalse(filterChannel.isOpen()); + } + + @Test + void testIsOpenDelegatesToChannel() throws IOException { + final Channel channel = mock(Channel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterChannel filterChannel = buildFilterChannel(channel); + assertTrue(filterChannel.isOpen()); + assertFalse(filterChannel.isOpen()); + verify(channel, org.mockito.Mockito.times(2)).isOpen(); + } + + @Test + void testUnwrapReturnsWrappedChannel() throws IOException { + final Channel channel = mock(Channel.class); + final FilterChannel filterChannel = buildFilterChannel(channel); + assertSame(channel, filterChannel.unwrap()); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FilterReadableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterReadableByteChannelTest.java new file mode 100644 index 00000000000..48530546e4c --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterReadableByteChannelTest.java @@ -0,0 +1,133 @@ +/* + * 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.channels; + +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterReadableByteChannel}. + */ +class FilterReadableByteChannelTest { + + private FilterReadableByteChannel build(final ReadableByteChannel channel) throws IOException { + return FilterReadableByteChannel.forReadableByteChannel().setChannel(channel).get(); + } + + @Test + void testBuilderRequiresChannel() { + assertThrows(IllegalStateException.class, () -> FilterReadableByteChannel.forReadableByteChannel().get()); + } + + @Test + void testClose() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final FilterReadableByteChannel filterChannel = build(channel); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testImplementsReadableByteChannel() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + assertInstanceOf(ReadableByteChannel.class, build(channel)); + } + + @Test + void testIsOpenAfterClose() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterReadableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + assertFalse(filterChannel.isOpen()); + } + + @Test + void testIsOpenDelegatesToChannel() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterReadableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + assertFalse(filterChannel.isOpen()); + verify(channel, times(2)).isOpen(); + } + + @Test + void testReadDelegatesToChannel() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(8); + final FilterReadableByteChannel filterChannel = build(channel); + assertEquals(8, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadMultipleCalls() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(4, 4, -1); + final FilterReadableByteChannel filterChannel = build(channel); + assertEquals(4, filterChannel.read(buffer)); + assertEquals(4, filterChannel.read(buffer)); + assertEquals(-1, filterChannel.read(buffer)); + verify(channel, times(3)).read(buffer); + } + + @Test + void testReadPropagatesIOException() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenThrow(new IOException("read error")); + final FilterReadableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadReturnsMinusOneAtEndOfStream() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(-1); + final FilterReadableByteChannel filterChannel = build(channel); + assertEquals(-1, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testUnwrapReturnsWrappedChannel() throws IOException { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + assertSame(channel, build(channel).unwrap()); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FilterSeekableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterSeekableByteChannelTest.java new file mode 100644 index 00000000000..8f71c38d1f2 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterSeekableByteChannelTest.java @@ -0,0 +1,220 @@ +/* + * 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.channels; + +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterSeekableByteChannel}. + */ +class FilterSeekableByteChannelTest { + + private FilterSeekableByteChannel build(final SeekableByteChannel channel) throws IOException { + return FilterSeekableByteChannel.forSeekableByteChannel().setChannel(channel).get(); + } + + @Test + void testBuilderRequiresChannel() { + assertThrows(IllegalStateException.class, () -> FilterSeekableByteChannel.forSeekableByteChannel().get()); + } + + @Test + void testClose() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final FilterSeekableByteChannel filterChannel = build(channel); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testImplementsSeekableByteChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + assertInstanceOf(SeekableByteChannel.class, build(channel)); + } + + @Test + void testIsOpenAfterClose() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterSeekableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + assertFalse(filterChannel.isOpen()); + } + + @Test + void testIsOpenDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterSeekableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + assertFalse(filterChannel.isOpen()); + verify(channel, times(2)).isOpen(); + } + // -- read -- + + @Test + void testPositionDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.position()).thenReturn(42L); + final FilterSeekableByteChannel filterChannel = build(channel); + assertEquals(42L, filterChannel.position()); + verify(channel).position(); + } + + @Test + void testPositionPropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.position()).thenThrow(new IOException("position error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, filterChannel::position); + verify(channel).position(); + } + + @Test + void testReadDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(8); + final FilterSeekableByteChannel filterChannel = build(channel); + assertEquals(8, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + // -- write -- + + @Test + void testReadPropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenThrow(new IOException("read error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadReturnsMinusOneAtEndOfStream() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(-1); + final FilterSeekableByteChannel filterChannel = build(channel); + assertEquals(-1, filterChannel.read(buffer)); + verify(channel).read(buffer); + } + // -- position() -- + + @Test + void testSetPositionDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.position(10L)).thenReturn(channel); + final FilterSeekableByteChannel filterChannel = build(channel); + assertSame(channel, filterChannel.position(10L)); + verify(channel).position(10L); + } + + @Test + void testSetPositionPropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.position(10L)).thenThrow(new IOException("position error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.position(10L)); + verify(channel).position(10L); + } + // -- position(long) -- + + @Test + void testSizeDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.size()).thenReturn(1024L); + final FilterSeekableByteChannel filterChannel = build(channel); + assertEquals(1024L, filterChannel.size()); + verify(channel).size(); + } + + @Test + void testSizePropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.size()).thenThrow(new IOException("size error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, filterChannel::size); + verify(channel).size(); + } + // -- size() -- + + @Test + void testTruncateDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.truncate(512L)).thenReturn(channel); + final FilterSeekableByteChannel filterChannel = build(channel); + assertSame(channel, filterChannel.truncate(512L)); + verify(channel).truncate(512L); + } + + @Test + void testTruncatePropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.truncate(512L)).thenThrow(new IOException("truncate error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.truncate(512L)); + verify(channel).truncate(512L); + } + // -- truncate(long) -- + + @Test + void testUnwrapReturnsWrappedChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + assertSame(channel, build(channel).unwrap()); + } + + @Test + void testWriteDelegatesToChannel() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenReturn(16); + final FilterSeekableByteChannel filterChannel = build(channel); + assertEquals(16, filterChannel.write(buffer)); + verify(channel).write(buffer); + } + // -- unwrap -- + + @Test + void testWritePropagatesIOException() throws IOException { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenThrow(new IOException("write error")); + final FilterSeekableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.write(buffer)); + verify(channel).write(buffer); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FilterWritableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterWritableByteChannelTest.java new file mode 100644 index 00000000000..42ee4cb7251 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterWritableByteChannelTest.java @@ -0,0 +1,132 @@ +/* + * 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.channels; + +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterWritableByteChannel}. + */ +class FilterWritableByteChannelTest { + + private FilterWritableByteChannel build(final WritableByteChannel channel) throws IOException { + return FilterWritableByteChannel.forWritableByteChannel().setChannel(channel).get(); + } + + @Test + void testBuilderRequiresChannel() { + assertThrows(IllegalStateException.class, () -> FilterWritableByteChannel.forWritableByteChannel().get()); + } + + @Test + void testClose() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + final FilterWritableByteChannel filterChannel = build(channel); + filterChannel.close(); + verify(channel).close(); + } + + @Test + void testImplementsWritableByteChannel() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + assertInstanceOf(WritableByteChannel.class, build(channel)); + } + + @Test + void testIsOpenAfterClose() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterWritableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + filterChannel.close(); + verify(channel).close(); + assertFalse(filterChannel.isOpen()); + } + + @Test + void testIsOpenDelegatesToChannel() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + when(channel.isOpen()).thenReturn(true, false); + final FilterWritableByteChannel filterChannel = build(channel); + assertTrue(filterChannel.isOpen()); + assertFalse(filterChannel.isOpen()); + verify(channel, times(2)).isOpen(); + } + + @Test + void testUnwrapReturnsWrappedChannel() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + assertSame(channel, build(channel).unwrap()); + } + + @Test + void testWriteDelegatesToChannel() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenReturn(16); + final FilterWritableByteChannel filterChannel = build(channel); + assertEquals(16, filterChannel.write(buffer)); + verify(channel).write(buffer); + } + + @Test + void testWriteMultipleCalls() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenReturn(8, 8); + final FilterWritableByteChannel filterChannel = build(channel); + assertEquals(8, filterChannel.write(buffer)); + assertEquals(8, filterChannel.write(buffer)); + verify(channel, times(2)).write(buffer); + } + + @Test + void testWritePropagatesIOException() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenThrow(new IOException("write error")); + final FilterWritableByteChannel filterChannel = build(channel); + assertThrows(IOException.class, () -> filterChannel.write(buffer)); + verify(channel).write(buffer); + } + + @Test + void testWriteReturnsZeroOnFullBuffer() throws IOException { + final WritableByteChannel channel = mock(WritableByteChannel.class); + final ByteBuffer buffer = ByteBuffer.allocate(0); + when(channel.write(buffer)).thenReturn(0); + final FilterWritableByteChannel filterChannel = build(channel); + assertEquals(0, filterChannel.write(buffer)); + verify(channel).write(buffer); + } +}