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/25 03:04:09 UTC

[sis] 01/03: Redesign the way that readers and writers co-exist in `DataStore` implementations: - `isWritable(…)` needs to distinguish between opening an existing file or creating a new one. - `setStreamPosition(long)` removed. It was misused in most places, causing probable bugs. - Allow creation of `ChannelDataOutput` from a `ChannelDataInput`, sharing same internal. - Add `synchronize(…)` for making input `ChannelData` consistent with output, or conversely.

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 3762fd80fe3177d3070e0b601a6387fc85e6b78d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Oct 22 15:34:50 2023 +0200

    Redesign the way that readers and writers co-exist in `DataStore` implementations:
    - `isWritable(…)` needs to distinguish between opening an existing file or creating a new one.
    - `setStreamPosition(long)` removed. It was misused in most places, causing probable bugs.
    - Allow creation of `ChannelDataOutput` from a `ChannelDataInput`, sharing same internal.
    - Add `synchronize(…)` for making input `ChannelData` consistent with output, or conversely.
---
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   2 +-
 .../sis/storage/geotiff/ReversedBitsChannel.java   |  19 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |   2 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |   3 +-
 .../org/apache/sis/storage/gpx/StoreProvider.java  |   2 +-
 .../main/org/apache/sis/io/stream/ChannelData.java | 235 +++++++++++++++------
 .../org/apache/sis/io/stream/ChannelDataInput.java |  86 +++++---
 .../apache/sis/io/stream/ChannelDataOutput.java    | 191 ++++++++++++-----
 .../main/org/apache/sis/io/stream/Markable.java    |   2 +-
 .../org/apache/sis/io/stream/UpdatableWrite.java   |  39 +++-
 .../org/apache/sis/storage/StorageConnector.java   |  25 ++-
 .../org/apache/sis/storage/base/URIDataStore.java  |  26 ++-
 .../sis/storage/esri/AsciiGridStoreProvider.java   |   2 +-
 .../org/apache/sis/storage/image/FormatFinder.java |   2 +-
 .../storage/internal/WritableResourceSupport.java  |   1 -
 .../main/org/apache/sis/storage/package-info.java  |   2 +-
 .../io/stream/ChannelImageOutputStreamTest.java    |   2 +-
 17 files changed, 457 insertions(+), 184 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index cd0d520035..4b8fa1f835 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -224,7 +224,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
         location   = connector.getStorageAs(URI.class);
         path       = connector.getStorageAs(Path.class);
         try {
-            if (URIDataStore.Provider.isWritable(connector)) {
+            if (URIDataStore.Provider.isWritable(connector, true)) {
                 ChannelDataOutput output = connector.commit(ChannelDataOutput.class, Constants.GEOTIFF);
                 writer = new Writer(this, output, connector.getOption(GeoTiffOption.OPTION_KEY));
             } else {
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
index 394d10adf0..8d181cb727 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
@@ -63,11 +63,10 @@ final class ReversedBitsChannel implements ReadableByteChannel, SeekableByteChan
      * and because a new buffer is created for each strip or tile to read.
      */
     static ChannelDataInput wrap(final ChannelDataInput input) throws IOException {
-        final ChannelDataInput output = new ChannelDataInput(
-                input.filename, new ReversedBitsChannel(input),
-                ByteBuffer.allocate(2048).order(input.buffer.order()).limit(0), true);
-        output.setStreamPosition(input.getStreamPosition());
-        return output;
+        final var buffer  = ByteBuffer.allocate(2048).order(input.buffer.order());
+        final var reverse = new ChannelDataInput(input.filename, new ReversedBitsChannel(input), buffer, true);
+        input.yield(reverse);
+        return reverse;
     }
 
     /**
@@ -75,11 +74,9 @@ final class ReversedBitsChannel implements ReadableByteChannel, SeekableByteChan
      */
     @Override
     public long size() throws IOException {
-        if (input.channel instanceof SeekableByteChannel) {
-            return ((SeekableByteChannel) input.channel).size();
-        } else {
-            throw unsupported("size");
-        }
+        final long size = input.length();
+        if (size >= 0) return size;
+        throw unsupported("size");
     }
 
     /**
@@ -95,7 +92,7 @@ final class ReversedBitsChannel implements ReadableByteChannel, SeekableByteChan
      */
     @Override
     public SeekableByteChannel position(final long p) throws IOException {
-        input.setStreamPosition(p);
+        input.seek(p);
         return this;
     }
 
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 360504e823..4331f9a5d4 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -172,7 +172,7 @@ final class Writer extends GeoTIFF implements Flushable {
          * Write the TIFF file header before first IFD. Stream position matter and must start at zero.
          * Note that it does not necessarily mean that the stream has no bytes before current position.
          */
-        output.setStreamPosition(0);                          // Not a seek, only setting the counter.
+        output.relocateOrigin();
         output.writeShort(ByteOrder.LITTLE_ENDIAN.equals(output.buffer.order()) ? LITTLE_ENDIAN : BIG_ENDIAN);
         output.writeShort(isBigTIFF ? BIG_TIFF : CLASSIC);
         if (isBigTIFF) {
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
index 308ade26c4..3adadb067f 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
@@ -265,8 +265,7 @@ public abstract class Inflater implements Closeable {
          */
         if (input.channel instanceof PixelChannel) {
             ((PixelChannel) input.channel).setInputRegion(start, byteCount);
-            input.buffer.limit(0);
-            input.setStreamPosition(start);         // Must be after above call to `limit(0)`.
+            input.refresh(start);
         }
     }
 
diff --git a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
index 52a176f91f..48e63bbf5b 100644
--- a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
+++ b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/gpx/StoreProvider.java
@@ -99,7 +99,7 @@ public final class StoreProvider extends StaxDataStoreProvider {
      */
     @Override
     public DataStore open(final StorageConnector connector) throws DataStoreException {
-        if (isWritable(connector)) {
+        if (isWritable(connector, false)) {
             return new WritableStore(this, connector);
         } else {
             return new Store(this, connector);
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
index 6df0d552ec..3e67483dc9 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelData.java
@@ -62,8 +62,10 @@ public abstract class ChannelData implements Markable {
      *
      * <p>This value is added to the argument given to the {@link #seek(long)} method. Users can ignore
      * this field, unless they want to invoke {@link SeekableByteChannel#position(long)} directly.</p>
+     *
+     * @see #toSeekableByteChannelPosition(long)
      */
-    public final long channelOffset;
+    private long channelOffset;
 
     /**
      * The channel position where is located the {@link #buffer} value at index 0.
@@ -117,25 +119,31 @@ public abstract class ChannelData implements Markable {
      * @throws IOException if an error occurred while reading the channel.
      */
     ChannelData(final String filename, final Channel channel, final ByteBuffer buffer) throws IOException {
-        this.filename      = filename;
-        this.buffer        = buffer;
-        this.channelOffset = (channel instanceof SeekableByteChannel) ? ((SeekableByteChannel) channel).position() : 0;
+        this.filename = filename;
+        this.buffer   = buffer;
+        if (channel instanceof SeekableByteChannel) {
+            channelOffset = ((SeekableByteChannel) channel).position();
+        }
     }
 
     /**
      * Creates a new instance from the given {@code ChannelData}.
      * This constructor is invoked when we need to change the implementation class.
-     * The old {@code ChannelData} should not be used anymore after this constructor.
+     * If {@code replacing} is {@code true}, then the old {@code ChannelData} should
+     * not be used anymore after this constructor.
      *
-     * @param  old  the existing instance from which to takes the channel and buffer.
+     * @param  other      the existing instance from which to takes the channel and buffer.
+     * @param  replacing  {@code true} if {@code other} will be discarded in favor of the new instance.
      */
-    ChannelData(final ChannelData old) {
-        filename      = old.filename;
-        buffer        = old.buffer;
-        channelOffset = old.channelOffset;
-        bufferOffset  = old.bufferOffset;
-        bitPosition   = old.bitPosition;
-        mark          = old.mark;
+    ChannelData(final ChannelData other, final boolean replacing) {
+        filename      = other.filename;
+        buffer        = other.buffer;
+        channelOffset = other.channelOffset;
+        bufferOffset  = other.bufferOffset;
+        bitPosition   = other.bitPosition;
+        if (replacing) {
+            mark = other.mark;
+        }
     }
 
     /**
@@ -150,7 +158,35 @@ public abstract class ChannelData implements Markable {
         this.filename = filename;
         buffer = data.asReadOnlyBuffer();
         buffer.order(data.order());
-        channelOffset = 0;
+    }
+
+    /**
+     * {@return the wrapped channel where data are read or written}.
+     * This is the {@code channel} field of the {@code ChannelData} subclass.
+     *
+     * @see ChannelDataInput#channel
+     * @see ChannelDataOutput#channel
+     */
+    public abstract Channel channel();
+
+    /**
+     * Returns the length of the stream (in bytes), or -1 if unknown.
+     * The length is relative to the position during the last call to {@link #relocateOrigin()}.
+     * If the latter method has never been invoked, then the length is relative to the channel
+     * position at {@code ChannelData} construction time.
+     *
+     * @return the length of the stream (in bytes) relative to origin, or -1 if unknown.
+     * @throws IOException if an error occurred while fetching the stream length.
+     */
+    public final long length() throws IOException {     // Method signature must match ImageInputStream.length().
+        final Channel channel = channel();
+        if (channel instanceof SeekableByteChannel) {
+            final long length = Math.subtractExact(((SeekableByteChannel) channel).size(), channelOffset);
+            if (length >= 0) {
+                return Math.max(length, Math.addExact(bufferOffset, buffer.limit()));
+            }
+        }
+        return -1;
     }
 
     /**
@@ -220,47 +256,25 @@ public abstract class ChannelData implements Markable {
 
     /**
      * Returns the current byte position of the stream.
-     * The returned value is relative to the position that the stream had at {@code ChannelData} construction time.
+     * The returned value is relative to the position during the last call to {@link #relocateOrigin()}.
+     * If the latter method has never been invoked, then the returned value is relative to the channel
+     * position at {@code ChannelData} construction time.
      *
      * @return the position of the stream.
      */
     @Override
-    public long getStreamPosition() {
-        return position();
-    }
+    public abstract long getStreamPosition();
 
     /**
      * Returns the current byte position of the stream, ignoring overriding by subclasses.
-     * The returned value is relative to the position that the stream had at {@code ChannelData} construction time.
+     * The returned value is relative to the position during the last call to {@link #relocateOrigin()}.
+     * If the latter method has never been invoked, then the returned value is relative to the channel
+     * position at {@code ChannelData} construction time.
      */
-    private long position() {
+    final long position() {
         return Math.addExact(bufferOffset, buffer.position());
     }
 
-    /**
-     * Sets the current byte position of the stream. This method does <strong>not</strong> seeks the stream;
-     * this method only modifies the value to be returned by {@link #getStreamPosition()}. This method can
-     * be invoked when some external code has performed some work with the channel and wants to inform this
-     * {@code ChannelData} about the new position resulting from this work.
-     *
-     * <b>Notes:</b>
-     * <ul>
-     *   <li>Invoking this method clears the {@linkplain #getBitOffset() bit offset}
-     *       and the {@linkplain #mark() marks}.</li>
-     *   <li>This method does not need to be invoked when only the {@linkplain ByteBuffer#position() buffer position}
-     *       has changed.</li>
-     * </ul>
-     *
-     * @param position the new position of the stream.
-     */
-    public final void setStreamPosition(final long position) {
-        bufferOffset = Math.subtractExact(position, buffer.position());
-        // Clearing the bit offset is needed if we don't want to handle the case of ChannelDataOutput,
-        // which use a different stream position calculation when the bit offset is non-zero.
-        clearBitOffset();
-        mark = null;
-    }
-
     /**
      * Returns the earliest position in the stream to which {@linkplain #seek(long) seeking} may be performed.
      *
@@ -291,11 +305,10 @@ public abstract class ChannelData implements Markable {
         if (buffer.isReadOnly()) {
             return;
         }
-        final int n = (int) Math.max(position - bufferOffset, 0);
-        final int p = buffer.position() - n;
-        final int r = buffer.limit() - n;
-        flushAndSetPosition(n);                             // Number of bytes to forget.
-        buffer.compact().position(p).limit(r);
+        final long count = Math.subtractExact(position, bufferOffset);
+        if (count > 0) {
+            flushNBytes((int) Math.min(count, buffer.limit()));
+        }
         /*
          * Discard trailing obsolete marks. Note that obsolete marks between valid marks
          * cannot be discarded - only the trailing obsolete marks can be removed.
@@ -314,18 +327,20 @@ public abstract class ChannelData implements Markable {
     }
 
     /**
-     * Writes (if applicable) the buffer content up to the given position, then sets the buffer position
-     * to the given value. The {@linkplain ByteBuffer#limit() buffer limit} is unchanged, and the buffer
-     * offset is incremented by the given value.
+     * Flushes the given number of bytes in the buffer.
+     * This is invoked for making room for more bytes.
+     * If the given count is larger than the buffer content, then this method flushes everything.
+     * If the given count is zero or negative, then this method does nothing.
+     *
+     * @param  count  number of bytes to write, between 1 and buffer limit.
+     * @throws IOException if an error occurred while writing the bytes to the channel.
      */
-    void flushAndSetPosition(final int position) throws IOException {
-        buffer.position(position);
-        bufferOffset += position;
-    }
+    abstract void flushNBytes(final int count) throws IOException;
 
     /**
-     * Moves to the given position in the stream. The given position is relative to
-     * the position that the stream had at {@code ChannelData} construction time.
+     * Moves to the given position in the stream. The given position is relative to the position during
+     * the last call to {@link #relocateOrigin()}. If the latter method has never been invoked, then the
+     * argument is relative to the channel position at {@code ChannelData} construction time.
      *
      * @param  position  the position where to move.
      * @throws IOException if the stream cannot be moved to the given position.
@@ -339,7 +354,7 @@ public abstract class ChannelData implements Markable {
      */
     @Override
     public final void mark() {
-        mark = new Mark(getStreamPosition(), (byte) getBitOffset(), mark);
+        mark = new Mark(position(), (byte) getBitOffset(), mark);
     }
 
     /**
@@ -388,6 +403,108 @@ public abstract class ChannelData implements Markable {
         }
     }
 
+    /**
+     * Empties the buffer and sets the channel position to the beginning of this stream (the origin).
+     * This method is similar to {@code seek(0)} except that the buffer content and all marks are discarded,
+     * and that this method returns {@code false} instead of throwing an exception if the channel is not seekable.
+     *
+     * @return {@code true} on success, or {@code false} if it is not possible to reset the position.
+     * @throws IOException if an error occurred while setting the channel position.
+     */
+    public final boolean rewind() throws IOException {
+        final Channel channel = channel();
+        if (channel instanceof SeekableByteChannel) {
+            ((SeekableByteChannel) channel).position(channelOffset);
+            buffer.clear().limit(0);
+            bufferOffset = 0;
+            clearBitOffset();
+            mark = null;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Notifies two {@code ChannelData} instances that operations will continue with the specified take over.
+     * The two {@code ChannelData} instances should share the same {@link Channel}, or use two channels that
+     * are at the same {@linkplain SeekableByteChannel#position() channel position}.
+     *
+     * <h4>Usage</h4>
+     * This method is used when a {@link ChannelDataInput} and a {@link ChannelDataOutput} are wrapping
+     * the same {@link java.nio.channels.ByteChannel} and used alternatively for reading and writing.
+     * After a read operation, {@code in.yield(put)} should be invoked for ensuring that the output
+     * position is valid for the new channel position.
+     *
+     * <h4>Implementation note</h4>
+     * Subclasses <strong>must</strong> override this method and set the buffer position to zero.
+     * Whether this need to be done before or after {@code super.field(takeOver)} depends on whether
+     * this {@code ChannelData} is for input or output.
+     *
+     * @param  takeOver  the {@code ChannelData} which will continue operations after this one.
+     *
+     * @see ChannelDataOutput#ChannelDataOutput(ChannelDataInput)
+     */
+    public void yield(final ChannelData takeOver) throws IOException {
+        takeOver.bufferOffset  = bufferOffset;
+        takeOver.channelOffset = channelOffset;
+        takeOver.bitPosition   = bitPosition;
+    }
+
+    /**
+     * Invalidates the buffer content and updates the value reported as the stream position.
+     * This method is not a {@linkplain #seek(long) seek},
+     * i.e. it does not change the {@linkplain #channel() channel} position,
+     * This method only modifies the value returned by the {@link #getStreamPosition()} method.
+     * This {@code refresh(long)} method can be invoked when external code has performed some work
+     * directly on the {@linkplain #channel() channel} and wants to inform this {@code ChannelData}
+     * about the new position resulting from this work.
+     *
+     * <b>Notes:</b>
+     * <ul>
+     *   <li>Invoking this method clears the {@linkplain #getBitOffset() bit offset} and {@linkplain #mark() marks}.</li>
+     *   <li>Invoking this method sets the {@linkplain #buffer} {@linkplain ByteBuffer#limit() limit} to zero.</li>
+     *   <li>This method does not need to be invoked when only the {@linkplain ByteBuffer#position() buffer position}
+     *       has changed.</li>
+     * </ul>
+     *
+     * @param  position  the new position of the stream.
+     */
+    public final void refresh(final long position) {
+        buffer.limit(0);
+        bufferOffset = position;
+        /*
+         * Clearing the bit offset is needed if we don't want to handle the case of `ChannelDataOutput`,
+         * which use a different stream position calculation when the bit offset is non-zero.
+         */
+        clearBitOffset();
+        mark = null;
+    }
+
+    /**
+     * Sets the current position as the new origin of this {@code ChannelData}.
+     * After this method call, {@link #getStreamPosition()} will return zero when
+     * {@code ChannelData} is about to read the byte located at the current position.
+     *
+     * <p>Note that invoking this method may change the value returned by {@link #length()},
+     * because the length is relative to the origin.</p>
+     */
+    public final void relocateOrigin() {
+        final long position = getStreamPosition();
+        channelOffset = toSeekableByteChannelPosition(position);
+        bufferOffset  = Math.subtractExact(bufferOffset, position);
+    }
+
+    /**
+     * Converts a position in this {@code ChannelData} to position in the Java NIO channel.
+     * This is often the same value, but not necessarily.
+     *
+     * @param  position  position in this {@code ChannelData}.
+     * @return Corresponding position in the {@code SeekableByteChannel}.
+     */
+    final long toSeekableByteChannelPosition(final long position) {
+        return Math.addExact(channelOffset, position);
+    }
+
     /**
      * Invoked when an operation between the channel and the buffer transferred no byte. Note that this is unrelated
      * to end-of-file, in which case {@link java.nio.channels.ReadableByteChannel#read(ByteBuffer)} returns -1.
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 a7aa272888..7480b27156 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
@@ -30,6 +30,7 @@ import java.nio.LongBuffer;
 import java.nio.FloatBuffer;
 import java.nio.DoubleBuffer;
 import java.nio.charset.Charset;
+import java.nio.channels.Channel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.SeekableByteChannel;
 import org.apache.sis.storage.internal.Resources;
@@ -127,22 +128,19 @@ public class ChannelDataInput extends ChannelData implements DataInput {
      * @param  input  the existing instance from which to takes the channel and buffer.
      */
     ChannelDataInput(final ChannelDataInput input) {
-        super(input);
+        super(input, true);
         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}.
+     * {@return the wrapped channel where data are read}.
+     * This is the {@link #channel} field value.
      *
-     * @return the length of the stream (in bytes) relative to {@link #channelOffset}, or -1 if unknown.
-     * @throws IOException if an error occurred while fetching the stream length.
+     * @see #channel
      */
-    public final long length() throws IOException {     // Method signature must match ImageInputStream.length().
-        if (channel instanceof SeekableByteChannel) {
-            return Math.subtractExact(((SeekableByteChannel) channel).size(), channelOffset);
-        }
-        return -1;
+    @Override
+    public final Channel channel() {
+        return channel;
     }
 
     /**
@@ -236,6 +234,16 @@ public class ChannelDataInput extends ChannelData implements DataInput {
         }
     }
 
+    /**
+     * Returns the current byte position of the stream.
+     *
+     * @return the position of the stream.
+     */
+    @Override
+    public final long getStreamPosition() {
+        return position();
+    }
+
     /**
      * Returns the "end of file" error message, for {@link EOFException} creations.
      */
@@ -998,8 +1006,7 @@ loop:   while (hasRemaining()) {
     }
 
     /**
-     * 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.
+     * Moves to the given position in this stream.
      *
      * @param  position  the position where to move.
      * @throws IOException if the stream cannot be moved to the given position.
@@ -1017,7 +1024,7 @@ loop:   while (hasRemaining()) {
              * Requested position is outside the current limits of the buffer,
              * but we can set the new position directly in the channel.
              */
-            ((SeekableByteChannel) channel).position(Math.addExact(channelOffset, position));
+            ((SeekableByteChannel) channel).position(toSeekableByteChannelPosition(position));
             bufferOffset = position;
             buffer.clear().limit(0);
         } else if (p >= 0) {
@@ -1058,27 +1065,52 @@ loop:   while (hasRemaining()) {
      */
     public final void rangeOfInterest(long lower, long upper) {
         if (channel instanceof ByteRangeChannel) {
-            lower = Math.addExact(lower, channelOffset);
-            upper = Math.addExact(upper, channelOffset);
+            lower = toSeekableByteChannelPosition(lower);
+            upper = toSeekableByteChannelPosition(upper);
             ((ByteRangeChannel) channel).rangeOfInterest(lower, upper);
         }
     }
 
     /**
-     * Empties the buffer and reset the channel position at the beginning of the stream.
-     * This method is similar to {@code seek(0)} except that the buffer content is discarded.
+     * Forgets the given number of bytes in the buffer.
+     * This is invoked for making room for more bytes.
      *
-     * @return {@code true} on success, or {@code false} if it is not possible to reset the position.
-     * @throws IOException if the stream cannot be moved to the original position.
+     * @param  count  number of bytes to forget, between 1 and buffer limit.
      */
-    public final boolean rewind() throws IOException {
-        if (channel instanceof SeekableByteChannel) {
-            ((SeekableByteChannel) channel).position(channelOffset);
-            buffer.clear().limit(0);
-            bufferOffset = 0;
-            clearBitOffset();
-            return true;
+    @Override
+    final void flushNBytes(final int count) throws IOException {
+        final int p = buffer.position();
+        buffer.position(count).compact()
+              .limit(buffer.position())     // Not the same value as `p`. It is rather equal to `limit - count`.
+              .position(p - count);
+        bufferOffset = Math.addExact(bufferOffset, count);
+    }
+
+    /**
+     * Notifies two {@code ChannelData} instances that operations will continue with the specified take over.
+     * This method should be invoked when read operations with this {@code ChannelDataInput} are completed for
+     * now, and write operations are about to begin with a {@link ChannelDataOutput} sharing the same channel.
+     *
+     * @param  takeOver  the {@link ChannelDataOutput} which will continue operations after this instance.
+     */
+    @Override
+    public void yield(final ChannelData takeOver) throws IOException {
+        /*
+         * If we filled the buffer with more bytes than the buffer position,
+         * the channel position is too far ahead. We need to seek backward.
+         */
+        if (buffer.hasRemaining()) {
+            if (channel instanceof SeekableByteChannel) {
+                ((SeekableByteChannel) channel).position(toSeekableByteChannelPosition(position()));
+            } else {
+                throw new IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, takeOver.filename));
+            }
+        }
+        clearBitOffset();
+        bufferOffset = position();
+        if (buffer.limit(0) != takeOver.buffer) {          // Must be after `position()`.
+            takeOver.buffer.limit(0);
         }
-        return false;
+        super.yield(takeOver);
     }
 }
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
index ccf2e6701e..419410c591 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/ChannelDataOutput.java
@@ -22,13 +22,13 @@ import java.io.Flushable;
 import java.io.IOException;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
 import java.nio.CharBuffer;
 import java.nio.DoubleBuffer;
 import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
 import java.nio.LongBuffer;
 import java.nio.ShortBuffer;
+import java.nio.channels.Channel;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import java.nio.charset.StandardCharsets;
@@ -53,6 +53,11 @@ 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>Interpretation of buffer position and limit</h2>
+ * The buffer position is the position where to write the next byte.
+ * It may be either a new byte appended to the channel, or byte overwriting an existing byte.
+ * Those two case are differentiated by the buffer limit, which is the number of valid bytes in the buffer.
+ *
  * <h2>Relationship with {@code ImageOutputStream}</h2>
  * This class API is compatibly with the {@link javax.imageio.stream.ImageOutputStream} interface, so subclasses
  * can implement that interface if they wish. This class does not implement {@code ImageOutputStream} because it
@@ -84,20 +89,65 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
         buffer.limit(0);
     }
 
+    /**
+     * Creates a new data output which will write in the same channel than the given input.
+     * The new instance will share the same channel and buffer than the given {@code input}.
+     * Callers should not use the two {@code ChannelData} in same time for avoiding chaos.
+     * Bytes will be written starting at the current position of the given input.
+     *
+     * <p>Callers <strong>must</strong> invoke {@link ChannelDataInput#yield(ChannelData)}
+     * before the first use of this output. Example:</p>
+     *
+     * {@snippet lang="java":
+     *     ChannelDataInput  input  = ...;
+     *     ChannelDataOutput output = new ChannelDataOutput(input);
+     *     input.yield(output)
+     *     // ...some writing to `output` here...
+     *     output.yield(input);
+     *     // ...some reading from `input` here...
+     *     input.yield(output)
+     *     // ...some writing to `output` here...
+     * }
+     *
+     * @param  input  the input to make writable.
+     * @throws ClassCastException if the given input is not writable.
+     *
+     * @see #flush()
+     * @see #yield(ChannelData)
+     */
+    public ChannelDataOutput(final ChannelDataInput input) {
+        super(input, false);
+        channel = (WritableByteChannel) input.channel;      // `ClassCastException` is part of the contract.
+        // Do not invoke `synchronized(input)` because caller may want to do some more read operations first.
+    }
+
+    /**
+     * {@return the wrapped channel where data are written}.
+     * This is the {@link #channel} field value.
+     *
+     * @see #channel
+     */
+    @Override
+    public final Channel channel() {
+        return channel;
+    }
+
     /**
      * Makes sure that the buffer can accept at least <var>n</var> more bytes.
      * It is caller's responsibility to ensure that the given number of bytes is
      * not greater than the {@linkplain ByteBuffer#capacity() buffer capacity}.
      *
      * <p>After this method call, the buffer {@linkplain ByteBuffer#limit() limit}
-     * will be equal or greater than {@code position + n}.</p>
+     * will be equal or greater than {@code position + n}. This limit is the number
+     * of valid bytes in the buffer, i.e. bytes that already exist in the channel.
+     * If the caller is appending new bytes and does not use all the space specified
+     * to this method, then the caller should adjust the limit after writing.</p>
      *
      * @param  n  the minimal number of additional bytes that the {@linkplain #buffer buffer} shall accept.
      * @throws IOException if an error occurred while writing to the channel.
      */
     public final void ensureBufferAccepts(final int n) throws IOException {
-        final int capacity = buffer.capacity();
-        assert n >= 0 && n <= capacity : n;
+        assert n >= 0 && n <= buffer.capacity() : n;
         int after = buffer.position() + n;
         if (after > buffer.limit()) {
             /*
@@ -105,7 +155,7 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
              * of valid bytes in the buffer. If the new limit would exceed the buffer capacity, then we
              * need to write some bytes now.
              */
-            if (after > capacity) {
+            if (after > buffer.capacity()) {
                 buffer.flip();
                 do {
                     final int c = channel.write(buffer);
@@ -113,14 +163,14 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
                         onEmptyTransfer();
                     }
                     after -= c;
-                } while (after > capacity);
+                } while (after > buffer.capacity());
                 /*
                  * We wrote a sufficient amount of bytes - usually all of them, but not necessarily.
                  * If there is some unwritten bytes, move them the beginning of the buffer.
                  */
                 bufferOffset += buffer.position();
                 buffer.compact();
-                assert after >= buffer.position();
+                assert after >= buffer.position() : after;
             }
             buffer.limit(after);
         }
@@ -132,8 +182,8 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @return the position of the stream.
      */
     @Override
-    public long getStreamPosition() {
-        long position = super.getStreamPosition();
+    public final long getStreamPosition() {
+        long position = position();
         /*
          * ChannelDataOutput uses a different strategy than ChannelDataInput: if some bits were in process
          * of being written, the buffer position is set to the byte AFTER the byte containing the bits.
@@ -540,11 +590,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of chars to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeChars(final char[] src, int offset, int length) throws IOException {
+    public final void writeChars(final char[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private CharBuffer view;
             @Override Buffer createView() {return view = buffer.asCharBuffer();}
-            @Override void transfer(int offset, int n) {view.put(src, offset, n);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Character.BYTES, offset, length);
     }
 
@@ -556,11 +606,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of shorts to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeShorts(final short[] src, int offset, int length) throws IOException {
+    public final void writeShorts(final short[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private ShortBuffer view;
             @Override Buffer createView() {return view = buffer.asShortBuffer();}
-            @Override void transfer(int offset, int length) {view.put(src, offset, length);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Short.BYTES, offset, length);
     }
 
@@ -572,11 +622,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of integers to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeInts(final int[] src, int offset, int length) throws IOException {
+    public final void writeInts(final int[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private IntBuffer view;
             @Override Buffer createView() {return view = buffer.asIntBuffer();}
-            @Override void transfer(int offset, int n) {view.put(src, offset, n);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Integer.BYTES, offset, length);
     }
 
@@ -588,11 +638,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of longs to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeLongs(final long[] src, int offset, int length) throws IOException {
+    public final void writeLongs(final long[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private LongBuffer view;
             @Override Buffer createView() {return view = buffer.asLongBuffer();}
-            @Override void transfer(int offset, int n) {view.put(src, offset, n);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Long.BYTES, offset, length);
     }
 
@@ -604,11 +654,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of floats to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeFloats(final float[] src, int offset, int length) throws IOException {
+    public final void writeFloats(final float[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private FloatBuffer view;
             @Override Buffer createView() {return view = buffer.asFloatBuffer();}
-            @Override void transfer(int offset, int n) {view.put(src, offset, n);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Float.BYTES, offset, length);
     }
 
@@ -620,11 +670,11 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
      * @param  length  the number of doubles to write.
      * @throws IOException if an error occurred while writing the stream.
      */
-    public final void writeDoubles(final double[] src, int offset, int length) throws IOException {
+    public final void writeDoubles(final double[] src, final int offset, final int length) throws IOException {
         new ArrayWriter() {
             private DoubleBuffer view;
             @Override Buffer createView() {return view = buffer.asDoubleBuffer();}
-            @Override void transfer(int offset, int n) {view.put(src, offset, n);}
+            @Override void transfer(int start, int n) {view.put(src, start, n);}
         }.writeFully(Double.BYTES, offset, length);
     }
 
@@ -669,18 +719,15 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
     @Override
     public void writeUTF(final String s) throws IOException {
         byte[] data = s.getBytes(StandardCharsets.UTF_8);
-        if (data.length > Short.MAX_VALUE) {
+        final int length = data.length;
+        if (length > Short.MAX_VALUE) {
             throw new IllegalArgumentException(Resources.format(
-                    Resources.Keys.ExcessiveStringSize_3, filename, Short.MAX_VALUE, data.length));
-        }
-        final ByteOrder oldOrder = buffer.order();
-        buffer.order(ByteOrder.BIG_ENDIAN);
-        try {
-            writeShort(data.length);
-            write(data);
-        } finally {
-            buffer.order(oldOrder);
+                    Resources.Keys.ExcessiveStringSize_3, filename, Short.MAX_VALUE, length));
         }
+        ensureBufferAccepts(Short.BYTES);
+        buffer.put((byte) (length >>> Byte.SIZE));      // Write using ByteOrder.BIG_ENDIAN.
+        buffer.put((byte) length);
+        write(data);
     }
 
     /**
@@ -734,9 +781,9 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
     }
 
     /**
-     * Moves to the given position in the stream, relative to the stream position at construction time.
-     * If the given position is greater than the stream length, then the values of bytes between the
-     * previous stream length and the given position are unspecified. The limit is unchanged.
+     * Moves to the given position in this stream.
+     * If the given position is greater than the stream length, then the values of all bytes between
+     * the previous stream length and the given position are unspecified. The limit is unchanged.
      *
      * @param  position  the position where to move.
      * @throws IOException if the stream cannot be moved to the given position.
@@ -756,7 +803,7 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
              * but we can set the new position directly in the channel.
              */
             flush();
-            ((SeekableByteChannel) channel).position(Math.addExact(channelOffset, position));
+            ((SeekableByteChannel) channel).position(toSeekableByteChannelPosition(position));
             bufferOffset = position;
         } else if ((p -= buffer.position()) >= 0) {
             /*
@@ -768,52 +815,86 @@ public class ChannelDataOutput extends ChannelData implements DataOutput, Flusha
             // We cannot move position beyond the buffered part.
             throw new IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
         }
-        assert super.getStreamPosition() == position;
+        assert position() == position;
     }
 
     /**
-     * Flushes the {@link #buffer buffer} content to the channel.
+     * Flushes the buffer content to the channel, from buffer beginning to buffer limit.
+     * If the buffer position is not already at the buffer limit, the position is moved.
+     * The buffer is empty after this method call, i.e. the limit is zero.
      * This method does <strong>not</strong> flush the channel itself.
      *
      * @throws IOException if an error occurred while writing to the channel.
      */
     @Override
     public final void flush() throws IOException {
-        buffer.rewind();
+        clearBitOffset();
         writeFully();
+        bufferOffset = position();
         buffer.limit(0);
-        clearBitOffset();
     }
 
     /**
-     * Writes the buffer content up to the given position, then set the buffer position to the given value.
-     * The {@linkplain ByteBuffer#limit() buffer limit} is unchanged, and the buffer offset is incremented
-     * by the given value.
+     * Writes the given number of bytes from the buffer.
+     * This is invoked for making room for more bytes.
+     *
+     * @param  count  number of bytes to write, between 1 and buffer limit.
+     * @throws IOException if an error occurred while writing the bytes to the channel.
      */
     @Override
-    final void flushAndSetPosition(final int position) throws IOException {
-        final int limit = buffer.limit();
-        buffer.rewind().limit(position);
+    final void flushNBytes(final int count) throws IOException {
+        final int position = buffer.position();
+        final int validity = buffer.limit();
+        buffer.limit(count);
         writeFully();
-        buffer.limit(limit);
+        bufferOffset = position();
+        buffer.limit(validity).compact().limit(buffer.position()).position(position - count);
     }
 
     /**
-     * Writes fully the buffer content from its position to its limit.
-     * After this method call, the buffer position is equal to its limit.
+     * Writes fully the buffer content from beginning to buffer limit.
+     * Caller must update the buffer position after this method call.
      *
      * @throws IOException if an error occurred while writing to the channel.
      */
     private void writeFully() throws IOException {
-        int n = buffer.remaining();
-        bufferOffset += n;
-        while (n != 0) {
-            final int c = channel.write(buffer);
-            if (c == 0) {
+        buffer.rewind();
+        while (buffer.hasRemaining()) {
+            if (channel.write(buffer) == 0) {
                 onEmptyTransfer();
             }
-            n -= c;
         }
-        assert !buffer.hasRemaining() : buffer;
+    }
+
+    /**
+     * Notifies two {@code ChannelData} instances that operations will continue with the specified take over.
+     * This method should be invoked when write operations with this {@code ChannelDataOutput} are completed
+     * for now, and read operations are about to begin with a {@link ChannelDataInput} sharing the same channel.
+     *
+     * @param  takeOver  the {@link ChannelDataInput} which will continue operations after this instance.
+     */
+    @Override
+    public void yield(final ChannelData takeOver) throws IOException {
+        final int position = buffer.position();
+        final int limit    = buffer.limit();
+        /*
+         * Flush the full buffer content for avoiding data lost. Note that the buffer position
+         * is not necessarily at the end, so we may write more bytes than the stream position.
+         * It may force us to seek backward after flushing.
+         */
+        clearBitOffset();
+        writeFully();
+        if (position >= limit) {
+            // We were at the end of the buffer, so the channel is already at the right position.
+            bufferOffset = position();
+            buffer.limit(0);
+        } else if (channel instanceof SeekableByteChannel) {
+            // Do not move `bufferOffset`. Instead make the channel position consistent with it.
+            buffer.limit(limit).position(position);
+            ((SeekableByteChannel) channel).position(toSeekableByteChannelPosition(position()));
+        } else {
+            throw new IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
+        }
+        super.yield(takeOver);
     }
 }
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Markable.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Markable.java
index a4e1acd9a8..2400329043 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Markable.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/Markable.java
@@ -30,7 +30,7 @@ import java.io.IOException;
  *
  * <h2>Design note</h2>
  * An alternative could be to support the {@code seek(long)} method. But using marks instead allows the stream
- * to invalidate the marks if needed (for example when {@link ChannelData#setStreamPosition(long)} is invoked).
+ * to invalidate the marks if needed (for example when {@link ChannelData#refresh(long)} is invoked).
  *
  * @author  Martin Desruisseaux (Geomatys)
  *
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/UpdatableWrite.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/UpdatableWrite.java
index 22416314be..26b5e103ce 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/UpdatableWrite.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/UpdatableWrite.java
@@ -43,7 +43,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
     public final long position;
 
     /**
-     * Prepares a new updatable value.
+     * Prepares a new updatable value at the current output position.
      *
      * @param  position  stream where to write the value.
      */
@@ -52,7 +52,16 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
     }
 
     /**
-     * Creates a pseudo-updatable associated to no value.
+     * Prepares a new updatable value at the specified position.
+     *
+     * @param  position  position where to write the value.
+     */
+    private UpdatableWrite(final long position) {
+        this.position = position;
+    }
+
+    /**
+     * Creates a pseudo-updatable associated to no value at the current output position.
      * This variant can be used when the caller only want to record the position, with no write operation.
      *
      * @param  output  stream where to write the value.
@@ -65,7 +74,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
     }
 
     /**
-     * Creates an updatable unsigned short value.
+     * Creates an updatable unsigned short value at the current output position.
      *
      * @param  output  stream where to write the value.
      * @param  value   the unsigned short value to write.
@@ -79,7 +88,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
     }
 
     /**
-     * Creates an updatable unsigned integer value.
+     * Creates an updatable unsigned integer value at the current output position.
      *
      * @param  output  stream where to write the value.
      * @param  value   the unsigned integer value to write.
@@ -93,7 +102,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
     }
 
     /**
-     * Creates an updatable long value.
+     * Creates an updatable long value at the current output position.
      *
      * @param  output  stream where to write the value.
      * @param  value   the value to write.
@@ -106,6 +115,23 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
         return dw;
     }
 
+    /**
+     * Creates an updatable value at the specified position.
+     * The existing value is assumed to be zero.
+     *
+     * @param  <V>       compile-time value of {@code type}.
+     * @param  position  position where the value is written. Current value must be zero (this is not verified).
+     * @param  type      class of the value as {@code Short.class}, {@code Integer.class} or {@code Long.class}.
+     * @return handler for modifying the value later.
+     */
+    @SuppressWarnings("unchecked")
+    public static <V extends Number> UpdatableWrite<V> ofZeroAt(final long position, final Class<V> type) {
+        if (type == Integer.class) return (UpdatableWrite<V>) new OfInt  (position);
+        if (type ==   Short.class) return (UpdatableWrite<V>) new OfShort(position);
+        if (type ==    Long.class) return (UpdatableWrite<V>) new OfLong (position);
+        throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
+    }
+
     /**
      * Implementation of {@link UpdatableWrite#of(ChannelDataOutput)}.
      * This class only records the stream position, with no associated value.
@@ -136,6 +162,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
             defined = value;
         }
 
+        OfShort(long position) {super(position);}
         @Override public Class<Short> getElementType()   {return Short.class;}
         @Override public int          sizeInBytes()      {return Short.BYTES;}
         @Override public boolean      changed()          {return defined != current;}
@@ -164,6 +191,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
             defined = value;
         }
 
+        OfInt(long position) {super(position);}
         @Override public Class<Integer> getElementType()  {return Integer.class;}
         @Override public int            sizeInBytes()     {return Integer.BYTES;}
         @Override public boolean        changed()         {return defined != current;}
@@ -192,6 +220,7 @@ public abstract class UpdatableWrite<V> implements CheckedContainer<V> {
             defined = value;
         }
 
+        OfLong(long position) {super(position);}
         @Override public Class<Long>  getElementType()   {return Long.class;}
         @Override public int          sizeInBytes()      {return Long.BYTES;}
         @Override public boolean      changed()          {return defined != current;}
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 eb2f6d4628..c1cef85344 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
@@ -37,7 +37,6 @@ import java.nio.ByteBuffer;
 import java.nio.channels.Channel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
-import java.nio.channels.SeekableByteChannel;
 import java.nio.file.Path;
 import java.nio.file.OpenOption;
 import java.nio.file.NoSuchFileException;
@@ -64,6 +63,7 @@ import org.apache.sis.storage.base.StoreUtilities;
 import org.apache.sis.io.InvalidSeekException;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.io.stream.ChannelFactory;
+import org.apache.sis.io.stream.ChannelData;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.ChannelImageInputStream;
@@ -106,7 +106,7 @@ import org.apache.sis.setup.OptionKey;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 public class StorageConnector implements Serializable {
@@ -513,15 +513,14 @@ public class StorageConnector implements Serializable {
                  * (except in BufferedReader if the original storage does not support mark/reset).
                  */
                 ((Reader) view).reset();
-            } else if (view instanceof ChannelDataInput) {
+            } else if (view instanceof ChannelData) {
                 /*
-                 * ChannelDataInput can be recycled without the need to discard and recreate them. Note that
-                 * this code requires that SeekableByteChannel has been seek to the channel beginning first.
-                 * This should be done by the above `wrapperFor.reset()` call.
+                 * `ChannelDataInput` can be recycled without the need to discard and recreate it.
+                 * However if a `Channel` was used directly, it should have been seek to the channel
+                 * beginning first. This seek should be done by above call to `wrapperFor.reset()`,
+                 * which should cause the block below (with a call to `rewind()`) to be executed.
                  */
-                final ChannelDataInput input = (ChannelDataInput) view;
-                input.buffer.limit(0);                                      // Must be after channel reset.
-                input.setStreamPosition(0);                                 // Must be after buffer.limit(0).
+                ((ChannelData) view).seek(0);
             } else if (view instanceof Channel) {
                 /*
                  * Searches for a ChannelDataInput wrapping the channel, because it contains the original position
@@ -532,10 +531,10 @@ public class StorageConnector implements Serializable {
                 String name = null;
                 if (wrappedBy != null) {
                     for (Coupled c : wrappedBy) {
-                        if (c.view instanceof ChannelDataInput) {
-                            final ChannelDataInput in = ((ChannelDataInput) c.view);
-                            if (view instanceof SeekableByteChannel) {
-                                ((SeekableByteChannel) view).position(in.channelOffset);
+                        if (c.view instanceof ChannelData) {
+                            final var in = (ChannelData) c.view;
+                            assert in.channel() == view : c;
+                            if (in.rewind()) {
                                 return true;
                             }
                             name = in.filename;                                     // For the error message.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
index acfbb6cb9b..e7f4bbf143 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
@@ -24,6 +24,8 @@ import java.io.OutputStream;
 import java.io.File;
 import java.net.URI;
 import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.FileSystemNotFoundException;
 import java.nio.charset.Charset;
@@ -317,17 +319,35 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R
 
         /**
          * Returns {@code true} if the open options contains {@link StandardOpenOption#WRITE}
-         * or if the storage type is some kind of output stream.
+         * or if the storage type is some kind of output stream. An ambiguity may exist between
+         * the case when a new file would be created and when an existing file would be updated.
+         * This ambiguity is resolved by the {@code ifNew} argument:
+         * if {@code false}, then the two cases are not distinguished.
+         * If {@code true}, then this method returns {@code true} only if a new file would be created.
          *
          * @param  connector  the connector to use for opening a file.
+         * @param  ifNew  whether to return {@code true} only if a new file would be created.
          * @return whether the specified connector should open a writable data store.
          * @throws DataStoreException if the storage object has already been used and cannot be reused.
          */
-        public static boolean isWritable(final StorageConnector connector) throws DataStoreException {
+        public static boolean isWritable(final StorageConnector connector, final boolean ifNew) throws DataStoreException {
             final Object storage = connector.getStorage();
             if (storage instanceof OutputStream || storage instanceof DataOutput) return true;    // Must be tested first.
             if (storage instanceof InputStream  || storage instanceof DataInput)  return false;   // Ignore options.
-            return ArraysExt.contains(connector.getOption(OptionKey.OPEN_OPTIONS), StandardOpenOption.WRITE);
+            final OpenOption[] options = connector.getOption(OptionKey.OPEN_OPTIONS);
+            if (ArraysExt.contains(options, StandardOpenOption.WRITE)) {
+                if (!ifNew || ArraysExt.contains(options, StandardOpenOption.TRUNCATE_EXISTING)) {
+                    return true;
+                }
+                if (ArraysExt.contains(options, StandardOpenOption.CREATE_NEW)) {
+                    return IOUtilities.isKindOfPath(storage);
+                }
+                if (ArraysExt.contains(options, StandardOpenOption.CREATE)) {
+                    final Path path = connector.getStorageAs(Path.class);
+                    return (path != null) && Files.notExists(path);
+                }
+            }
+            return false;
         }
     }
 
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/AsciiGridStoreProvider.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/AsciiGridStoreProvider.java
index 6af287a2d7..65a5791c12 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/AsciiGridStoreProvider.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/AsciiGridStoreProvider.java
@@ -135,7 +135,7 @@ cellsize:           if (!header.containsKey(AsciiGridStore.CELLSIZE)) {
      */
     @Override
     public DataStore open(final StorageConnector connector) throws DataStoreException {
-        if (isWritable(connector)) {
+        if (isWritable(connector, false)) {
             return new WritableStore(this, connector);
         } else {
             return new AsciiGridStore(this, connector, true);
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
index 5df4febc2e..8110dca82f 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/FormatFinder.java
@@ -155,7 +155,7 @@ final class FormatFinder implements AutoCloseable {
             openAsWriter = false;
             fileIsEmpty  = false;
         } else {
-            isWritable = WorldFileStoreProvider.isWritable(connector);
+            isWritable = WorldFileStoreProvider.isWritable(connector, false);
             if (isWritable) {
                 final Path path = connector.getStorageAs(Path.class);
                 if (path != null) {
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java
index 8b73eef922..a36e06002e 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java
@@ -34,7 +34,6 @@ import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.ResourceAlreadyExistsException;
 import org.apache.sis.storage.IncompatibleResourceException;
 import org.apache.sis.storage.WritableGridCoverageResource;
-import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/package-info.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/package-info.java
index 5c474f44c4..6a1020cb19 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/package-info.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/package-info.java
@@ -26,7 +26,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 package org.apache.sis.storage;
diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
index 9fe710f87d..818a3feacd 100644
--- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
+++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/ChannelImageOutputStreamTest.java
@@ -84,7 +84,7 @@ public final class ChannelImageOutputStreamTest extends ChannelDataOutputTest {
      */
     @Test
     public void testMarkAndReset() throws IOException {
-        initialize("testMarkAndReset", STREAM_LENGTH, 1000); // We need a larger buffer for this test.
+        initialize("testMarkAndReset", STREAM_LENGTH, 1000);        // We need a larger buffer for this test.
         /*
          * Fill both streams with random data.
          * During this process, randomly takes mark.