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 <[email protected]>
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));
+ }
}