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..a77a4d45566 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/FilterChannel.java @@ -0,0 +1,127 @@ +/* + * 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; + + /** + * Constructs a new instance. + * + * @param builder The source 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/test/java/org/apache/commons/io/channels/FileChannelProxy.java b/src/main/java/org/apache/commons/io/channels/FilterFileChannel.java similarity index 87% rename from src/test/java/org/apache/commons/io/channels/FileChannelProxy.java rename to src/main/java/org/apache/commons/io/channels/FilterFileChannel.java index ce7c0daddf7..f5861c0c970 100644 --- a/src/test/java/org/apache/commons/io/channels/FileChannelProxy.java +++ b/src/main/java/org/apache/commons/io/channels/FilterFileChannel.java @@ -24,16 +24,19 @@ import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.util.Objects; /** - * Proxies a FileChannel. + * Filters a {@link FileChannel}. + * + * @since 2.22.0 */ -class FileChannelProxy extends FileChannel { +public class FilterFileChannel extends FileChannel { - FileChannel fileChannel; + final FileChannel fileChannel; - FileChannelProxy(final FileChannel fileChannel) { - this.fileChannel = fileChannel; + FilterFileChannel(final FileChannel fileChannel) { + this.fileChannel = Objects.requireNonNull(fileChannel, "fileChannel"); } @Override @@ -121,6 +124,18 @@ public FileLock tryLock(final long position, final long size, final boolean shar return fileChannel.tryLock(position, size, shared); } + /** + * Unwraps this instance by returning the underlying {@link FileChannel}. + *

+ * Use with caution. + *

+ * + * @return the underlying {@link FileChannel}. + */ + public FileChannel unwrap() { + return fileChannel; + } + @Override public int write(final ByteBuffer src) throws IOException { return fileChannel.write(src); 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/FileChannelsTest.java b/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java index bbfbee9c575..c0727709daf 100644 --- a/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java +++ b/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java @@ -91,7 +91,7 @@ private static FileChannel wrap(final FileChannel fc, final FileChannelType file case STOCK: return fc; case PROXY: - return new FileChannelProxy(fc); + return new FilterFileChannel(fc); case FIXED_READ_SIZE: return new FixedReadSizeFileChannelProxy(fc, readSize); default: 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/FilterFileChannelTest.java b/src/test/java/org/apache/commons/io/channels/FilterFileChannelTest.java new file mode 100644 index 00000000000..9b539e4944d --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FilterFileChannelTest.java @@ -0,0 +1,445 @@ +/* + * 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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +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.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FilterFileChannel}. + */ +class FilterFileChannelTest { + + private FilterFileChannel build(final FileChannel channel) { + return new FilterFileChannel(channel); + } + + private FileChannel mockFileChannel() { + return mock(FileChannel.class); + } + + @Test + void testConstructorRequiresNonNullChannel() { + assertThrows(NullPointerException.class, () -> new FilterFileChannel(null)); + } + + @Test + void testEqualsDelegatesToChannel() { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + // equals() delegates to the underlying channel. The mock returns false by default, + // so filter.equals(anything) should be false. + final Object other = new Object(); + assertEquals(channel.equals(other), filter.equals(other)); + } + + @Test + void testForceDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + filter.force(true); + verify(channel).force(true); + } + + @Test + void testForcePropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + // IOException is checked via implCloseChannel; use force directly + final IOException ex = new IOException("force error"); + org.mockito.Mockito.doThrow(ex).when(channel).force(false); + assertThrows(IOException.class, () -> filter.force(false)); + verify(channel).force(false); + } + + @Test + void testHashCodeDelegatesToChannel() { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + // hashCode() delegates to the underlying channel; both should return the same value. + assertEquals(channel.hashCode(), filter.hashCode()); + } + + @Test + void testImplCloseChannelDelegatesToChannelClose() throws IOException { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + filter.close(); + verify(channel).close(); + } + + @Test + void testImplementsFileChannel() { + final FileChannel channel = mockFileChannel(); + assertInstanceOf(FileChannel.class, build(channel)); + } + + @Test + void testLockDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final FileLock lock = mock(FileLock.class); + when(channel.lock(0L, Long.MAX_VALUE, false)).thenReturn(lock); + final FilterFileChannel filter = build(channel); + assertSame(lock, filter.lock(0L, Long.MAX_VALUE, false)); + verify(channel).lock(0L, Long.MAX_VALUE, false); + } + + @Test + void testLockPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.lock(0L, Long.MAX_VALUE, false)).thenThrow(new IOException("lock error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.lock(0L, Long.MAX_VALUE, false)); + verify(channel).lock(0L, Long.MAX_VALUE, false); + } + + @Test + void testMapDelegatesToChannel() throws IOException { + final Path tmp = Files.createTempFile("FilterFileChannelTest", ".bin"); + try { + Files.write(tmp, new byte[1024]); + try (FileChannel real = FileChannel.open(tmp, StandardOpenOption.READ); FilterFileChannel filter = build(real)) { + final MappedByteBuffer mapped = filter.map(MapMode.READ_ONLY, 0L, 1024L); + assertNotNull(mapped); + assertEquals(1024, mapped.capacity()); + } + } finally { + Files.deleteIfExists(tmp); + } + } + + @Test + void testMapPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.map(MapMode.READ_ONLY, 0L, 1024L)).thenThrow(new IOException("map error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.map(MapMode.READ_ONLY, 0L, 1024L)); + verify(channel).map(MapMode.READ_ONLY, 0L, 1024L); + } + + @Test + void testPositionDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.position()).thenReturn(42L); + final FilterFileChannel filter = build(channel); + assertEquals(42L, filter.position()); + verify(channel).position(); + } + + @Test + void testPositionPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.position()).thenThrow(new IOException("position error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, filter::position); + verify(channel).position(); + } + + @Test + void testReadAtPositionDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer, 10L)).thenReturn(8); + final FilterFileChannel filter = build(channel); + assertEquals(8, filter.read(buffer, 10L)); + verify(channel).read(buffer, 10L); + } + + @Test + void testReadAtPositionPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer, 10L)).thenThrow(new IOException("read error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.read(buffer, 10L)); + verify(channel).read(buffer, 10L); + } + // -- read(ByteBuffer) -- + + @Test + void testReadDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(8); + final FilterFileChannel filter = build(channel); + assertEquals(8, filter.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenThrow(new IOException("read error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadReturnsMinusOneAtEndOfStream() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.read(buffer)).thenReturn(-1); + final FilterFileChannel filter = build(channel); + assertEquals(-1, filter.read(buffer)); + verify(channel).read(buffer); + } + + @Test + void testReadScatteringDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer[] buffers = { ByteBuffer.allocate(8), ByteBuffer.allocate(8) }; + when(channel.read(buffers, 0, 2)).thenReturn(16L); + final FilterFileChannel filter = build(channel); + assertEquals(16L, filter.read(buffers, 0, 2)); + verify(channel).read(buffers, 0, 2); + } + + @Test + void testReadScatteringPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer[] buffers = { ByteBuffer.allocate(8), ByteBuffer.allocate(8) }; + when(channel.read(buffers, 0, 2)).thenThrow(new IOException("scatter read error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.read(buffers, 0, 2)); + verify(channel).read(buffers, 0, 2); + } + + @Test + void testSetPositionDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.position(100L)).thenReturn(channel); + final FilterFileChannel filter = build(channel); + assertSame(channel, filter.position(100L)); + verify(channel).position(100L); + } + + @Test + void testSetPositionPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.position(100L)).thenThrow(new IOException("position error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.position(100L)); + verify(channel).position(100L); + } + + @Test + void testSizeDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.size()).thenReturn(2048L); + final FilterFileChannel filter = build(channel); + assertEquals(2048L, filter.size()); + verify(channel).size(); + } + + @Test + void testSizePropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.size()).thenThrow(new IOException("size error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, filter::size); + verify(channel).size(); + } + + @Test + void testToStringDelegatesToChannel() { + final FileChannel channel = mockFileChannel(); + when(channel.toString()).thenReturn("mockChannel"); + final FilterFileChannel filter = build(channel); + assertEquals("mockChannel", filter.toString()); + } + + @Test + void testTransferFromDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ReadableByteChannel src = mock(ReadableByteChannel.class); + when(channel.transferFrom(src, 0L, 512L)).thenReturn(512L); + final FilterFileChannel filter = build(channel); + assertEquals(512L, filter.transferFrom(src, 0L, 512L)); + verify(channel).transferFrom(src, 0L, 512L); + } + + @Test + void testTransferFromPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ReadableByteChannel src = mock(ReadableByteChannel.class); + when(channel.transferFrom(src, 0L, 512L)).thenThrow(new IOException("transferFrom error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.transferFrom(src, 0L, 512L)); + verify(channel).transferFrom(src, 0L, 512L); + } + + @Test + void testTransferToDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final WritableByteChannel target = mock(WritableByteChannel.class); + when(channel.transferTo(0L, 512L, target)).thenReturn(512L); + final FilterFileChannel filter = build(channel); + assertEquals(512L, filter.transferTo(0L, 512L, target)); + verify(channel).transferTo(0L, 512L, target); + } + + @Test + void testTransferToPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final WritableByteChannel target = mock(WritableByteChannel.class); + when(channel.transferTo(0L, 512L, target)).thenThrow(new IOException("transferTo error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.transferTo(0L, 512L, target)); + verify(channel).transferTo(0L, 512L, target); + } + + @Test + void testTruncateDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.truncate(512L)).thenReturn(channel); + final FilterFileChannel filter = build(channel); + assertSame(channel, filter.truncate(512L)); + verify(channel).truncate(512L); + } + + @Test + void testTruncatePropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.truncate(512L)).thenThrow(new IOException("truncate error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.truncate(512L)); + verify(channel).truncate(512L); + } + + @Test + void testTryLockDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final FileLock lock = mock(FileLock.class); + when(channel.tryLock(0L, Long.MAX_VALUE, false)).thenReturn(lock); + final FilterFileChannel filter = build(channel); + assertSame(lock, filter.tryLock(0L, Long.MAX_VALUE, false)); + verify(channel).tryLock(0L, Long.MAX_VALUE, false); + } + + @Test + void testTryLockPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.tryLock(0L, Long.MAX_VALUE, false)).thenThrow(new IOException("tryLock error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.tryLock(0L, Long.MAX_VALUE, false)); + verify(channel).tryLock(0L, Long.MAX_VALUE, false); + } + + @Test + void testTryLockReturnsNullWhenLockNotAcquired() throws IOException { + final FileChannel channel = mockFileChannel(); + when(channel.tryLock(0L, Long.MAX_VALUE, false)).thenReturn(null); + final FilterFileChannel filter = build(channel); + assertEquals(null, filter.tryLock(0L, Long.MAX_VALUE, false)); + verify(channel).tryLock(0L, Long.MAX_VALUE, false); + } + + @Test + void testUnwrapIsNotNull() { + assertNotNull(build(mockFileChannel()).unwrap()); + } + + @Test + void testUnwrapReturnsWrappedChannel() { + final FileChannel channel = mockFileChannel(); + final FilterFileChannel filter = build(channel); + assertSame(channel, filter.unwrap()); + } + + @Test + void testWriteAtPositionDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer, 10L)).thenReturn(16); + final FilterFileChannel filter = build(channel); + assertEquals(16, filter.write(buffer, 10L)); + verify(channel).write(buffer, 10L); + } + + @Test + void testWriteAtPositionPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer, 10L)).thenThrow(new IOException("write error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.write(buffer, 10L)); + verify(channel).write(buffer, 10L); + } + + @Test + void testWriteDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenReturn(16); + final FilterFileChannel filter = build(channel); + assertEquals(16, filter.write(buffer)); + verify(channel).write(buffer); + } + + @Test + void testWriteGatheringDelegatesToChannel() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer[] buffers = { ByteBuffer.allocate(8), ByteBuffer.allocate(8) }; + when(channel.write(buffers, 0, 2)).thenReturn(16L); + final FilterFileChannel filter = build(channel); + assertEquals(16L, filter.write(buffers, 0, 2)); + verify(channel).write(buffers, 0, 2); + } + + @Test + void testWriteGatheringPropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer[] buffers = { ByteBuffer.allocate(8), ByteBuffer.allocate(8) }; + when(channel.write(buffers, 0, 2)).thenThrow(new IOException("gather write error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.write(buffers, 0, 2)); + verify(channel).write(buffers, 0, 2); + } + + @Test + void testWritePropagatesIOException() throws IOException { + final FileChannel channel = mockFileChannel(); + final ByteBuffer buffer = ByteBuffer.allocate(16); + when(channel.write(buffer)).thenThrow(new IOException("write error")); + final FilterFileChannel filter = build(channel); + assertThrows(IOException.class, () -> filter.write(buffer)); + verify(channel).write(buffer); + } +} 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); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FixedReadSizeFileChannelProxy.java b/src/test/java/org/apache/commons/io/channels/FixedReadSizeFileChannelProxy.java index bfb1b72576b..bd77ed8320f 100644 --- a/src/test/java/org/apache/commons/io/channels/FixedReadSizeFileChannelProxy.java +++ b/src/test/java/org/apache/commons/io/channels/FixedReadSizeFileChannelProxy.java @@ -24,7 +24,7 @@ /** * Always reads the same amount of bytes on each call (or less). */ -class FixedReadSizeFileChannelProxy extends FileChannelProxy { +class FixedReadSizeFileChannelProxy extends FilterFileChannel { final int readSize; diff --git a/src/test/java/org/apache/commons/io/channels/NonBlockingFileChannelProxy.java b/src/test/java/org/apache/commons/io/channels/NonBlockingFileChannelProxy.java index 0af10704d41..d9c389d0619 100644 --- a/src/test/java/org/apache/commons/io/channels/NonBlockingFileChannelProxy.java +++ b/src/test/java/org/apache/commons/io/channels/NonBlockingFileChannelProxy.java @@ -25,7 +25,7 @@ /** * Simulates a non-blocking file channel by returning 0 from reads every other call as allowed by {@link ReadableByteChannel} and {@link FileChannel}. */ -class NonBlockingFileChannelProxy extends FileChannelProxy { +class NonBlockingFileChannelProxy extends FilterFileChannel { boolean toggleRead0;