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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new c1c1be4b9a Allow `StorageConnector` to create `ChannelDataOutput` 
instance. This feature allows writer such as ASCII Grid to write in 
destinations other than files.
c1c1be4b9a is described below

commit c1c1be4b9abed49af2a7f79cc780935224697cb0
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Apr 10 17:26:35 2022 +0200

    Allow `StorageConnector` to create `ChannelDataOutput` instance.
    This feature allows writer such as ASCII Grid to write in destinations 
other than files.
---
 .../apache/sis/internal/gui/io/FileAccessView.java |  20 +++-
 .../sis/internal/storage/ascii/CharactersView.java |   7 +-
 .../apache/sis/internal/storage/ascii/Store.java   |  31 +++--
 .../sis/internal/storage/ascii/StoreProvider.java  |   2 +-
 .../sis/internal/storage/ascii/WritableStore.java  |  56 +++++++--
 .../sis/internal/storage/io/ChannelFactory.java    | 126 ++++++++++++++++-----
 .../org/apache/sis/storage/StorageConnector.java   | 103 +++++++++++++----
 .../sis/internal/storage/ascii/StoreTest.java      |   4 +-
 .../internal/storage/ascii/WritableStoreTest.java  |  45 ++++++--
 9 files changed, 313 insertions(+), 81 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java
index ea7db4ac23..5c3f2e0b63 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/io/FileAccessView.java
@@ -90,14 +90,30 @@ public final class FileAccessView extends Widget implements 
UnaryOperator<Channe
     /**
      * Invoked when a new {@link ReadableByteChannel} or {@link 
WritableByteChannel} is about to be created.
      * The caller will replace the given factory by the returned factory. It 
allows us to wrap the channel
-     * in an object will will collect information about blocks read.
+     * in an object which will collect information about blocks read.
      *
      * @param  factory  the factory for creating channels.
      * @return the factory to use instead of the factory given in argument.
      */
     @Override
     public ChannelFactory apply(final ChannelFactory factory) {
-        return new ChannelFactory() {
+        return new ChannelFactory(factory.suggestDirectBuffer) {
+            /**
+             * Returns whether using the streams or channels will affect the 
original {@code storage} object.
+             */
+            @Override
+            public boolean isCoupled() {
+                return factory.isCoupled();
+            }
+
+            /**
+             * Returns {@code true} if this factory is capable to create 
another readable byte channel.
+             */
+            @Override
+            public boolean canOpen() {
+                return factory.canOpen();
+            }
+
             /**
              * Creates a readable channel and listens (if possible) read 
operations.
              * Current implementation listens only to {@link 
SeekableByteChannel}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
index e3c66ed3b4..47851621c2 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/CharactersView.java
@@ -75,12 +75,9 @@ final class CharactersView implements CharSequence {
      * Creates a new sequence of characters.
      *
      * @param  input   the source of bytes, or {@code null} if unavailable.
-     * @oaram  buffer  the buffer, or {@code null} for {@code input.buffer}.
+     * @param  buffer  {@code input.buffer} or a standalone buffer if {@code 
input} is null.
      */
-    CharactersView(final ChannelDataInput input, ByteBuffer buffer) {
-        if (buffer == null) {
-            buffer = input.buffer;
-        }
+    CharactersView(final ChannelDataInput input, final ByteBuffer buffer) {
         this.input  = input;
         this.buffer = buffer;
         this.direct = buffer.hasArray();
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
index ed7531792f..aea96a47b6 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
@@ -159,20 +159,37 @@ class Store extends PRJDataStore implements 
GridCoverageResource {
      *
      * @param  provider   the factory that created this {@code DataStore} 
instance, or {@code null} if unspecified.
      * @param  connector  information about the storage (URL, stream, 
<i>etc</i>).
+     * @param  readOnly   whether to fail if the channel can not be opened at 
least in read mode.
      * @throws DataStoreException if an error occurred while opening the 
stream.
      */
-    public Store(final StoreProvider provider, final StorageConnector 
connector) throws DataStoreException {
+    Store(final StoreProvider provider, final StorageConnector connector, 
final boolean readOnly)
+            throws DataStoreException
+    {
         super(provider, connector);
-        input = new CharactersView(connector.commit(ChannelDataInput.class, 
StoreProvider.NAME), null);
+        final ChannelDataInput channel;
+        if (readOnly) {
+            channel = connector.commit(ChannelDataInput.class, 
StoreProvider.NAME);
+        } else {
+            channel = connector.getStorageAs(ChannelDataInput.class);
+            if (channel != null) {
+                connector.closeAllExcept(channel);
+            }
+        }
+        if (channel != null) {
+            input = new CharactersView(channel, channel.buffer);
+        }
         listeners.useWarningEventsOnly();
     }
 
     /**
-     * Returns whether this store is read-only. If {@code true}, we can close 
the channel
-     * as soon as the coverage has been fully read. Otherwise we need to keep 
it open.
+     * Returns whether this store can read or write. If this store can not 
write,
+     * then we can close the {@linkplain #input} channel as soon as the 
coverage
+     * has been fully read. Otherwise we need to keep it open.
+     *
+     * @param  write  {@code false} for testing read capability, or {@code 
true} for testing write capability.
      */
-    boolean isReadOnly() {
-        return true;
+    boolean canReadOrWrite(final boolean write) {
+        return !write && (input != null);
     }
 
     /**
@@ -428,7 +445,7 @@ cellsize:       if (value != null) {
              * TODO: a future version could try to convert the image to 
integer values.
              * In this case only we may need to declare the NODATA_VALUE.
              */
-            if (isReadOnly()) {
+            if (!canReadOrWrite(true)) {
                 input = null;
                 view.input.channel.close();
             }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
index aa58acc499..a77dbaf7de 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
@@ -131,7 +131,7 @@ cellsize:           if 
(!header.containsKey(Store.CELLSIZE)) {
         if (isWritable(connector)) {
             return new WritableStore(this, connector);
         } else {
-            return new Store(this, connector);
+            return new Store(this, connector, true);
         }
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
index 7dd32f37a0..4246b2ac5b 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
@@ -34,7 +34,6 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.WritableGridCoverageResource;
 import org.apache.sis.internal.storage.WritableResourceSupport;
-import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.ChannelDataOutput;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -58,6 +57,12 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
      */
     private final String lineSeparator;
 
+    /**
+     * The output if this store is write-only, or {@code null} if this store 
is read/write.
+     * This is set to {@code null} when the store is closed.
+     */
+    private ChannelDataOutput output;
+
     /**
      * Creates a new ASCII Grid store from the given file, URL or stream.
      *
@@ -66,16 +71,19 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
      * @throws DataStoreException if an error occurred while opening the 
stream.
      */
     public WritableStore(final StoreProvider provider, final StorageConnector 
connector) throws DataStoreException {
-        super(provider, connector);
+        super(provider, connector, false);
         lineSeparator = System.lineSeparator();
+        if (!super.canReadOrWrite(false)) {
+            output = connector.commit(ChannelDataOutput.class, 
StoreProvider.NAME);
+        }
     }
 
     /**
-     * Returns whether this store is read-only.
+     * Returns whether this store can read or write.
      */
     @Override
-    boolean isReadOnly() {
-        return false;
+    boolean canReadOrWrite(final boolean write) {
+        return write || super.canReadOrWrite(write);
     }
 
     /**
@@ -185,10 +193,13 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
     @Override
     public synchronized void write(GridCoverage coverage, final Option... 
options) throws DataStoreException {
         final WritableResourceSupport h = new WritableResourceSupport(this, 
options);   // Does argument validation.
-        final ChannelDataInput input = input().input;
         final int band = 0;                                 // May become 
configurable in a future version.
         try {
-            if (!h.replace(input)) {
+            /*
+             * If `output` is null, we are in write-only mode and there is no 
previously existing image.
+             * Otherwise an image may exist and the behavior will depends on 
which options were supplied.
+             */
+            if (output == null && !h.replace(input().input)) {
                 coverage = h.update(coverage);
             }
             final RenderedImage data = coverage.render(null);               // 
Fail if not two-dimensional.
@@ -201,7 +212,7 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
              * After this point we should not have any validation errors. 
Write the nodata value even if it is
              * "NaN" because the default is -9999, and we need to overwrite 
that default if it can not be used.
              */
-            final ChannelDataOutput out = h.channel(input);
+            final ChannelDataOutput out = (output != null) ? output : 
h.channel(input().input);
             final Number nodataValue = setCoverage(coverage, data, band);
             header.put(NODATA_VALUE, nodataValue);
             writeHeader(header, out);
@@ -251,6 +262,14 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
             }
             out.flush();
             writePRJ();
+            /*
+             * If the channel is write-only (e.g. if we are writing in an 
`OutputStream`),
+             * we will not be able to write a second time.
+             */
+            if (output != null) {
+                output = null;
+                out.channel.close();
+            }
         } catch (IOException e) {
             closeOnError(e);
             throw new DataStoreException(e);
@@ -267,4 +286,25 @@ final class WritableStore extends Store implements 
WritableGridCoverageResource
             out.buffer.put((byte) text.charAt(i));
         }
     }
+
+    /**
+     * Closes this data store and releases any underlying resources.
+     *
+     * @throws DataStoreException if an error occurred while closing this data 
store.
+     */
+    @Override
+    public synchronized void close() throws DataStoreException {
+        final ChannelDataOutput out = output;
+        output = null;
+        if (out != null) try {
+            out.channel.close();
+        } catch (IOException e) {
+            throw new DataStoreException(e);
+        }
+        /*
+         * No need for try-with-resource because only one
+         * of `input` and `output` should be non-null.
+         */
+        super.close();
+    }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
index 9755cfb212..12f16b8ee3 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
@@ -42,6 +42,7 @@ import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.channels.Channel;
 import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import org.apache.sis.util.logging.Logging;
@@ -55,7 +56,7 @@ import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
- * Opens a readable channel for a given input object (URL, input stream, 
<i>etc</i>).
+ * Opens a readable or writable channel for a given input object (URL, input 
stream, <i>etc</i>).
  * The {@link #prepare prepare(…)} method analyzes the given input {@link 
Object} and tries to return a factory instance
  * capable to open at least a {@link ReadableByteChannel} for that input. For 
some kinds of input like {@link Path} or
  * {@link URL}, the {@link #readable readable(…)} method can be invoked an 
arbitrary amount of times for creating as many
@@ -75,15 +76,26 @@ import org.apache.sis.storage.event.StoreListeners;
  */
 public abstract class ChannelFactory {
     /**
-     * Options to be rejected by {@link #prepare(Object, boolean, String, 
OpenOption[])} for safety reasons.
+     * Options to be rejected by {@link #prepare(Object, boolean, String, 
OpenOption[])} for safety reasons,
+     * unless {@code allowWriteOnly} is {@code true}.
      */
     private static final Set<StandardOpenOption> ILLEGAL_OPTIONS = EnumSet.of(
             StandardOpenOption.APPEND, StandardOpenOption.TRUNCATE_EXISTING, 
StandardOpenOption.DELETE_ON_CLOSE);
 
+    /**
+     * Whether this factory suggests to use direct buffers instead of heap 
buffers.
+     * Direct buffer should be used for channels on the default file system or 
other
+     * native source of data, and avoided otherwise.
+     */
+    public final boolean suggestDirectBuffer;
+
     /**
      * For subclass constructors.
+     *
+     * @param  suggestDirectBuffer  whether this factory suggests to use 
direct buffers instead of heap buffers.
      */
-    protected ChannelFactory() {
+    protected ChannelFactory(final boolean suggestDirectBuffer) {
+        this.suggestDirectBuffer = suggestDirectBuffer;
     }
 
     /**
@@ -98,7 +110,7 @@ public abstract class ChannelFactory {
      *   <li>If the given storage is a {@link WritableByteChannel} or an 
{@link OutputStream}
      *       and the {@code allowWriteOnly} argument is {@code true},
      *       then the factory will return that output directly or indirectly 
as a wrapper.</li>
-     *   <li>If the given storage if a {@link Path}, {@link File}, {@link 
URL}, {@link URI}
+     *   <li>If the given storage is a {@link Path}, {@link File}, {@link 
URL}, {@link URI}
      *       or {@link CharSequence} and the file is not a directory, then the 
factory will
      *       open new channels on demand.</li>
      * </ul>
@@ -153,10 +165,10 @@ public abstract class ChannelFactory {
      * @throws IOException if an error occurred while processing the given 
input.
      */
     private static ChannelFactory prepare(Object storage, final boolean 
allowWriteOnly,
-            final String encoding, OpenOption[] options) throws IOException
+            final String encoding, final OpenOption[] options) throws 
IOException
     {
         /*
-         * Unconditionally verify the options (unless 'allowWriteOnly' is 
true),
+         * Unconditionally verify the options (unless `allowWriteOnly` is 
true),
          * even if we may not use them.
          */
         final Set<OpenOption> optionSet;
@@ -171,17 +183,20 @@ public abstract class ChannelFactory {
             }
         }
         /*
-         * Check for inputs that are already readable channels or input 
streams.
+         * Check for storages that are already readable/Writable channels or 
input/output streams.
+         * The channel or stream will be either returned directly or wrapped 
when first needed,
+         * depending which factory method will be invoked.
+         *
          * Note that Channels.newChannel(InputStream) checks for instances of 
FileInputStream in order to delegate
          * to its getChannel() method, but only if the input stream type is 
exactly FileInputStream, not a subtype.
          * If Apache SIS defines its own FileInputStream subclass someday, we 
may need to add a special case here.
          */
         if (storage instanceof ReadableByteChannel || (allowWriteOnly && 
storage instanceof WritableByteChannel)) {
-            return new Stream((Channel) storage);
+            return new Stream(storage, storage instanceof FileChannel);
         } else if (storage instanceof InputStream) {
-            return new Stream(Channels.newChannel((InputStream) storage));
+            return new Stream(storage, storage.getClass() == 
FileInputStream.class);
         } else if (allowWriteOnly && storage instanceof OutputStream) {
-            return new Stream(Channels.newChannel((OutputStream) storage));
+            return new Stream(storage, storage.getClass() == 
FileOutputStream.class);
         }
         /*
          * In the following cases, we will try hard to convert to Path objects 
before to fallback
@@ -257,7 +272,13 @@ public abstract class ChannelFactory {
          */
         if (storage instanceof URL) {
             final URL file = (URL) storage;
-            return new ChannelFactory() {
+            return new ChannelFactory(false) {
+                @Override public InputStream inputStream(String filename, 
StoreListeners listeners) throws IOException {
+                    return file.openStream();
+                }
+                @Override public OutputStream outputStream(String filename, 
StoreListeners listeners) throws IOException {
+                    return file.openConnection().getOutputStream();
+                }
                 @Override public ReadableByteChannel readable(String filename, 
StoreListeners listeners) throws IOException {
                     return Channels.newChannel(file.openStream());
                 }
@@ -278,7 +299,7 @@ public abstract class ChannelFactory {
         if (storage instanceof Path) {
             final Path path = (Path) storage;
             if (!Files.isDirectory(path)) {
-                return new ChannelFactory() {
+                return new ChannelFactory(true) {
                     @Override public ReadableByteChannel readable(String 
filename, StoreListeners listeners) throws IOException {
                         return Files.newByteChannel(path, optionSet);
                     }
@@ -318,6 +339,10 @@ public abstract class ChannelFactory {
      * Returns the readable channel as an input stream. The returned stream is 
<strong>not</strong> buffered;
      * it is caller's responsibility to wrap the stream in a {@link 
java.io.BufferedInputStream} if desired.
      *
+     * <p>The default implementation wraps the channel returned by {@link 
#readable(String, StoreListeners)}.
+     * This wrapping is preferred to direct instantiation of {@link 
FileInputStream} in order to take in account
+     * the {@link OpenOption}s.</p>
+     *
      * @param  filename  data store name to report in case of failure.
      * @param  listeners set of registered {@code StoreListener}s for the data 
store, or {@code null} if none.
      * @return the input stream.
@@ -334,6 +359,10 @@ public abstract class ChannelFactory {
      * Returns the writable channel as an output stream. The returned stream 
is <strong>not</strong> buffered;
      * it is caller's responsibility to wrap the stream in a {@link 
java.io.BufferedOutputStream} if desired.
      *
+     * <p>The default implementation wraps the channel returned by {@link 
#writable(String, StoreListeners)}.
+     * This wrapping is preferred to direct instantiation of {@link 
FileOutputStream} in order to take in account
+     * the {@link OpenOption}s.</p>
+     *
      * @param  filename  data store name to report in case of failure.
      * @param  listeners set of registered {@code StoreListener}s for the data 
store, or {@code null} if none.
      * @return the output stream.
@@ -375,20 +404,26 @@ public abstract class ChannelFactory {
             throws DataStoreException, IOException;
 
     /**
-     * A factory that returns an existing channel <cite>as-is</cite>.
+     * A factory that returns an existing channel <cite>as-is</cite>. The 
channel is often wrapping an
+     * {@link InputStream} or {@link OutputStream} (which is the reason for 
{@code Stream} class name),
+     * otherwise {@link org.apache.sis.storage.StorageConnector} would hare 
returned the storage object
+     * directly instead of instantiating this factory.
      * The channel can be returned only once.
      */
     private static final class Stream extends ChannelFactory {
         /**
-         * The channel, or {@code null} if it has already been returned.
+         * The stream or channel, or {@code null} if it has already been 
returned.
+         * Shall be an instance of {@link InputStream}, {@link OutputStream},
+         * {@link ReadableByteChannel} or {@link WritableByteChannel}.
          */
-        private Channel channel;
+        private Object storage;
 
         /**
-         * Creates a new factory for the given channel, which will be returned 
only once.
+         * Creates a new factory for the given stream or channel, which will 
be returned only once.
          */
-        Stream(final Channel input) {
-            this.channel = input;
+        Stream(final Object storage, final boolean suggestDirectBuffer) {
+            super(suggestDirectBuffer);
+            this.storage = storage;
         }
 
         /**
@@ -404,21 +439,54 @@ public abstract class ChannelFactory {
          */
         @Override
         public boolean canOpen() {
-            return channel != null;
+            return storage != null;
+        }
+
+        /**
+         * Returns the storage object as an input stream. This is either the 
stream specified at construction
+         * time if it can be returned directly, or a wrapper around the {@link 
ReadableByteChannel} otherwise.
+         * The input stream can be returned at most once, otherwise an 
exception is thrown.
+         */
+        @Override
+        public InputStream inputStream(String filename, StoreListeners 
listeners) throws DataStoreException, IOException {
+            final Object in = storage;
+            if (in instanceof InputStream) {
+                storage = null;
+                return (InputStream) in;
+            }
+            return super.inputStream(filename, listeners);
+        }
+
+        /**
+         * Returns the storage object as an output stream. This is either the 
stream specified at construction
+         * time if it can be returned directly, or a wrapper around the {@link 
WritableByteChannel} otherwise.
+         * The output stream can be returned at most once, otherwise an 
exception is thrown.
+         */
+        @Override
+        public OutputStream outputStream(String filename, StoreListeners 
listeners) throws DataStoreException, IOException {
+            final Object out = storage;
+            if (out instanceof OutputStream) {
+                storage = null;
+                return (OutputStream) out;
+            }
+            return super.outputStream(filename, listeners);
         }
 
         /**
-         * Returns the readable channel on the first invocation or
-         * throws an exception on all subsequent invocations.
+         * Returns the readable channel on the first invocation or throws an 
exception on all subsequent invocations.
+         * This is either the channel specified at construction time, or a 
wrapper around the {@link InputStream}.
          */
         @Override
         public ReadableByteChannel readable(final String filename, final 
StoreListeners listeners)
                 throws DataStoreException, IOException
         {
-            final Channel in = channel;
+            final Object in = storage;
             if (in instanceof ReadableByteChannel) {
-                channel = null;
+                storage = null;
                 return (ReadableByteChannel) in;
+            } else if (in instanceof InputStream) {
+                storage = null;
+                return Channels.newChannel((InputStream) in);
             }
             String message = Resources.format(in != null ? 
Resources.Keys.StreamIsNotReadable_1
                                                          : 
Resources.Keys.StreamIsReadOnce_1, filename);
@@ -430,17 +498,20 @@ public abstract class ChannelFactory {
         }
 
         /**
-         * Returns the writable channel on the first invocation or
-         * throws an exception on all subsequent invocations.
+         * Returns the writable channel on the first invocation or throws an 
exception on all subsequent invocations.
+         * This is either the channel specified at construction time, or a 
wrapper around the {@link OutputStream}.
          */
         @Override
         public WritableByteChannel writable(final String filename, final 
StoreListeners listeners)
                 throws DataStoreException, IOException
         {
-            final Channel out = channel;
+            final Object out = storage;
             if (out instanceof WritableByteChannel) {
-                channel = null;
+                storage = null;
                 return (WritableByteChannel) out;
+            } else if (out instanceof OutputStream) {
+                storage = null;
+                return Channels.newChannel((OutputStream) out);
             }
             String message = Resources.format(out != null ? 
Resources.Keys.StreamIsNotWritable_1
                                                           : 
Resources.Keys.StreamIsWriteOnce_1, filename);
@@ -474,6 +545,7 @@ public abstract class ChannelFactory {
          * Creates a new fallback to use if the given file can not be 
converted to a {@link Path}.
          */
         Fallback(final File file, final InvalidPathException cause) {
+            super(true);
             this.file  = file;
             this.cause = cause;
         }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
index abe5800b2b..a9de16c43f 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
@@ -31,6 +31,7 @@ import java.io.Serializable;
 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.NoSuchFileException;
 import javax.imageio.stream.ImageInputStream;
@@ -53,6 +54,7 @@ import org.apache.sis.internal.storage.StoreUtilities;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.io.ChannelFactory;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.ChannelDataOutput;
 import org.apache.sis.internal.storage.io.ChannelImageInputStream;
 import org.apache.sis.internal.storage.io.InputStreamAdapter;
 import org.apache.sis.internal.storage.io.RewindableLineReader;
@@ -181,15 +183,16 @@ public class StorageConnector implements Serializable {
      */
     private static final Map<Class<?>, Opener<?>> OPENERS = new 
IdentityHashMap<>(13);
     static {
-        add(String.class,           StorageConnector::createString);
-        add(ByteBuffer.class,       StorageConnector::createByteBuffer);
-        add(DataInput.class,        StorageConnector::createDataInput);
-        add(ImageInputStream.class, StorageConnector::createImageInputStream);
-        add(InputStream.class,      StorageConnector::createInputStream);
-        add(Reader.class,           StorageConnector::createReader);
-        add(Connection.class,       StorageConnector::createConnection);
-        add(ChannelDataInput.class, (s) -> s.createChannelDataInput(false));   
 // Undocumented case (SIS internal)
-        add(ChannelFactory.class,   (s) -> null);                              
 // Undocumented. Shall not cache.
+        add(String.class,            StorageConnector::createString);
+        add(ByteBuffer.class,        StorageConnector::createByteBuffer);
+        add(DataInput.class,         StorageConnector::createDataInput);
+        add(ImageInputStream.class,  StorageConnector::createImageInputStream);
+        add(InputStream.class,       StorageConnector::createInputStream);
+        add(Reader.class,            StorageConnector::createReader);
+        add(Connection.class,        StorageConnector::createConnection);
+        add(ChannelDataInput.class,  (s) -> s.createChannelDataInput(false));  
 // Undocumented case (SIS internal)
+        add(ChannelDataOutput.class, (s) -> s.createChannelDataOutput());      
 // Undocumented case (SIS internal)
+        add(ChannelFactory.class,    (s) -> null);                             
 // Undocumented. Shall not cache.
         /*
          * ChannelFactory may have been created as a side effect of creating a 
ReadableByteChannel.
          * Caller should have asked for another type (e.g. InputStream) before 
to ask for that type.
@@ -931,6 +934,8 @@ public class StorageConnector implements Serializable {
      *
      * @param  asImageInputStream  whether the {@code ChannelDataInput} needs 
to be {@link ChannelImageInputStream} subclass.
      * @throws IOException if an error occurred while opening a channel for 
the input.
+     *
+     * @see #createChannelDataOutput()
      */
     private ChannelDataInput createChannelDataInput(final boolean 
asImageInputStream) throws IOException, DataStoreException {
         /*
@@ -943,7 +948,7 @@ public class StorageConnector implements Serializable {
             ((InputStream) storage).mark(DEFAULT_BUFFER_SIZE);
         }
         /*
-         * Following method call recognizes ReadableByteChannel, InputStream 
(with special case for FileInputStream),
+         * Following method call recognizes ReadableByteChannel, InputStream 
(with optimization for FileInputStream),
          * URL, URI, File, Path or other types that may be added in future 
Apache SIS versions.
          * If the given storage is already a ReadableByteChannel, then the 
factory will return it as-is.
          */
@@ -961,16 +966,7 @@ public class StorageConnector implements Serializable {
         final String name = getStorageName();
         final ReadableByteChannel channel = factory.readable(name, null);
         addView(ReadableByteChannel.class, channel, null, factory.isCoupled() 
? CASCADE_ON_RESET : 0);
-        ByteBuffer buffer = getOption(OptionKey.BYTE_BUFFER);       // 
User-supplied buffer.
-        if (buffer == null) {
-            /*
-             * If the user did not specified a buffer, creates one now. We use 
a direct buffer for better
-             * leveraging of `ChannelDataInput`, which tries hard to transfer 
data in the most direct way
-             * between buffers and arrays. By contrast creating a heap buffer 
would have implied the use
-             * of a temporary direct buffer cached by the JDK itself (in JDK 
internal implementation).
-             */
-            buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
-        }
+        final ByteBuffer buffer = getChannelBuffer(factory);
         final ChannelDataInput asDataInput;
         if (asImageInputStream) {
             asDataInput = new ChannelImageInputStream(name, channel, buffer, 
false);
@@ -1037,6 +1033,28 @@ public class StorageConnector implements Serializable {
         return asDataInput;
     }
 
+    /**
+     * Returns or allocate a buffer for use with the {@link ChannelDataInput} 
or {@link ChannelDataOutput}.
+     * If the user did not specified a buffer, this method may allocate a 
direct buffer for better
+     * leveraging of {@link ChannelDataInput}, which tries hard to transfer 
data in the most direct
+     * way between buffers and arrays. By contrast creating a heap buffer may 
imply the use of a
+     * temporary direct buffer cached by the JDK itself (in JDK internal 
implementation).
+     *
+     * @param  factory  the factory which will be used for creating the 
readable or writable channel.
+     * @return the byte buffer to use with {@link ChannelDataInput} or {@link 
ChannelDataOutput}.
+     */
+    private ByteBuffer getChannelBuffer(final ChannelFactory factory) {
+        ByteBuffer buffer = getOption(OptionKey.BYTE_BUFFER);               // 
User-supplied buffer.
+        if (buffer == null) {
+            if (factory.suggestDirectBuffer) {
+                buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE);
+            } else {
+                buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
+            }
+        }
+        return buffer;
+    }
+
     /**
      * Creates a {@link ByteBuffer} from the {@link ChannelDataInput} if 
possible, or from the
      * {@link ImageInputStream} otherwise. The buffer will be initialized with 
an arbitrary amount
@@ -1240,6 +1258,51 @@ public class StorageConnector implements Serializable {
         addView(type, view, null, (byte) 0);
     }
 
+    /**
+     * Creates a view for the storage as a {@link ChannelDataOutput} if 
possible.
+     *
+     * @throws IOException if an error occurred while opening a channel for 
the output.
+     *
+     * @see #createChannelDataInput(boolean)
+     */
+    private ChannelDataOutput createChannelDataOutput() throws IOException, 
DataStoreException {
+        /*
+         * We need to reset because the output that we will build may be 
derived
+         * from the `ChannelDataInput`, which may have read some bytes.
+         */
+        reset();
+        /*
+         * Following method call recognizes WritableByteChannel, OutputStream 
(with optimization for FileOutputStream),
+         * URL, URI, File, Path or other types that may be added in future 
Apache SIS versions.
+         * If the given storage is already a WritableByteChannel, then the 
factory will return it as-is.
+         */
+        final ChannelFactory factory = ChannelFactory.prepare(storage, true,
+                getOption(OptionKey.URL_ENCODING),
+                getOption(OptionKey.OPEN_OPTIONS),
+                getOption(InternalOptionKey.CHANNEL_FACTORY_WRAPPER));
+        if (factory == null) {
+            return null;
+        }
+        /*
+         * ChannelDataOutput depends on WritableByteChannel, which itself 
depends on storage
+         * (potentially an OutputStream). We need to remember this chain in 
`Coupled` objects.
+         */
+        final String name = getStorageName();
+        final WritableByteChannel channel = factory.writable(name, null);
+        addView(WritableByteChannel.class, channel, null, factory.isCoupled() 
? CASCADE_ON_RESET : 0);
+        final ByteBuffer buffer = getChannelBuffer(factory);
+        final ChannelDataOutput asDataOutput = new ChannelDataOutput(name, 
channel, buffer);
+        addView(ChannelDataOutput.class, asDataOutput, 
WritableByteChannel.class, CASCADE_ON_RESET);
+        /*
+         * Following is an undocumented mechanism for allowing some Apache SIS 
implementations of DataStore
+         * to re-open the same channel or output stream another time, 
typically for re-writing the same data.
+         */
+        if (factory.canOpen()) {
+            addView(ChannelFactory.class, factory);
+        }
+        return asDataOutput;
+    }
+
     /**
      * Adds the given view in the cache together with information about its 
dependency.
      * For example {@link InputStreamReader} is a wrapper for a {@link 
InputStream}: read operations
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java
index 3743611b11..5e253e7617 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/StoreTest.java
@@ -72,7 +72,7 @@ public final strictfp class StoreTest extends TestCase {
      */
     @Test
     public void testMetadata() throws DataStoreException {
-        try (Store store = new Store(null, testData())) {
+        try (Store store = new Store(null, testData(), true)) {
             assertEquals("grid", store.getIdentifier().get().toString());
             final Metadata metadata = store.getMetadata();
             /*
@@ -105,7 +105,7 @@ public final strictfp class StoreTest extends TestCase {
      */
     @Test
     public void testRead() throws DataStoreException {
-        try (Store store = new Store(null, testData())) {
+        try (Store store = new Store(null, testData(), true)) {
             final List<Category> categories = 
getSingleton(store.getSampleDimensions()).getCategories();
             assertEquals(2, categories.size());
             assertEquals(   -2, 
categories.get(0).getSampleRange().getMinDouble(), 1);
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/WritableStoreTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/WritableStoreTest.java
index c36ff2a82f..36e68ebd66 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/WritableStoreTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ascii/WritableStoreTest.java
@@ -18,19 +18,22 @@ package org.apache.sis.internal.storage.ascii;
 
 import java.nio.file.Path;
 import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.nio.charset.StandardCharsets;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.awt.image.BufferedImage;
 import java.awt.image.DataBufferByte;
-import java.nio.file.StandardOpenOption;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.coverage.grid.GridCoverageBuilder;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.ResourceAlreadyExistsException;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.setup.OptionKey;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.referencing.crs.HardCodedCRS;
-import org.apache.sis.storage.ResourceAlreadyExistsException;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -63,11 +66,10 @@ public final strictfp class WritableStoreTest extends 
TestCase {
     }
 
     /**
-     * Verifies that the content of the given file is equal to the expected 
values
-     * for a coverage created by {@link 
#createTestCoverage(CoordinateReferenceSystem)}.
+     * Returns the expected ASCII Grid lines for the coverage created by 
{@link #createTestCoverage()}.
      */
-    private static void verifyContent(final Path file) throws IOException {
-        assertArrayEquals(new String[] {
+    private static String[] getExpectedLines() {
+        return new String[] {
             "NCOLS           4",
             "NROWS           3",
             "XLLCORNER    20.0",
@@ -78,7 +80,15 @@ public final strictfp class WritableStoreTest extends 
TestCase {
             "6 9 12 15",
             "18 21 24 27",
             "30 33 36 39",
-        }, Files.readAllLines(file).toArray());
+        };
+    }
+
+    /**
+     * Verifies that the content of the given file is equal to the expected 
values
+     * for a coverage created by {@link #createTestCoverage()}.
+     */
+    private static void verifyContent(final Path file) throws IOException {
+        assertArrayEquals(getExpectedLines(), 
Files.readAllLines(file).toArray());
         assertArrayEquals(new String[] {
             "GEOGCS[\"WGS 84\",",
             "  DATUM[\"World Geodetic System 1984\",",
@@ -103,10 +113,10 @@ public final strictfp class WritableStoreTest extends 
TestCase {
      * Tests writing an ASCII Grid in a temporary file.
      *
      * @throws IOException if the temporary file can not be created.
-     * @throws DataStoreException if an error occurred while reading the file.
+     * @throws DataStoreException if an error occurred while writing the file.
      */
     @Test
-    public void testWriteFile() throws IOException, DataStoreException {
+    public void testWriteInFile() throws IOException, DataStoreException {
         final GridCoverage coverage = createTestCoverage(HardCodedCRS.WGS84);
         final Path file = Files.createTempFile(null, ".asc");
         try {
@@ -143,4 +153,21 @@ public final strictfp class WritableStoreTest extends 
TestCase {
             Files.delete(file);
         }
     }
+
+    /**
+     * Tests writing an ASCII Grid in an in-memory buffer. The PRJ files can 
not be created in this test,
+     * which force us to use a null CRS for avoiding {@link 
java.net.UnknownServiceException} to be thrown.
+     *
+     * @throws DataStoreException if an error occurred while writing the file.
+     */
+    @Test
+    public void testWriteInMemory() throws DataStoreException {
+        final GridCoverage coverage = createTestCoverage(null);
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (WritableStore store = new WritableStore(null, new 
StorageConnector(out))) {
+            store.write(coverage);
+        }
+        final String text = new String(out.toByteArray(), 
StandardCharsets.US_ASCII).trim();
+        assertArrayEquals(getExpectedLines(), CharSequences.splitOnEOL(text));
+    }
 }

Reply via email to