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)); + } }