This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit f96f7ad0f81b69506c16b18c44730fb238fd57a8 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Feb 9 14:55:16 2023 +0100 When the `InputStream` to wrap in a `ChannelDataInput` is backed by an array, wrap the underlying array (in read-only mode) instead of copying it. --- .../sis/internal/sql/postgis/RasterReader.java | 14 ++- .../sis/internal/storage/io/ChannelDataInput.java | 27 +++-- .../storage/io/InputStreamArrayGetter.java | 135 +++++++++++++++++++++ .../storage/io/InputStreamArrayGetterTest.java | 62 ++++++++++ .../apache/sis/test/suite/StorageTestSuite.java | 1 + 5 files changed, 221 insertions(+), 18 deletions(-) diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java index 8b056cbc5a..70c5633727 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/postgis/RasterReader.java @@ -37,7 +37,6 @@ import java.awt.image.DataBufferDouble; import java.awt.image.WritableRaster; import java.awt.image.BufferedImage; import java.awt.image.RasterFormatException; -import java.nio.channels.Channels; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; @@ -46,6 +45,7 @@ import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.internal.coverage.j2d.ColorModelFactory; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; +import org.apache.sis.internal.storage.io.InputStreamArrayGetter; import org.apache.sis.internal.storage.io.ChannelDataInput; import org.apache.sis.internal.sql.feature.InfoStatements; import org.apache.sis.internal.util.Constants; @@ -70,7 +70,7 @@ import static org.apache.sis.internal.sql.postgis.Band.OPPOSITE_SIGN; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.4 * @since 1.2 */ public final class RasterReader extends RasterFormat { @@ -398,9 +398,11 @@ public final class RasterReader extends RasterFormat { * @throws IOException if an error occurred while reading data from the input stream. */ public ChannelDataInput channel(final InputStream input) throws IOException { - if (buffer == null) { - buffer = ByteBuffer.allocate(8192); - } - return new ChannelDataInput("raster", Channels.newChannel(input), buffer, false); + return InputStreamArrayGetter.channel("raster", input, () -> { + if (buffer == null) { + buffer = ByteBuffer.allocate(8192); + } + return buffer; + }); } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java index 40eb30dbe9..ec18853ec8 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java @@ -86,6 +86,9 @@ public class ChannelDataInput extends ChannelData { * If the buffer already contains some data, then the {@code filled} argument shall be {@code true}. * Otherwise (e.g. if it is a newly created buffer), then {@code filled} shall be {@code false}. * + * <p><b>Tip:</b> + * for building a data input from an input stream, see {@link InputStreamArrayGetter}.</p> + * * @param filename a short identifier (typically a filename without path) used for formatting error message. * @param channel the channel from where data are read. * @param buffer the buffer where to copy the data. @@ -105,18 +108,6 @@ public class ChannelDataInput extends ChannelData { } } - /** - * Creates a new input stream from the given {@code ChannelDataInput}. - * This constructor is invoked when we need to change the implementation class. - * The old input should not be used anymore after this constructor has been invoked. - * - * @param input the existing instance from which to takes the channel and buffer. - */ - ChannelDataInput(final ChannelDataInput input) { - super(input); - channel = input.channel; - } - /** * Creates a new instance for a buffer filled with the bytes to use. * This constructor uses an independent, read-only view of the given buffer. @@ -130,6 +121,18 @@ public class ChannelDataInput extends ChannelData { channel = new NullChannel(); } + /** + * Creates a new input stream from the given {@code ChannelDataInput}. + * This constructor is invoked when we need to change the implementation class. + * The old input should not be used anymore after this constructor has been invoked. + * + * @param input the existing instance from which to takes the channel and buffer. + */ + ChannelDataInput(final ChannelDataInput input) { + super(input); + channel = input.channel; + } + /** * Returns the length of the stream (in bytes), or -1 if unknown. * The length is relative to the channel position at {@linkplain #ChannelDataInput construction time}. diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamArrayGetter.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamArrayGetter.java new file mode 100644 index 0000000000..7432a391a7 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/InputStreamArrayGetter.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://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.sis.internal.storage.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.function.Supplier; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.internal.storage.StoreUtilities; + + +/** + * A hack for getting the backing array of an input stream if that array exists. + * This class searches the array in the following locations: + * + * <ul> + * <li>{@link ByteArrayInputStream#buf} together with offset and length. + * Those fields have {@code protected} access, so they are committed Java API. + * However as of Java 19, there is no public accessor for them.</li> + * </ul> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +public final class InputStreamArrayGetter extends OutputStream { + /** + * The buffer wrapping the array, or {@code null} if unknown. + * This buffer is not read-only, but nevertheless should not be modified. + */ + private ByteBuffer buffer; + + /** + * Creates a pseudo output stream to use as a trick for getting the backing array. + */ + private InputStreamArrayGetter() { + } + + /** + * Whether it is hopefully safe to apply the trick used by this class for getting the backing array. + * The trick is that the {@link ByteArrayInputStream#transferTo(OutputStream)} implementation gives + * the backing array in argument in a call to {@link InputStream#write(byte[], int, int)} method. + * This is verified by looking at the OpenJDK source code, and presumed relatively stable because + * the code uses only protected fields and public methods, which are committed API. + * But because users could override the {@code transferTo(…)} method in a subclass, + * we rely on this strick only if the method implementation is the one provided by + * the standard {@link ByteArrayInputStream} class. + * + * @param input the input stream for which to verify the implementation class. + * @return whether the implementation is the OpenJDK one. + */ + private static boolean isKnownImplementation(final InputStream input) { + try { + return input.getClass().getMethod("transferTo", OutputStream.class).getDeclaringClass() == ByteArrayInputStream.class; + } catch (NoSuchMethodException e) { + // Should not happen because we requested a method which should exist. + Logging.unexpectedException(StoreUtilities.LOGGER, InputStreamArrayGetter.class, "isKnownImplementation", e); + return false; + } + } + + /** + * Creates a new data input for the given input stream. + * If the input stream is backed by an array, the array will be wrapped in a read-only mode. + * + * @param filename a short identifier (typically a filename without path) used for formatting error message. + * @param input the input stream from where data are read. + * @param bs supplier of the buffer where to copy the data. + * @return the data input using a readable channel. + * @throws IOException if an error occurred while reading the input stream. + */ + public static ChannelDataInput channel(final String filename, final InputStream input, final Supplier<ByteBuffer> bs) + throws IOException + { + if (isKnownImplementation(input)) { + final InputStreamArrayGetter getter = new InputStreamArrayGetter(); + input.transferTo(getter); + if (getter.buffer != null) { + return new ChannelDataInput(filename, getter.buffer); + } + } + final ReadableByteChannel channel = Channels.newChannel(input); + return new ChannelDataInput(filename, channel, bs.get(), false); + } + + /** + * Invoked by {@link ByteArrayInputStream#transferTo(OutputStream)}. + * We use this method as a callback for getting the underlying array. + * + * @param array the data. + * @param offset the start offset in the array. + * @param length the number of valid bytes in the array. + * @throws IOException if this method is invoked more than once. + */ + @Override + public void write(final byte[] array, final int offset, final int length) throws IOException { + if (buffer == null) { + buffer = ByteBuffer.wrap(array, offset, length); + } else { + super.write(array, offset, length); + } + } + + /** + * Should never be invoked. If this method is nevertheless invoked, + * then the {@link ByteArrayInputStream#transferTo(OutputStream)} + * implementation is not what we expected. + * + * @param b ignored. + * @throws IOException always thrown. + */ + @Override + public void write(int b) throws IOException { + throw new IOException("Unexpected implementation."); + } +} diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/InputStreamArrayGetterTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/InputStreamArrayGetterTest.java new file mode 100644 index 0000000000..ae4ea03579 --- /dev/null +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/io/InputStreamArrayGetterTest.java @@ -0,0 +1,62 @@ +/* + * 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 + * + * http://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.sis.internal.storage.io; + +import java.io.IOException; +import java.io.ByteArrayInputStream; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests the {@link InputStreamArrayGetter} class. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +public final class InputStreamArrayGetterTest extends TestCase { + /** + * Tests the creation of a channel data input which uses directly the array. + * + * @throws IOException if an error occurred while creating the channel data input. + */ + @Test + public void testDirectChannel() throws IOException { + final var array = new byte[20]; + for (int i=0; i<array.length; i++) { + array[i] = (byte) ((i ^ 5772) * 37); + } + final int offset = 7; + final var input = new ByteArrayInputStream(array, offset, 9); + final ChannelDataInput data = InputStreamArrayGetter.channel("Test", input, () -> { + throw new AssertionError("Should not create new buffer."); + }); + /* + * Replace a few values in the backing array AFTER the `ChannelDataInput` creation. + * If the data input does not wrap the array, the changes below would be unnoticed. + */ + array[offset + 1] = 99; // Replace 20. + array[offset + 4] = 100; // Replace -125. + final byte[] expected = {23, 99, 57, 94, 100, -128}; + for (int i=0; i<expected.length; i++) { + assertEquals(expected[i], data.readByte()); + } + } +} diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java index 8a8556ede6..e84a691f07 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java @@ -40,6 +40,7 @@ import org.junit.BeforeClass; org.apache.sis.internal.storage.io.HyperRectangleReaderTest.class, org.apache.sis.internal.storage.io.RewindableLineReaderTest.class, org.apache.sis.internal.storage.io.FileCacheByteChannelTest.class, + org.apache.sis.internal.storage.io.InputStreamArrayGetterTest.class, org.apache.sis.internal.storage.MetadataBuilderTest.class, org.apache.sis.internal.storage.RangeArgumentTest.class, org.apache.sis.internal.storage.MemoryGridResourceTest.class,