You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/12/03 22:44:09 UTC
[sis] 02/02: GPX data store should implement `WritableFeatureSet`.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 138a82a9e84a52c093d5b0314773427f68edea7c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Dec 3 23:43:15 2022 +0100
GPX data store should implement `WritableFeatureSet`.
https://issues.apache.org/jira/browse/SIS-411
---
.../java/org/apache/sis/test/MetadataAssert.java | 4 +-
.../apache/sis/test/xml/DocumentComparator.java | 7 +-
.../java/org/apache/sis/test/xml/package-info.java | 2 +-
.../java/org/apache/sis/internal/jdk9/JDK9.java | 13 +
.../sis/storage/IllegalFeatureTypeException.java | 2 +-
.../apache/sis/internal/storage/gpx/Reader.java | 2 +-
.../org/apache/sis/internal/storage/gpx/Store.java | 103 +++++++-
.../apache/sis/internal/storage/gpx/Updater.java | 89 +++++++
.../apache/sis/internal/storage/gpx/Writer.java | 12 +-
.../storage/xml/stream/RewriteOnUpdate.java | 283 +++++++++++++++++++++
.../internal/storage/xml/stream/StaxDataStore.java | 83 +++---
.../storage/xml/stream/StaxStreamWriter.java | 12 +-
.../internal/storage/xml/stream/package-info.java | 2 +-
.../sis/internal/storage/gpx/UpdaterTest.java | 179 +++++++++++++
.../org/apache/sis/test/suite/GPXTestSuite.java | 3 +-
15 files changed, 737 insertions(+), 59 deletions(-)
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
index 703fb6f0e8..0081be869e 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
@@ -134,8 +134,8 @@ public strictfp class MetadataAssert extends Assert {
*
* <ul>
* <li>{@link org.w3c.dom.Node}: used directly without further processing.</li>
- * <li>{@link java.io.File}, {@link java.net.URL} or {@link java.net.URI}: the
- * stream is opened and parsed as a XML document.</li>
+ * <li>{@link java.nio.file.Path}, {@link java.io.File}, {@link java.net.URL} or {@link java.net.URI}:
+ * the stream is opened and parsed as a XML document.</li>
* <li>{@link String}: The string content is parsed directly as a XML document.</li>
* </ul>
*
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
index 908b307cc8..d8388b9870 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/DocumentComparator.java
@@ -22,6 +22,8 @@ import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.net.URI;
import java.net.URL;
import java.io.File;
@@ -71,7 +73,7 @@ import static org.apache.sis.util.CharSequences.trimWhitespaces;
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @author Guilhem Legal (Geomatys)
- * @version 1.0
+ * @version 1.3
*
* @see TestCase
* @see org.apache.sis.test.MetadataAssert#assertXmlEquals(Object, Object, String[])
@@ -190,7 +192,7 @@ public strictfp class DocumentComparator {
*
* <ul>
* <li>{@link Node}; used directly without further processing.</li>
- * <li>{@link File}, {@link URL} or {@link URI}: the stream is opened and parsed as a XML document.</li>
+ * <li>{@link Path}, {@link File}, {@link URL} or {@link URI}: the stream is opened and parsed as a XML document.</li>
* <li>{@link String}: The string content is parsed directly as a XML document.</li>
* </ul>
*
@@ -248,6 +250,7 @@ public strictfp class DocumentComparator {
if (input instanceof File) return new FileInputStream((File) input);
if (input instanceof URI) return ((URI) input).toURL().openStream();
if (input instanceof URL) return ((URL) input).openStream();
+ if (input instanceof Path) return Files.newInputStream((Path) input);
if (input instanceof String) return new ByteArrayInputStream(input.toString().getBytes("UTF-8"));
throw new IOException("Cannot handle input type: " + (input != null ? input.getClass() : input));
}
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/package-info.java b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/package-info.java
index da318ef7df..e4405984f3 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/xml/package-info.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/xml/package-info.java
@@ -25,7 +25,7 @@
* in any future version without notice.</p>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
* @since 1.0
* @module
*/
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
index ec1de1d236..047b6f326b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.internal.jdk9;
+import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
@@ -23,6 +24,8 @@ import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
import java.util.List;
@@ -32,6 +35,7 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
+import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.apache.sis.internal.util.CollectionsExt;
@@ -350,4 +354,13 @@ public final class JDK9 {
public static <T> List<T> toList(final Stream<T> s) {
return (List<T>) UnmodifiableArrayList.wrap(s.toArray());
}
+
+ /**
+ * Placeholder for {@link Files#readString(Path)}.
+ */
+ public static String readString(final Path path) throws IOException {
+ final StringJoiner j = new StringJoiner("\n");
+ Files.readAllLines(path).forEach(j::add);
+ return j.toString();
+ }
}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalFeatureTypeException.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalFeatureTypeException.java
index 809f6b2e79..4e9352ca54 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalFeatureTypeException.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalFeatureTypeException.java
@@ -69,7 +69,7 @@ public class IllegalFeatureTypeException extends DataStoreException {
/**
* Creates an exception with a default message in the given locale.
*
- * @param locale the message locale.
+ * @param locale the locale for the message, or {@code null} for the default locale.
* @param format short name of the format that do not accept the given feature type.
* @param dataType name of the feature type that cannot be accepted by the data store.
*/
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Reader.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Reader.java
index ebf94fb328..09f7f574db 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Reader.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Reader.java
@@ -100,7 +100,7 @@ final class Reader extends StaxStreamReader {
* @throws IOException if an error occurred while preparing the input stream.
* @throws Exception if another kind of error occurred while closing a previous stream.
*/
- public Reader(final Store owner) throws Exception {
+ Reader(final Store owner) throws Exception {
super(owner);
}
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
index 2e353f687e..5d14b374ef 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
@@ -17,8 +17,12 @@
package org.apache.sis.internal.storage.gpx;
import java.util.Optional;
+import java.util.Iterator;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
+import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import org.opengis.util.NameFactory;
@@ -26,12 +30,13 @@ import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.Metadata;
import org.opengis.metadata.distribution.Format;
-import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.WritableFeatureSet;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.ConcurrentReadException;
import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.storage.IllegalFeatureTypeException;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.storage.StoreUtilities;
import org.apache.sis.internal.storage.xml.stream.StaxDataStore;
@@ -52,6 +57,8 @@ import org.opengis.feature.FeatureType;
/**
* A data store backed by GPX files.
+ * This store does not cache the feature instances.
+ * Any new {@linkplain #features(boolean) request for features} will re-read from the file.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
@@ -59,7 +66,7 @@ import org.opengis.feature.FeatureType;
* @since 0.8
* @module
*/
-public final class Store extends StaxDataStore implements FeatureSet {
+public final class Store extends StaxDataStore implements WritableFeatureSet {
/**
* Version of the GPX file, or {@code null} if unknown.
*/
@@ -73,6 +80,7 @@ public final class Store extends StaxDataStore implements FeatureSet {
/**
* If a reader has been created for parsing the {@linkplain #metadata} and has not yet been used
* for iterating over the features, that reader. Otherwise {@code null}.
+ * Used for continuing XML parsing after metadata header instead of closing and reopening the file.
*/
private Reader reader;
@@ -208,8 +216,26 @@ public final class Store extends StaxDataStore implements FeatureSet {
return types.names.get(this, name);
}
+ /**
+ * Verifies the type of feature instances in this feature set.
+ * This method does nothing if the specified type is equal to {@link #getType()},
+ * or throws {@link IllegalFeatureTypeException} otherwise.
+ *
+ * @param newType new feature type definition (not {@code null}).
+ * @throws DataStoreException if the given type is not compatible with the types supported by the store.
+ */
+ @Override
+ public void updateType(final FeatureType newType) throws DataStoreException {
+ if (!newType.equals(getType())) {
+ throw new IllegalFeatureTypeException(getLocale(), StoreProvider.NAME, newType.getName());
+ }
+ }
+
/**
* Returns the stream of features.
+ * This store does not cache the features. Any new iteration over features will re-read from the file.
+ * The XML file is kept open until the feature stream is closed;
+ * callers should not modify the file while an iteration is in progress.
*
* @param parallel ignored in current implementation.
* @return a stream over all features in the XML file.
@@ -233,6 +259,68 @@ public final class Store extends StaxDataStore implements FeatureSet {
return features.onClose(r);
}
+ /**
+ * Appends new feature instances in this {@code FeatureSet}.
+ * Any feature already present in this {@link FeatureSet} will remain unmodified.
+ *
+ * @param features feature instances to append in this {@code FeatureSet}.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ @Override
+ public synchronized void add(final Iterator<? extends Feature> features) throws DataStoreException {
+ try (Updater updater = updater()) {
+ updater.add(features);
+ updater.flush();
+ }
+ }
+
+ /**
+ * Removes all feature instances from this {@code FeatureSet} which matches the given predicate.
+ *
+ * @param filter a predicate which returns {@code true} for feature instances to be removed.
+ * @return {@code true} if any elements were removed.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ @Override
+ public synchronized boolean removeIf(final Predicate<? super Feature> filter) throws DataStoreException {
+ try (Updater updater = updater()) {
+ return updater.removeIf(filter);
+ }
+ }
+
+ /**
+ * Updates all feature instances from this {@code FeatureSet} which match the given predicate.
+ * If the given operator returns {@code null}, then the filtered feature is removed.
+ *
+ * @param filter a predicate which returns {@code true} for feature instances to be updated.
+ * @param replacement operation called for each matching {@link Feature} instance. May return {@code null}.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ @Override
+ public synchronized void replaceIf(final Predicate<? super Feature> filter, final UnaryOperator<Feature> replacement)
+ throws DataStoreException
+ {
+ try (Updater updater = updater()) {
+ updater.replaceIf(filter, replacement);
+ updater.flush();
+ }
+ }
+
+ /**
+ * Returns the helper object to use for updating the GPX file.
+ *
+ * @todo In current version, we flush the updater after each write operation.
+ * In a future version, we should keep it in a private field and flush
+ * only after some delay, on close, or before a read operation.
+ */
+ private Updater updater() throws DataStoreException {
+ try {
+ return new Updater(this, getSpecifiedPath());
+ } catch (IOException e) {
+ throw new DataStoreException(e);
+ }
+ }
+
/**
* Replaces the content of this GPX file by the given metadata and features.
*
@@ -240,13 +328,18 @@ public final class Store extends StaxDataStore implements FeatureSet {
* @param features the features to write, or {@code null} if none.
* @throws ConcurrentReadException if the {@code features} stream was provided by this data store.
* @throws DataStoreException if an error occurred while writing the data.
+ *
+ * @deprecated To be replaced by {@link #add(Iterator)}, after we resolved how to specify metadata.
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-411">SIS-411</a>
*/
+ @Deprecated
public synchronized void write(final Metadata metadata, final Stream<? extends Feature> features) throws DataStoreException {
try {
/*
* If we created a reader for reading metadata, we need to close that reader now otherwise the call
- * to 'new Writer(…)' will fail. Note that if that reader was in use by someone else, the 'reader'
- * field would be null and the 'new Writer(…)' call should detect that a reader is in use somewhere.
+ * to `new Writer(…)` will fail. Note that if that reader was in use by someone else, the `reader`
+ * field would be null and the `new Writer(…)` call should detect that a reader is in use somewhere.
*/
final Reader r = reader;
if (r != null) {
@@ -256,7 +349,7 @@ public final class Store extends StaxDataStore implements FeatureSet {
/*
* Get the writer if no read or other write operation is in progress, then write the data.
*/
- try (Writer writer = new Writer(this, org.apache.sis.internal.storage.gpx.Metadata.castOrCopy(metadata, locale))) {
+ try (Writer writer = new Writer(this, org.apache.sis.internal.storage.gpx.Metadata.castOrCopy(metadata, locale), null)) {
writer.writeStartDocument();
if (features != null) {
features.forEachOrdered(writer);
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Updater.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Updater.java
new file mode 100644
index 0000000000..8a739175ff
--- /dev/null
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Updater.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.gpx;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+import org.apache.sis.internal.storage.xml.stream.RewriteOnUpdate;
+import org.apache.sis.internal.storage.xml.stream.StaxStreamWriter;
+import org.apache.sis.storage.DataStoreException;
+import org.opengis.feature.Feature;
+
+
+/**
+ * Updates the content of a GPX file by rewriting it.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since 1.3
+ * @module
+ */
+final class Updater extends RewriteOnUpdate {
+ /**
+ * The metadata to write.
+ */
+ private Metadata metadata;
+
+ /**
+ * Creates an updater for the given source of features.
+ *
+ * @param source the set of features to update.
+ * @param location the main file, or {@code null} if unknown.
+ * @throws IOException if an error occurred while determining whether the file is empty.
+ */
+ Updater(final Store source, final Path location) throws IOException {
+ super(source, location);
+ }
+
+ /**
+ * Returns the stream of features to copy.
+ *
+ * @return all features contained in the dataset.
+ * @throws DataStoreException if an error occurred while fetching the features.
+ */
+ @Override
+ protected Stream<? extends Feature> features() throws DataStoreException {
+ metadata = Metadata.castOrCopy(source.getMetadata(), getLocale());
+ return super.features();
+ }
+
+ /**
+ * Creates an initially empty temporary file.
+ *
+ * @return the temporary file.
+ * @throws IOException if an error occurred while creating the temporary file.
+ */
+ @Override
+ protected Path createTemporaryFile() throws IOException {
+ return Files.createTempFile(StoreProvider.NAME, ".xml");
+ }
+
+ /**
+ * Creates a new GPX writer for an output in the specified file.
+ *
+ * @param temporary the temporary stream where to write, or {@code null} for writing directly in the store file.
+ * @return the writer where to copy updated features.
+ * @throws Exception if an error occurred while creating the writer.
+ */
+ @Override
+ protected StaxStreamWriter createWriter(OutputStream temporary) throws Exception {
+ return new Writer((Store) source, metadata, temporary);
+ }
+}
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Writer.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Writer.java
index 840873ab8b..97d33b705a 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Writer.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Writer.java
@@ -17,6 +17,7 @@
package org.apache.sis.internal.storage.gpx;
import java.io.IOException;
+import java.io.OutputStream;
import java.util.Collection;
import javax.xml.stream.XMLStreamException;
import javax.xml.bind.JAXBException;
@@ -39,7 +40,7 @@ import org.opengis.feature.FeatureType;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
* @since 0.8
* @module
*/
@@ -57,16 +58,17 @@ final class Writer extends StaxStreamWriter {
/**
* Creates a new GPX writer for the given data store.
*
- * @param owner the data store for which this writer is created.
- * @param metadata the metadata to write, or {@code null} if none.
+ * @param owner the data store for which this writer is created.
+ * @param metadata the metadata to write, or {@code null} if none.
+ * @param temporary the temporary stream where to write, or {@code null} for the main storage.
* @throws DataStoreException if the output type is not recognized or the data store is closed.
* @throws XMLStreamException if an error occurred while opening the XML file.
* @throws IOException if an error occurred while preparing the output stream.
*/
- public Writer(final Store owner, final Metadata metadata)
+ Writer(final Store owner, final Metadata metadata, final OutputStream temporary)
throws DataStoreException, XMLStreamException, IOException
{
- super(owner);
+ super(owner, temporary);
this.metadata = metadata;
final Version ver = owner.version;
if (ver != null && ver.compareTo(StoreProvider.V1_0, 2) <= 0) {
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/RewriteOnUpdate.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/RewriteOnUpdate.java
new file mode 100644
index 0000000000..5377bda50b
--- /dev/null
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/RewriteOnUpdate.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.xml.stream;
+
+import java.util.Locale;
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.StreamSupport;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.ReadOnlyStorageException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
+
+/**
+ * Helper class for updating an existing XML file, with no feature type change permitted.
+ * The implementation strategy is to rewrite fully the updated features in a temporary file,
+ * then replaces the source file by the temporary file when ready.
+ *
+ * <p>The {@link #flush()} method should always been invoked before a {@code RewriteOnUpdate}
+ * reference is lost, otherwise data may be lost.</p>
+ *
+ * <h2>Multi-threading</h2>
+ * This class is not synchronized for multi-threading. Synchronization is caller's responsibility,
+ * because the caller usually needs to take in account other data store operations such as reads.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since 1.3
+ * @module
+ */
+public abstract class RewriteOnUpdate implements AutoCloseable {
+ /**
+ * The set of features to update. This is the set specified at construction time.
+ */
+ protected final FeatureSet source;
+
+ /**
+ * The main file, or {@code null} if unknown.
+ */
+ private final Path location;
+
+ /**
+ * Whether the store is initially empty.
+ * It may be the underlying file does not exist or has a length of zero.
+ */
+ private boolean isSourceEmpty;
+
+ /**
+ * The features to write, fetched when first needed.
+ *
+ * @see #filtered()
+ */
+ private Stream<? extends Feature> filtered;
+
+ /**
+ * Creates an updater for the given source of features.
+ *
+ * @param source the set of features to update.
+ * @param location the main file, or {@code null} if unknown.
+ * @throws IOException if an error occurred while determining whether the file is empty.
+ */
+ public RewriteOnUpdate(final FeatureSet source, final Path location) throws IOException {
+ this.source = source;
+ this.location = location;
+ isSourceEmpty = (location == null) || Files.notExists(location) || Files.size(location) == 0;
+ }
+
+ /**
+ * Returns the locale to use for locale-sensitive data, or {@code null} if unspecified.
+ * This is <strong>not</strong> for logging or warning messages.
+ *
+ * @return the data locale, or {@code null}.
+ */
+ protected final Locale getLocale() {
+ return (source instanceof StaxDataStore) ? ((StaxDataStore) source).locale : null;
+ }
+
+ /**
+ * Returns {@code true} if there is currently no data.
+ */
+ private boolean isEmpty() throws ReadOnlyStorageException {
+ if (isSourceEmpty) {
+ return filtered == null;
+ } else if (location != null) {
+ return false;
+ } else {
+ throw new ReadOnlyStorageException();
+ }
+ }
+
+ /**
+ * Returns the features to write.
+ *
+ * @throws DataStoreException if the feature stream cannot be obtained.
+ */
+ private Stream<? extends Feature> filtered() throws DataStoreException {
+ if (filtered == null) {
+ filtered = features();
+ }
+ return filtered;
+ }
+
+ /**
+ * Returns the stream of features to copy.
+ * The default implementation delegates to {@link FeatureSet#features(boolean)}.
+ *
+ * @return all features contained in the dataset.
+ * @throws DataStoreException if an error occurred while fetching the features.
+ */
+ protected Stream<? extends Feature> features() throws DataStoreException {
+ return source.features(false);
+ }
+
+ /**
+ * Appends new feature instances in the {@code FeatureSet}.
+ * Any feature already present in the {@link FeatureSet} will remain unmodified.
+ *
+ * @param features feature instances to append in the {@code FeatureSet}.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ public void add(final Iterator<? extends Feature> features) throws DataStoreException {
+ ArgumentChecks.ensureNonNull("features", features);
+ final Stream<? extends Feature> toAdd = StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(features, Spliterator.ORDERED), false);
+ if (isEmpty()) {
+ filtered = toAdd;
+ } else {
+ filtered = Stream.concat(filtered(), toAdd);
+ }
+ }
+
+ /**
+ * Removes all feature instances from the {@code FeatureSet} which matches the given predicate.
+ *
+ * @param filter a predicate which returns {@code true} for feature instances to be removed.
+ * @return {@code true} if any elements were removed.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ public boolean removeIf(final Predicate<? super Feature> filter) throws DataStoreException {
+ ArgumentChecks.ensureNonNull("filter", filter);
+ if (isEmpty()) {
+ return false;
+ }
+ filtered = filtered().filter((feature) -> {
+ boolean r = filter.test(feature);
+ if (r) modified = true;
+ return !r;
+ });
+ modified = false;
+ flush(); // Need immediate execution for getting the boolean value.
+ return modified;
+ }
+
+ /**
+ * A flag telling whether {@link #removeIf(Predicate)} removed at least one feature.
+ */
+ private boolean modified;
+
+ /**
+ * Updates all feature instances from the {@code FeatureSet} which match the given predicate.
+ * If the given operator returns {@code null}, then the filtered feature is removed.
+ *
+ * @param filter a predicate which returns {@code true} for feature instances to be updated.
+ * @param updater operation called for each matching {@link Feature} instance. May return {@code null}.
+ * @throws DataStoreException if the feature stream cannot be obtained or updated.
+ */
+ public void replaceIf(final Predicate<? super Feature> filter, final UnaryOperator<Feature> updater) throws DataStoreException {
+ ArgumentChecks.ensureNonNull("filter", filter);
+ ArgumentChecks.ensureNonNull("updater", updater);
+ if (!isEmpty()) {
+ filtered = filtered().map((feature) -> (feature != null) && filter.test(feature) ? updater.apply(feature) : feature);
+ }
+ }
+
+ /**
+ * Creates an initially empty temporary file.
+ *
+ * @return the temporary file.
+ * @throws IOException if an error occurred while creating the temporary file.
+ */
+ protected abstract Path createTemporaryFile() throws IOException;
+
+ /**
+ * Creates a new XML document writer for an output in the specified temporary file.
+ * Caller is responsible for closing the writer.
+ *
+ * @param temporary the temporary stream where to write, or {@code null} for writing directly in the store file.
+ * @return the writer where to copy updated features.
+ * @throws Exception if an error occurred while creating the writer.
+ * May be {@link DataStoreException}, {@link IOException}, {@link RuntimeException}, <i>etc.</i>
+ */
+ protected abstract StaxStreamWriter createWriter(OutputStream temporary) throws Exception;
+
+ /**
+ * Writes immediately all feature instances.
+ * This method does nothing if there is no data to write.
+ *
+ * @throws DataStoreException if an error occurred.
+ */
+ public void flush() throws DataStoreException {
+ try (Stream<? extends Feature> content = filtered) {
+ if (content != null) {
+ filtered = null;
+ OutputStream temporary = null;
+ Path target = isSourceEmpty ? null : createTemporaryFile();
+ try {
+ if (target != null) {
+ temporary = Files.newOutputStream(target);
+ }
+ try (StaxStreamWriter writer = createWriter(temporary)) {
+ temporary = null; // Stream will be closed by writer.
+ isSourceEmpty = false;
+ writer.writeStartDocument();
+ content.sequential().forEachOrdered(writer);
+ writer.writeEndDocument();
+ }
+ if (target != null) {
+ Files.move(target, location, StandardCopyOption.REPLACE_EXISTING);
+ target = null;
+ }
+ } finally {
+ if (temporary != null) temporary.close();
+ if (target != null) Files.delete(target); // Delete the temporary file if an error occurred.
+ }
+ }
+ } catch (DataStoreException e) {
+ throw e;
+ } catch (BackingStoreException e) {
+ final Throwable cause = e.getCause();
+ if (cause instanceof DataStoreException) {
+ throw (DataStoreException) cause;
+ }
+ throw new DataStoreException(e.getLocalizedMessage(), cause);
+ } catch (Exception e) {
+ if (e instanceof UncheckedIOException) {
+ e = ((UncheckedIOException) e).getCause();
+ }
+ throw new DataStoreException(e);
+ }
+ }
+
+ /**
+ * Releases resources used by this updater. If {@link #flush()} has not been invoked, data may be lost.
+ * This method is useful in try-with-resource in case something fails before {@link #flush()} invocation.
+ */
+ @Override
+ public void close() {
+ final Stream<? extends Feature> content = filtered;
+ if (content != null) {
+ filtered = null;
+ content.close();
+ }
+ }
+}
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
index 55cd4d4b32..3901ea351a 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
@@ -24,6 +24,7 @@ import java.util.logging.Filter;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.charset.Charset;
import java.nio.file.StandardOpenOption;
@@ -60,7 +61,7 @@ import org.apache.sis.storage.UnsupportedStorageException;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
* @since 0.8
* @module
*/
@@ -395,7 +396,7 @@ public abstract class StaxDataStore extends URIDataStore {
/**
* Returns the factory for StAX writers. The same instance is returned for all {@code StaxDataStore} lifetime.
*
- * <p>This method is indirectly invoked by {@link #createWriter(StaxStreamWriter)},
+ * <p>This method is indirectly invoked by {@link #createWriter(StaxStreamWriter, Object)},
* through a call to {@link OutputType#create(StaxDataStore, Object)}.</p>
*/
final XMLOutputFactory outputFactory() {
@@ -515,55 +516,65 @@ public abstract class StaxDataStore extends URIDataStore {
* whether this method will succeed in creating a new writer depends on the storage type
* (e.g. file or output stream).
*
- * @param target the writer which will store the {@code XMLStreamWriter} reference.
+ * @param target the writer which will store the {@code XMLStreamWriter} reference.
+ * @param temporary the temporary stream where to write, or {@code null} for the main storage.
* @return a new writer for writing the XML data.
* @throws DataStoreException if the output type is not recognized or the data store is closed.
* @throws XMLStreamException if an error occurred while opening the XML file.
* @throws IOException if an error occurred while preparing the output stream.
*/
- final synchronized XMLStreamWriter createWriter(final StaxStreamWriter target)
+ final synchronized XMLStreamWriter createWriter(final StaxStreamWriter target, final OutputStream temporary)
throws DataStoreException, XMLStreamException, IOException
{
- Object outputOrFile = storage;
- if (outputOrFile == null) {
- throw new DataStoreClosedException(getLocale(), getFormatName(), StandardOpenOption.WRITE);
- }
- switch (state) {
- default: throw new AssertionError(state);
- case READING: throw new ConcurrentReadException (getLocale(), getDisplayName());
- case WRITING: throw new ConcurrentWriteException(getLocale(), getDisplayName());
- case START: break; // Stream already at the data start; nothing to do.
- case FINISHED: {
- if (reset()) break;
- throw new ForwardOnlyStorageException(getLocale(), getDisplayName(), StandardOpenOption.WRITE);
+ AutoCloseable output;
+ Object outputOrFile;
+ OutputType outputType;
+ if (temporary == null) {
+ output = stream;
+ outputOrFile = storage;
+ outputType = storageToWriter;
+ if (outputOrFile == null) {
+ throw new DataStoreClosedException(getLocale(), getFormatName(), StandardOpenOption.WRITE);
}
- }
- /*
- * If the storage given by the user was not one of OutputStream, Writer or other type recognized
- * by OutputType, then maybe that storage was a Path, File or URL, in which case the constructor
- * should have opened an InputStream (not an OutputStream) for it. In some cases (e.g. reading a
- * channel opened on a file), the input stream can be converted to an output stream.
- */
- AutoCloseable output = stream;
- OutputType type = storageToWriter;
- if (type == null) {
- type = OutputType.STREAM;
- output = IOUtilities.toOutputStream(output);
- if (output == null) {
- throw new UnsupportedStorageException(getLocale(), getFormatName(), storage, StandardOpenOption.WRITE);
+ switch (state) {
+ default: throw new AssertionError(state);
+ case READING: throw new ConcurrentReadException (getLocale(), getDisplayName());
+ case WRITING: throw new ConcurrentWriteException(getLocale(), getDisplayName());
+ case START: break; // Stream already at the data start; nothing to do.
+ case FINISHED: {
+ if (reset()) break;
+ throw new ForwardOnlyStorageException(getLocale(), getDisplayName(), StandardOpenOption.WRITE);
+ }
}
- outputOrFile = output;
- if (output != stream) {
- stream = output;
- mark();
+ /*
+ * If the storage given by the user was not one of OutputStream, Writer or other type recognized
+ * by OutputType, then maybe that storage was a Path, File or URL, in which case the constructor
+ * should have opened an InputStream (not an OutputStream) for it. In some cases (e.g. reading a
+ * channel opened on a file), the input stream can be converted to an output stream.
+ */
+ if (outputType == null) {
+ outputType = OutputType.STREAM;
+ outputOrFile = output = IOUtilities.toOutputStream(output);
+ if (output == null) {
+ throw new UnsupportedStorageException(getLocale(), getFormatName(), outputOrFile, StandardOpenOption.WRITE);
+ }
+ if (output != stream) {
+ stream = output;
+ mark();
+ }
}
+ } else {
+ outputType = OutputType.STREAM;
+ outputOrFile = output = temporary;
}
- XMLStreamWriter writer = type.create(this, outputOrFile);
+ XMLStreamWriter writer = outputType.create(this, outputOrFile);
if (indentation >= 0) {
writer = new FormattedWriter(writer, indentation);
}
target.stream = output;
- state = WRITING;
+ if (temporary == null) {
+ state = WRITING;
+ }
return writer;
}
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxStreamWriter.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxStreamWriter.java
index 701812d20b..ece5ea44a5 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxStreamWriter.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxStreamWriter.java
@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.Date;
import java.util.function.Consumer;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import javax.xml.namespace.QName;
@@ -85,7 +86,7 @@ import org.opengis.feature.Feature;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
* @since 0.8
* @module
*/
@@ -106,15 +107,18 @@ public abstract class StaxStreamWriter extends StaxStreamIO implements Consumer<
/**
* Creates a new XML writer for the given data store.
*
- * @param owner the data store for which this writer is created.
+ * @param owner the data store for which this writer is created.
+ * @param temporary the temporary stream where to write, or {@code null} for the main storage.
* @throws DataStoreException if the output type is not recognized or the data store is closed.
* @throws XMLStreamException if an error occurred while opening the XML file.
* @throws IOException if an error occurred while preparing the output stream.
*/
@SuppressWarnings("ThisEscapedInObjectConstruction")
- protected StaxStreamWriter(final StaxDataStore owner) throws DataStoreException, XMLStreamException, IOException {
+ protected StaxStreamWriter(final StaxDataStore owner, final OutputStream temporary)
+ throws DataStoreException, XMLStreamException, IOException
+ {
super(owner);
- writer = owner.createWriter(this); // Okay because will not store the 'this' reference.
+ writer = owner.createWriter(this, temporary); // Okay because will not store the `this` reference.
}
/**
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/package-info.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/package-info.java
index ac07cc8613..20f295c0f6 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/package-info.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/package-info.java
@@ -24,7 +24,7 @@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
* @since 0.8
* @module
*/
diff --git a/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/UpdaterTest.java b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/UpdaterTest.java
new file mode 100644
index 0000000000..b951f5973e
--- /dev/null
+++ b/storage/sis-xmlstore/src/test/java/org/apache/sis/internal/storage/gpx/UpdaterTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.gpx;
+
+import java.util.Arrays;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.time.Instant;
+import com.esri.core.geometry.Point;
+import java.io.InputStream;
+import java.nio.file.StandardCopyOption;
+import org.apache.sis.setup.GeometryLibrary;
+import org.apache.sis.setup.OptionKey;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestCase;
+import org.junit.BeforeClass;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.apache.sis.test.MetadataAssert.*;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+
+
+/**
+ * Tests (indirectly) the {@link Updater} class.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-411">SIS-411</a>
+ *
+ * @since 1.3
+ * @module
+ */
+@DependsOn(WriterTest.class)
+public final strictfp class UpdaterTest extends TestCase {
+ /**
+ * The provider shared by all data stores created in this test class.
+ */
+ private static StoreProvider provider;
+
+ /**
+ * Creates the provider to be shared by all data stores created in this test class.
+ */
+ @BeforeClass
+ public static void createProvider() {
+ provider = new StoreProvider();
+ }
+
+ /**
+ * Disposes the data store provider after all tests have been completed.
+ */
+ @AfterClass
+ public static void disposeProvider() {
+ provider = null;
+ }
+
+ /**
+ * Temporary file where to write the GPX file.
+ */
+ private Path file;
+
+ /**
+ * Creates the temporary file before test execution.
+ *
+ * @throws IOException if the temporary file cannot be created.
+ */
+ @Before
+ public void createTemporaryFile() throws IOException {
+ file = Files.createTempFile("GPX", ".xml");
+ }
+
+ /**
+ * Deletes temporary file after test execution.
+ *
+ * @throws IOException if the temporary file cannot be deleted.
+ */
+ @After
+ public void deleteTemporaryFile() throws IOException {
+ if (file != null) {
+ Files.delete(file);
+ }
+ }
+
+ /**
+ * Creates a new GPX data store which will read and write in a temporary file.
+ */
+ private Store create() throws DataStoreException, IOException {
+ final StorageConnector connector = new StorageConnector(file);
+ connector.setOption(OptionKey.GEOMETRY_LIBRARY, GeometryLibrary.ESRI);
+ connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] {
+ StandardOpenOption.READ, StandardOpenOption.WRITE});
+ return new Store(provider, connector);
+ }
+
+ /**
+ * Tests writing in an initially empty file.
+ *
+ * @throws IOException if an error occurred while creating the temporary file.
+ * @throws DataStoreException if an error occurred while using the GPX store.
+ */
+ @Test
+ public void testWriteEmpty() throws DataStoreException, IOException {
+ try (final Store store = create()) {
+ final Types types = store.types;
+ final Feature point1 = types.wayPoint.newInstance();
+ final Feature point2 = types.wayPoint.newInstance();
+ final Feature point3 = types.wayPoint.newInstance();
+ point1.setPropertyValue("sis:geometry", new Point(15, 10));
+ point2.setPropertyValue("sis:geometry", new Point(25, 20));
+ point3.setPropertyValue("sis:geometry", new Point(35, 30));
+ point1.setPropertyValue("time", Instant.parse("2010-01-10T00:00:00Z"));
+ point3.setPropertyValue("time", Instant.parse("2010-01-30T00:00:00Z"));
+ store.add(Arrays.asList(point1, point2, point3).iterator());
+ }
+ assertXmlEquals(
+ "<gpx xmlns=\"" + Tags.NAMESPACE + "1/1\" version=\"1.1\">\n" +
+ " <wpt lat=\"10.0\" lon=\"15.0\">\n" +
+ " <time>2010-01-10T00:00:00Z</time>\n" +
+ " </wpt>\n" +
+ " <wpt lat=\"20.0\" lon=\"25.0\"/>\n" +
+ " <wpt lat=\"30.0\" lon=\"35.0\">\n" +
+ " <time>2010-01-30T00:00:00Z</time>\n" +
+ " </wpt>\n" +
+ "</gpx>", file, "xmlns:*");
+ }
+
+ /**
+ * Tests an update which requires rewriting the XML file.
+ *
+ * @throws IOException if an error occurred while creating the temporary file.
+ * @throws DataStoreException if an error occurred while using the GPX store.
+ */
+ @Test
+ public void testRewrite() throws DataStoreException, IOException {
+ try (InputStream in = UpdaterTest.class.getResourceAsStream("1.1/waypoint.xml")) {
+ Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
+ }
+ assertTrue(containsLat20());
+ final boolean result;
+ try (final Store store = create()) {
+ result = store.removeIf((feature) -> {
+ Object point = feature.getPropertyValue("sis:geometry");
+ return ((Point) point).getY() == 20;
+ });
+ }
+ assertTrue(result);
+ assertFalse(containsLat20());
+ }
+
+ /**
+ * Returns whether the temporary file contains the {@code lat="20"} string.
+ */
+ private boolean containsLat20() throws IOException {
+ return org.apache.sis.internal.jdk9.JDK9.readString(file).contains("lat=\"20"); // May have trailing ".0".
+ }
+}
diff --git a/storage/sis-xmlstore/src/test/java/org/apache/sis/test/suite/GPXTestSuite.java b/storage/sis-xmlstore/src/test/java/org/apache/sis/test/suite/GPXTestSuite.java
index e3ce9d97c7..f72d8132e3 100644
--- a/storage/sis-xmlstore/src/test/java/org/apache/sis/test/suite/GPXTestSuite.java
+++ b/storage/sis-xmlstore/src/test/java/org/apache/sis/test/suite/GPXTestSuite.java
@@ -28,7 +28,8 @@ import org.junit.BeforeClass;
org.apache.sis.internal.storage.gpx.TypesTest.class,
org.apache.sis.internal.storage.gpx.MetadataTest.class,
org.apache.sis.internal.storage.gpx.ReaderTest.class,
- org.apache.sis.internal.storage.gpx.WriterTest.class
+ org.apache.sis.internal.storage.gpx.WriterTest.class,
+ org.apache.sis.internal.storage.gpx.UpdaterTest.class
})
public final strictfp class GPXTestSuite extends TestSuite {
/**