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,

Reply via email to