You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/10/07 20:23:41 UTC

[sis] 02/06: `ChannelDataInput` implements `DataInput` for allowing `instanceof` checks against a public interface.

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 255a5b2bd73bcf4873d78a677eed30140f843e6d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Sep 17 14:51:39 2023 +0200

    `ChannelDataInput` implements `DataInput` for allowing `instanceof` checks against a public interface.
---
 .../org/apache/sis/io/stream/ChannelDataInput.java | 125 +++++++++++++++++++--
 .../sis/io/stream/ChannelImageInputStream.java     |  70 +-----------
 .../apache/sis/io/stream/InputStreamAdapter.java   |  58 +++++++++-
 .../org/apache/sis/storage/StorageConnector.java   |  99 ++++++++--------
 .../apache/sis/storage/StorageConnectorTest.java   |  10 +-
 5 files changed, 227 insertions(+), 135 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
index 5f0c4c5c9e..a7aa272888 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataInput.java
@@ -16,10 +16,13 @@
  */
 package org.apache.sis.io.stream;
 
+import java.io.DataInput;
+import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.EOFException;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.CharBuffer;
 import java.nio.ShortBuffer;
 import java.nio.IntBuffer;
@@ -51,17 +54,14 @@ import static org.apache.sis.util.ArgumentChecks.ensureBetween;
  * <p>Since this class is only a helper tool, it does not "own" the channel and consequently does not provide
  * {@code close()} method. It is users responsibility to close the channel after usage.</p>
  *
- * <h2>Relationship with {@code DataInput}</h2>
- * This class API is compatibly with the {@link java.io.DataInput} interface, so subclasses can implement that
- * interface if they wish. This class does not implement {@code DataInput} itself because it is not needed for
- * SIS purposes, and because {@code DataInput} has undesirable methods ({@code readLine()} and {@code readUTF()}).
- * However, the {@link ChannelImageInputStream} class implements the {@code DataInput} interface, together with
- * the {@link javax.imageio.stream.ImageInputStream} one, mostly for situations when inter-operability with
- * {@link javax.imageio} is needed.
+ * <h2>Relationship with {@code ChannelImageInputStream}</h2>
+ * This class API is compatible with the {@link javax.imageio.stream.ImageInputStream} interface, so subclasses
+ * can implement that interface if they wish. This is done by {@link ChannelImageInputStream} for situations
+ * when inter-operability with {@link javax.imageio} is needed.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public class ChannelDataInput extends ChannelData {
+public class ChannelDataInput extends ChannelData implements DataInput {
     /**
      * Minimum number of bytes to skip in the {@code seek(long)} operation.
      * If there is less bytes to skip, then it is not worth to do a seek
@@ -173,7 +173,7 @@ public class ChannelDataInput extends ChannelData {
 
     /**
      * Returns {@code true} if the buffer or the channel has at least one byte remaining.
-     * If the {@linkplain #buffer buffer} has no remaining bytes, then this method will attempts
+     * If the {@linkplain #buffer buffer} has no remaining bytes, then this method will attempt
      * to read at least one byte from the {@linkplain #channel}. If no bytes can be read because
      * the channel has reached the end of stream, then this method returns {@code false}.
      *
@@ -244,10 +244,10 @@ public class ChannelDataInput extends ChannelData {
     }
 
     /**
-     * Pushes back the last processed byte. This is used when a call to {@code readBit()} did not
-     * used every bits in a byte, or when {@code readLine()} checked for the Windows-style of EOL.
+     * Pushes back the last processed byte. This is used when a call to {@link #readBits(int)} did not
+     * used every bits in a byte, or when {@link #readLine()} checked for the Windows-style of EOL.
      */
-    final void pushBack() {
+    private void pushBack() {
         buffer.position(buffer.position() - 1);
     }
 
@@ -257,6 +257,8 @@ public class ChannelDataInput extends ChannelData {
      *
      * @return the value of the next bit from the stream.
      * @throws IOException if an error occurred while reading (including EOF).
+     *
+     * @see #readBoolean()
      */
     public final int readBit() throws IOException {
         ensureBufferContains(Byte.BYTES);
@@ -297,6 +299,26 @@ public class ChannelDataInput extends ChannelData {
         return value;
     }
 
+    /**
+     * Reads a byte from the stream and returns {@code true} if it is nonzero, {@code false} otherwise.
+     * The implementation is as below:
+     *
+     * {@snippet lang="java" :
+     *     return readByte() != 0;
+     *     }
+     *
+     * For reading a single bit, use {@link #readBit()} instead.
+     *
+     * @return the value of the next boolean from the stream.
+     * @throws IOException if an error (including EOF) occurred while reading the stream.
+     *
+     * @see #readBit()
+     */
+    @Override
+    public final boolean readBoolean() throws IOException {
+        return readByte() != 0;
+    }
+
     /**
      * Reads the next byte value (8 bits) from the stream. This method ensures that there is at
      * least 1 byte remaining in the buffer, reading new bytes from the channel if necessary,
@@ -305,6 +327,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next byte from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final byte readByte() throws IOException {
         ensureBufferContains(Byte.BYTES);
         return buffer.get();
@@ -321,6 +344,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next unsigned byte from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final int readUnsignedByte() throws IOException {
         return Byte.toUnsignedInt(readByte());
     }
@@ -333,6 +357,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next short from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final short readShort() throws IOException {
         ensureBufferContains(Short.BYTES);
         return buffer.getShort();
@@ -349,6 +374,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next unsigned short from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final int readUnsignedShort() throws IOException {
         return Short.toUnsignedInt(readShort());
     }
@@ -361,6 +387,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next character from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final char readChar() throws IOException {
         ensureBufferContains(Character.BYTES);
         return buffer.getChar();
@@ -374,6 +401,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next integer from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final int readInt() throws IOException {
         ensureBufferContains(Integer.BYTES);
         return buffer.getInt();
@@ -402,6 +430,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next integer from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final long readLong() throws IOException {
         ensureBufferContains(Long.BYTES);
         return buffer.getLong();
@@ -415,6 +444,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next float from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final float readFloat() throws IOException {
         ensureBufferContains(Float.BYTES);
         return buffer.getFloat();
@@ -428,6 +458,7 @@ public class ChannelDataInput extends ChannelData {
      * @return the value of the next double from the stream.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final double readDouble() throws IOException {
         ensureBufferContains(Double.BYTES);
         return buffer.getDouble();
@@ -542,6 +573,7 @@ public class ChannelDataInput extends ChannelData {
      * @param  dest An array of bytes to be written to.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final void readFully(final byte[] dest) throws IOException {
         readFully(dest, 0, dest.length);
     }
@@ -555,6 +587,7 @@ public class ChannelDataInput extends ChannelData {
      * @param  length  the number of bytes to read.
      * @throws IOException if an error (including EOF) occurred while reading the stream.
      */
+    @Override
     public final void readFully(final byte[] dest, int offset, int length) throws IOException {
         while (length != 0) {
             ensureNonEmpty();
@@ -896,6 +929,74 @@ public class ChannelDataInput extends ChannelData {
         return new String(array, position, length, encoding);
     }
 
+    /**
+     * Reads in a string that has been encoded using a UTF-8 string.
+     *
+     * @return the string reads from the stream.
+     * @throws IOException if an error (including EOF) occurred while reading the stream.
+     */
+    @Override
+    public final String readUTF() throws IOException {
+        final ByteOrder oldOrder = buffer.order();
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        try {
+            return DataInputStream.readUTF(this);
+        } finally {
+            buffer.order(oldOrder);
+        }
+    }
+
+    /**
+     * Reads new bytes until the next EOL. This method can read only US-ASCII strings.
+     * This method is provided for compliance with the {@link DataInput} interface,
+     * but is generally not recommended.
+     *
+     * @return the next line, or {@code null} if the EOF has been reached.
+     * @throws IOException if an error occurred while reading.
+     */
+    @Override
+    public final String readLine() throws IOException {
+        if (!hasRemaining()) {
+            return null;
+        }
+        int c = Byte.toUnsignedInt(buffer.get());
+        StringBuilder line = new StringBuilder();
+        line.append((char) c);
+loop:   while (hasRemaining()) {
+            c = Byte.toUnsignedInt(buffer.get());
+            switch (c) {
+                case '\n': break loop;
+                case '\r': {
+                    if (hasRemaining() && buffer.get() != '\n') {
+                        pushBack();
+                    }
+                    break loop;
+                }
+            }
+            line.append((char) c);
+        }
+        return line.toString();
+    }
+
+    /**
+     * Tries to skip over <var>n</var> bytes of data from the input stream.
+     * This method may skip over some smaller number of bytes, possibly zero.
+     * A negative value move backward in the input stream.
+     *
+     * @param  n  maximal number of bytes to skip. Can be negative.
+     * @return number of bytes actually skipped.
+     * @throws IOException if an error occurred while reading.
+     */
+    @Override
+    public int skipBytes(int n) throws IOException {
+        if (!hasRemaining()) {
+            return 0;
+        }
+        n = Math.min(n, buffer.remaining());
+        buffer.position(buffer.position() + n);
+        return n;
+    }
+
     /**
      * Moves to the given position in the stream. The given position is relative to
      * the position that the stream had at {@code ChannelDataInput} construction time.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
index e39b7cf8ae..04eeaead32 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelImageInputStream.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.io.stream;
 
-import java.io.DataInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -110,73 +109,6 @@ public class ChannelImageInputStream extends ChannelDataInput implements ImageIn
         return buffer.order();
     }
 
-    /**
-     * Reads a byte from the stream and returns a {@code true} if it is nonzero, {@code false} otherwise.
-     * The implementation is as below:
-     *
-     * {@snippet lang="java" :
-     *     return readByte() != 0;
-     *     }
-     *
-     * @return the value of the next boolean from the stream.
-     * @throws IOException if an error (including EOF) occurred while reading the stream.
-     */
-    @Override
-    public final boolean readBoolean() throws IOException {
-        return readByte() != 0;
-    }
-
-    /**
-     * Reads in a string that has been encoded using a UTF-8 string.
-     *
-     * @return the string reads from the stream.
-     * @throws IOException if an error (including EOF) occurred while reading the stream.
-     */
-    @Override
-    public final String readUTF() throws IOException {
-        final ByteOrder oldOrder = buffer.order();
-        buffer.order(ByteOrder.BIG_ENDIAN);
-        try {
-            return DataInputStream.readUTF(this);
-        } finally {
-            buffer.order(oldOrder);
-        }
-    }
-
-    /**
-     * Reads the new bytes until the next EOL. This method can read only US-ASCII strings.
-     * This method is provided for compliance with the {@link java.io.DataInput} interface,
-     * but is generally not recommended.
-     *
-     * @return the next line, or {@code null} if the EOF has been reached.
-     * @throws IOException if an error occurred while reading.
-     */
-    @Override
-    public final String readLine() throws IOException {
-        int c = read();
-        if (c < 0) {
-            return null;
-        }
-        StringBuilder line = new StringBuilder();
-        line.append((char) c);
-loop:   while ((c = read()) >= 0) {
-            switch (c) {
-                case '\r': {
-                    c = read();
-                    if (c >= 0 && c != '\n') {
-                        pushBack();
-                    }
-                    break loop;
-                }
-                case '\n': {
-                    break loop;
-                }
-            }
-            line.append((char) c);
-        }
-        return line.toString();
-    }
-
     /**
      * Returns the next byte from the stream as an unsigned integer between 0 and 255,
      * or -1 if we reached the end of stream.
@@ -259,7 +191,7 @@ loop:   while ((c = read()) >= 0) {
      * But experience shows that various {@code ImageReader} implementations outside Apache SIS
      * expect that we skip exactly the specified amount of bytes and ignore the returned value.
      *
-     * @param  n  maximal number of bytes to skip. Can be negative.
+     * @param  n  number of bytes to skip. Can be negative.
      * @return number of bytes actually skipped.
      * @throws IOException if an error occurred while reading.
      */
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InputStreamAdapter.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InputStreamAdapter.java
index 92907a8c85..81f05db4b8 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InputStreamAdapter.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/InputStreamAdapter.java
@@ -42,8 +42,8 @@ import org.apache.sis.storage.internal.Resources;
 public final class InputStreamAdapter extends InputStream implements Markable {
     /**
      * The underlying data input stream. In principle, public access to this field breaks encapsulation.
-     * But since {@code InputStreamAdapter} does not hold any state and just forwards every method calls
-     * to that {@code ImageInputStream}, using on object or the other does not make a difference.
+     * But since {@code InputStreamAdapter} forwards most method calls to that {@code ImageInputStream},
+     * using an object or the other does not make a difference except for marks and close operations.
      */
     public final ImageInputStream input;
 
@@ -119,6 +119,60 @@ public final class InputStreamAdapter extends InputStream implements Markable {
         return input.read(b, off, len);
     }
 
+    /**
+     * Reads up to a specified number of bytes from the input stream.
+     * This method may read less bytes if the end-of-stream is reached.
+     *
+     * @param  count  number of bytes to read.
+     * @return the bytes read.
+     * @throws IOException if an I/O error occurs.
+     */
+    @Override
+    public byte[] readNBytes(int count) throws IOException {
+        final long length = input.length();
+        if (length < 0) {
+            return super.readNBytes(count);
+        }
+        count = (int) Math.min(count, Math.subtractExact(length, input.getStreamPosition()));
+        final byte[] array = new byte[count];
+        input.readFully(array);
+        return array;
+    }
+
+    /**
+     * Reads up to a specified number of bytes from the input stream.
+     * This method may read less bytes if the end-of-stream is reached.
+     *
+     * @param  array   where to store the bytes.
+     * @param  offset  index if the first element where to store bytes.
+     * @param  count   number of bytes to read.
+     * @return number of bytes actually read.
+     * @throws IOException if an I/O error occurs.
+     */
+    @Override
+    public int readNBytes(final byte[] array, final int offset, int count) throws IOException {
+        final long length = input.length();
+        if (length < 0) {
+            return super.readNBytes(array, offset, count);
+        }
+        count = (int) Math.min(count, Math.subtractExact(length, input.getStreamPosition()));
+        input.readFully(array, offset, count);
+        return count;
+    }
+
+    /**
+     * Skips the specified number of bytes. If the final position is past the end of file,
+     * an {@link java.io.EOFException} will be thrown either by this method or at the next
+     * read operation.
+     *
+     * @param  count  number of bytes to skip.
+     * @throws IOException if an I/O error occurs.
+     */
+//  @Override       // Pending JDK12.
+    public void skipNBytes(final long count) throws IOException {
+        input.seek(Math.addExact(input.getStreamPosition(), count));
+    }
+
     /**
      * Skips over and discards {@code n} bytes of data from this input stream.
      *
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
index 86ee0df6bf..0be3ca9ad4 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java
@@ -213,7 +213,7 @@ public class StorageConnector implements Serializable {
         add(OutputStream.class,      StorageConnector::createOutputStream);
         add(Reader.class,            StorageConnector::createReader);
         add(Connection.class,        StorageConnector::createConnection);
-        add(ChannelDataInput.class,  (s) -> s.createChannelDataInput(false));     // Undocumented case (SIS internal)
+        add(ChannelDataInput.class,  StorageConnector::createChannelDataInput);   // Undocumented case (SIS internal)
         add(ChannelDataOutput.class, StorageConnector::createChannelDataOutput);  // Undocumented case (SIS internal)
         add(ChannelFactory.class,    (s) -> null);                                // Undocumented. Shall not cache.
         /*
@@ -1017,13 +1017,12 @@ public class StorageConnector implements Serializable {
      * This method is one of the {@link #OPENERS} methods and should be invoked at most once per
      * {@code StorageConnector} instance.
      *
-     * @param  asImageInputStream  whether the {@code ChannelDataInput} needs to be {@link ChannelImageInputStream} subclass.
      * @return input channel, or {@code null} if none or if {@linkplain #probing} result has been determined offline.
      * @throws IOException if an error occurred while opening a channel for the input.
      *
      * @see #createChannelDataOutput()
      */
-    private ChannelDataInput createChannelDataInput(final boolean asImageInputStream) throws IOException, DataStoreException {
+    private ChannelDataInput createChannelDataInput() throws IOException, DataStoreException {
         /*
          * Before to try to wrap an InputStream, mark its position so we can rewind if the user asks for
          * the InputStream directly. We need to reset because ChannelDataInput may have read some bytes.
@@ -1065,12 +1064,7 @@ public class StorageConnector implements Serializable {
         final ReadableByteChannel channel = factory.readable(name, null);
         addView(ReadableByteChannel.class, channel, null, factory.isCoupled() ? CASCADE_ON_RESET : 0);
         final ByteBuffer buffer = getChannelBuffer(factory);
-        final ChannelDataInput asDataInput;
-        if (asImageInputStream) {
-            asDataInput = new ChannelImageInputStream(name, channel, buffer, false);
-        } else {
-            asDataInput = new ChannelDataInput(name, channel, buffer, false);
-        }
+        final ChannelDataInput asDataInput = new ChannelDataInput(name, channel, buffer, false);
         addView(ChannelDataInput.class, asDataInput, ReadableByteChannel.class, CASCADE_ON_RESET);
         /*
          * Following is an undocumented mechanism for allowing some Apache SIS implementations of DataStore
@@ -1107,20 +1101,15 @@ public class StorageConnector implements Serializable {
         if (reset(c)) {
             in = (ChannelDataInput) c.view;
         } else {
-            in = createChannelDataInput(true);                      // May be null.
+            in = createChannelDataInput();          // May be null. This method should not have been invoked before.
         }
         final DataInput asDataInput;
         if (in != null) {
-            c = getView(ChannelDataInput.class);                    // May have been added by createChannelDataInput(…).
-            if (in instanceof DataInput) {
-                asDataInput = (DataInput) in;
-            } else {
-                asDataInput = new ChannelImageInputStream(in);      // Upgrade existing instance.
-                c.view = asDataInput;
-            }
-            views.put(DataInput.class, c);                          // Share the same Coupled instance.
+            asDataInput = in;
+            c = getView(ChannelDataInput.class);    // Refresh because may have been added by createChannelDataInput().
+            views.put(DataInput.class, c);          // Share the same `Coupled` instance.
         } else if (wasProbingAbsentFile()) {
-            return null;                                            // Do not cache, for allowing file creation later.
+            return null;                            // Do not cache, for allowing file creation later.
         } else {
             reset();
             try {
@@ -1195,8 +1184,8 @@ public class StorageConnector implements Serializable {
             /*
              * If no ChannelDataInput has been created by the above code, get the input as an ImageInputStream and
              * read an arbitrary number of bytes. Read only a small amount of bytes because, at the contrary of the
-             * buffer created in `createChannelDataInput(boolean)`, the buffer created here is unlikely to be used
-             * for the reading process after the recognition of the file format.
+             * buffer created in `createChannelDataInput()`, the buffer created here is unlikely to be used for the
+             * reading process after the recognition of the file format.
              */
             final ImageInputStream in = getStorageAs(ImageInputStream.class);
             if (in != null) {
@@ -1271,31 +1260,46 @@ public class StorageConnector implements Serializable {
     }
 
     /**
-     * Creates an {@link ImageInputStream} from the {@link DataInput} if possible. This method simply
-     * casts {@code DataInput} if such cast is allowed. Since {@link #createDataInput()} instantiates
-     * {@link ChannelImageInputStream}, this cast is usually possible.
+     * Creates an {@link ImageInputStream} from the {@link DataInput} if possible. This method casts
+     * {@code DataInput} if such cast is allowed, or upgrades {@link ChannelDataInput} implementation.
      *
      * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per
      * {@code StorageConnector} instance.</p>
      *
      * @return input stream, or {@code null} if none or if {@linkplain #probing} result has been determined offline.
      */
-    private ImageInputStream createImageInputStream() throws DataStoreException {
-        final Class<DataInput> source = DataInput.class;
-        final DataInput input = getStorageAs(source);
+    private ImageInputStream createImageInputStream() throws IOException, DataStoreException {
+        final Coupled c;
+        final ImageInputStream asDataInput;
+        DataInput input = getStorageAs(DataInput.class);
         if (input instanceof ImageInputStream) {
-            views.put(ImageInputStream.class, views.get(source));               // Share the same Coupled instance.
-            return (ImageInputStream) input;
-        } else if (!wasProbingAbsentFile()) {
-            /*
-             * We do not invoke `ImageIO.createImageInputStream(Object)` because we do not know
-             * how the stream will use the `storage` object. It may read in advance some bytes,
-             * which can invalidate the storage for use outside the `ImageInputStream`. Instead
-             * creating image input/output streams is left to caller's responsibility.
-             */
-            addView(ImageInputStream.class, null);                              // Remember that there is no view.
+            asDataInput = (ImageInputStream) input;
+            c = views.get(DataInput.class);
+        } else {
+            input = getStorageAs(ChannelDataInput.class);
+            if (input != null)  {
+                c = getView(ChannelDataInput.class);
+                if (input instanceof ImageInputStream) {
+                    asDataInput = (ImageInputStream) input;
+                } else {
+                    asDataInput = new ChannelImageInputStream((ChannelDataInput) input);
+                    c.view = asDataInput;   // Upgrade existing instance for all views.
+                }
+            } else {
+                if (!wasProbingAbsentFile()) {
+                    /*
+                     * We do not invoke `ImageIO.createImageInputStream(Object)` because we do not know
+                     * how the stream will use the `storage` object. It may read in advance some bytes,
+                     * which can invalidate the storage for use outside the `ImageInputStream`. Instead
+                     * creating image input/output streams is left to caller's responsibility.
+                     */
+                    addView(ImageInputStream.class, null);          // Remember that there is no view.
+                }
+                return null;
+            }
         }
-        return null;
+        views.put(ImageInputStream.class, c);       // Share the same `Coupled` instance.
+        return asDataInput;
     }
 
     /**
@@ -1310,18 +1314,19 @@ public class StorageConnector implements Serializable {
      * @see #createOutputStream()
      */
     private InputStream createInputStream() throws IOException, DataStoreException {
-        final Class<DataInput> source = DataInput.class;
-        final DataInput input = getStorageAs(source);
-        if (input instanceof InputStream) {
-            views.put(InputStream.class, views.get(source));                    // Share the same Coupled instance.
-            return (InputStream) input;
-        } else if (input instanceof ImageInputStream) {
+        final Class<ImageInputStream> source = ImageInputStream.class;
+        final ImageInputStream input = getStorageAs(source);
+        if (input != null) {
+            if (input instanceof InputStream) {
+                views.put(InputStream.class, views.get(source));        // Share the same `Coupled` instance.
+                return (InputStream) input;
+            }
             /*
              * Wrap the ImageInputStream as an ordinary InputStream. We avoid setting CASCADE_ON_RESET (unless
              * reset() needs to propagate further than ImageInputStream) because changes in InputStreamAdapter
              * position are immediately reflected by corresponding changes in ImageInputStream position.
              */
-            final InputStream in = new InputStreamAdapter((ImageInputStream) input);
+            final InputStream in = new InputStreamAdapter(input);
             addView(InputStream.class, in, source, (byte) (getView(source).cascade & CASCADE_ON_RESET));
             return in;
         } else if (!wasProbingAbsentFile()) {
@@ -1418,7 +1423,7 @@ public class StorageConnector implements Serializable {
      * @return output channel, or {@code null} if none.
      * @throws IOException if an error occurred while opening a channel for the output.
      *
-     * @see #createChannelDataInput(boolean)
+     * @see #createChannelDataInput()
      */
     private ChannelDataOutput createChannelDataOutput() throws IOException, DataStoreException {
         /*
@@ -1517,7 +1522,7 @@ public class StorageConnector implements Serializable {
         final Class<DataOutput> target = DataOutput.class;
         final DataOutput output = getStorageAs(target);
         if (output instanceof OutputStream) {
-            views.put(OutputStream.class, views.get(target));                   // Share the same Coupled instance.
+            views.put(OutputStream.class, views.get(target));                   // Share the same `Coupled` instance.
             return (OutputStream) output;
         } else {
             addView(OutputStream.class, null);                                  // Remember that there is no view.
diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/StorageConnectorTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/StorageConnectorTest.java
index 2f010e4a61..bed05efd43 100644
--- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/StorageConnectorTest.java
+++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/StorageConnectorTest.java
@@ -201,13 +201,13 @@ public final class StorageConnectorTest extends TestCase {
         assertEquals(asStream, connector.getStorageAs(Path.class) == null);
         final DataInput input = connector.getStorageAs(DataInput.class);
         assertSame("Value shall be cached.", input, connector.getStorageAs(DataInput.class));
-        assertInstanceOf("Needs the SIS implementation.", ChannelImageInputStream.class, input);
+        assertInstanceOf("Needs the SIS implementation.", ChannelDataInput.class, input);
         assertSame("Instance shall be shared.", input, connector.getStorageAs(ChannelDataInput.class));
         /*
          * Reads a single integer for checking that the stream is at the right position, then close the stream.
          * Since the file is a compiled Java class, the integer that we read shall be the Java magic number.
          */
-        final ReadableByteChannel channel = ((ChannelImageInputStream) input).channel;
+        final ReadableByteChannel channel = ((ChannelDataInput) input).channel;
         assertTrue("channel.isOpen()", channel.isOpen());
         assertEquals("First 4 bytes", MAGIC_NUMBER, input.readInt());
         connector.closeAllExcept(null);
@@ -343,7 +343,7 @@ public final class StorageConnectorTest extends TestCase {
         /*
          * Get as an image input stream and ensure that the cached value has been replaced.
          */
-        final DataInput stream = connector.getStorageAs(DataInput.class);
+        final ImageInputStream stream = connector.getStorageAs(ImageInputStream.class);
         assertInstanceOf("Needs the SIS implementation", ChannelImageInputStream.class, stream);
         assertNotSame("Expected a new instance.", input, stream);
         assertSame("Shall share the channel.", input.channel, ((ChannelDataInput) stream).channel);
@@ -439,8 +439,8 @@ public final class StorageConnectorTest extends TestCase {
     @DependsOnMethod("testGetAsDataInputFromStream")
     public void testCloseAllExcept() throws DataStoreException, IOException {
         final StorageConnector connector = create(true);
-        final DataInput input = connector.getStorageAs(DataInput.class);
-        final ReadableByteChannel channel = ((ChannelImageInputStream) input).channel;
+        final ChannelDataInput input = connector.getStorageAs(ChannelDataInput.class);
+        final ReadableByteChannel channel = input.channel;
         assertTrue("channel.isOpen()", channel.isOpen());
         connector.closeAllExcept(input);
         assertTrue("channel.isOpen()", channel.isOpen());