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 bf68fcef7c Consolidation in the handling of exceptions in SQLStore and
handle one more metadata (date of last update). This is work in preparation for
Geopackage support.
bf68fcef7c is described below
commit bf68fcef7cd396232304fed79da1a175039c9e2d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Aug 27 19:28:33 2024 +0200
Consolidation in the handling of exceptions in SQLStore and handle one more
metadata (date of last update).
This is work in preparation for Geopackage support.
---
.../apache/sis/metadata/sql/privy/SQLBuilder.java | 7 ++
.../sis/metadata/sql/IdentifierGeneratorTest.java | 2 +-
.../org/apache/sis/storage/geotiff/Writer.java | 5 +
.../org/apache/sis/storage/sql/DataAccess.java | 11 +-
.../main/org/apache/sis/storage/sql/SQLStore.java | 66 +++++++++--
.../apache/sis/storage/sql/SQLStoreProvider.java | 6 +-
.../sis/storage/sql/feature/FeatureIterator.java | 9 +-
.../sis/storage/sql/feature/FeatureStream.java | 5 +-
.../sis/storage/sql/feature/InfoStatements.java | 2 +
.../org/apache/sis/storage/sql/feature/Table.java | 3 +-
.../org/apache/sis/util/stream/DeferredStream.java | 21 +++-
.../org/apache/sis/storage/WritableAggregate.java | 2 +-
.../apache/sis/storage/base/MetadataFetcher.java | 126 +++++++++++++++++++--
13 files changed, 221 insertions(+), 44 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
index b341fedbfe..558a287dea 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java
@@ -43,6 +43,13 @@ public class SQLBuilder extends Syntax {
*/
public static final String INSERT = "INSERT INTO ";
+ /**
+ * The {@value} keyword (with a trailing space).
+ * Defined as a convenience for identifying locations in the Java code
+ * where we start to write a SQL statement using a builder.
+ */
+ public static final String DELETE = "DELETE FROM ";
+
/**
* The buffer where the SQL query is created.
*/
diff --git
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/IdentifierGeneratorTest.java
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/IdentifierGeneratorTest.java
index 8f7f0c1a3a..c46acdd0bc 100644
---
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/IdentifierGeneratorTest.java
+++
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/IdentifierGeneratorTest.java
@@ -115,7 +115,7 @@ public final class IdentifierGeneratorTest extends TestCase
{
* Tries to remove a few pre-selected record, then add them again.
*/
private void removeAndAddRecords(final String prefix) throws SQLException {
- assertEquals(5, stmt.executeUpdate("DELETE FROM \"" + TABLE + "\"
WHERE " +
+ assertEquals(5, stmt.executeUpdate(SQLBuilder.DELETE + '"' + TABLE +
"\" WHERE " +
"ID='" + prefix + IdentifierGenerator.SEPARATOR + "4' OR " +
"ID='" + prefix + IdentifierGenerator.SEPARATOR + "12' OR " +
"ID='" + prefix + IdentifierGenerator.SEPARATOR + "32' OR " +
diff --git
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 2f1b915774..3de9db2926 100644
---
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -37,6 +37,7 @@ import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
import javax.measure.IncommensurableException;
import org.opengis.util.FactoryException;
import org.opengis.metadata.Metadata;
+import org.opengis.metadata.citation.CitationDate;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.privy.ImageUtilities;
@@ -367,6 +368,10 @@ final class Writer extends IOBase implements Flushable {
final double[][] statistics = image.statistics(numBands);
final short[][] shortStats = toShorts(statistics, sampleFormat);
final MetadataFetcher<String> mf = new
MetadataFetcher<>(store.dataLocale) {
+ @Override protected boolean accept(final CitationDate info) {
+ return super.accept(info) || creationDate != null; //
Limit to a singleton.
+ }
+
@Override protected String convertDate(final Date date) {
return store.getDateFormat().format(date);
}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
index edc4cbd0d8..e18f8be797 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java
@@ -33,7 +33,6 @@ import org.apache.sis.storage.DataStoreReferencingException;
import org.apache.sis.storage.NoSuchDataException;
import org.apache.sis.storage.sql.feature.Database;
import org.apache.sis.storage.sql.feature.InfoStatements;
-import org.apache.sis.util.Exceptions;
import org.apache.sis.util.resources.Errors;
@@ -248,12 +247,10 @@ public class DataAccess implements AutoCloseable {
crs = spatialInformation().fetchCRS(srid);
} catch (DataStoreContentException e) {
throw new NoSuchDataException(e.getMessage(), e.getCause());
- } catch (DataStoreException e) {
- throw e;
} catch (FactoryException | ParseException e) {
- throw new DataStoreReferencingException(Exceptions.unwrap(e));
+ throw new DataStoreReferencingException(e.getMessage(), e);
} catch (Exception e) {
- throw new DataStoreException(Exceptions.unwrap(e));
+ throw SQLStore.cannotExecute(e);
}
return crs;
}
@@ -285,10 +282,8 @@ public class DataAccess implements AutoCloseable {
final int srid;
try {
srid = spatialInformation().findSRID(crs);
- } catch (DataStoreException e) {
- throw e;
} catch (Exception e) {
- throw new DataStoreException(Exceptions.unwrap(e));
+ throw SQLStore.cannotExecute(e);
}
return srid;
}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
index a44f0cf145..6cc17a02b7 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java
@@ -19,9 +19,11 @@ package org.apache.sis.storage.sql;
import java.util.Locale;
import java.util.Optional;
import java.util.Collection;
+import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import javax.sql.DataSource;
import java.sql.Connection;
+import java.sql.Statement;
import java.sql.DatabaseMetaData;
import java.lang.reflect.Method;
import org.opengis.util.GenericName;
@@ -47,6 +49,7 @@ import org.apache.sis.io.stream.InternalOptionKey;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.CharSequences;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.privy.Strings;
import org.apache.sis.setup.GeometryLibrary;
@@ -346,10 +349,8 @@ public abstract class SQLStore extends DataStore
implements Aggregate {
synchronized (this) {
try (Connection c = source.getConnection()) {
current = model(c);
- } catch (DataStoreException e) {
- throw e;
} catch (Exception e) {
- throw new DataStoreException(Exceptions.unwrap(e));
+ throw cannotExecute(e);
}
}
}
@@ -397,10 +398,8 @@ public abstract class SQLStore extends DataStore
implements Aggregate {
builder.addSpatialRepresentation(SpatialRepresentationType.TEXT_TABLE);
try (Connection c = source.getConnection()) {
model(c).metadata(c.getMetaData(), builder);
- } catch (DataStoreException e) {
- throw e;
} catch (Exception e) {
- throw new DataStoreException(Exceptions.unwrap(e));
+ throw cannotExecute(e);
}
getDataSourceProperty(TITLE_GETTERS).ifPresent(builder::addTitle);
builder.addIdentifier(identifier, MetadataBuilder.Scope.ALL);
@@ -429,7 +428,7 @@ public abstract class SQLStore extends DataStore implements
Aggregate {
} catch (NoSuchMethodException | SecurityException e) {
// Ignore - try the next method.
} catch (ReflectiveOperationException e) {
- throw new DataStoreException(Exceptions.unwrap(e));
+ throw cannotExecute(e);
}
return Optional.empty();
}
@@ -536,6 +535,59 @@ public abstract class SQLStore extends DataStore
implements Aggregate {
}
}
+ /**
+ * Returns the exception to throw for the given cause.
+ * The cause is typically a {@link java.sql.SQLException}, but not
necessarily.
+ * This method provides a central point where we may specialize the type
of the
+ * returned exception depending on the type of the cause.
+ *
+ * @param cause the cause of the error. Cannot be null.
+ * @return the data store exception to throw. May be {@code cause} itself.
+ */
+ static DataStoreException cannotExecute(final Exception cause) {
+ final Exception unwrap = Exceptions.unwrap(cause);
+ if (unwrap instanceof DataStoreException) {
+ return (DataStoreException) unwrap;
+ } else {
+ return new DataStoreException(cause.getMessage(), unwrap);
+ }
+ }
+
+ /**
+ * Executes the {@code "VACUUM"} statement on the database.
+ * This is a non-standard feature that exists on some databases such as
PostgreSQL and SQLite.
+ * This method executes the statement only if the {@linkplain
DatabaseMetaData#getSQLKeywords()
+ * <abbr>SQL</abbr> keywords declared in the database metadata} contains
{@code "VACUUM"}, ignoring case.
+ *
+ * <p>This is a potentially costly operation which should be rarely
executed.
+ * A typical use case if to compress a newly created Geopackage file
(which are SQLite database).</p>
+ *
+ * @throws DataStoreException if an error occurred while executing the
vacuum.
+ *
+ * @since 1.5
+ */
+ public void vacuum() throws DataStoreException {
+ try (Connection c = source.getConnection()) {
+ var keywords = (String[])
CharSequences.split(c.getMetaData().getSQLKeywords(), ',');
+ if (ArraysExt.containsIgnoreCase(keywords, "VACUUM")) {
+ Lock lock = null;
+ if (transactionLocks != null) {
+ lock = transactionLocks.writeLock();
+ lock.lock();
+ }
+ try (Statement stmt = c.createStatement()) {
+ stmt.executeUpdate("VACUUM");
+ } finally {
+ if (lock != null) {
+ lock.unlock();
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw cannotExecute(e);
+ }
+ }
+
/**
* Clears the cache so that next operations will reload all needed
information from the database.
* This method can be invoked when the database content has been modified
by a process other than
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStoreProvider.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStoreProvider.java
index 556acfe88f..c4495ed214 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStoreProvider.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStoreProvider.java
@@ -41,6 +41,7 @@ import org.apache.sis.storage.base.Capability;
import org.apache.sis.storage.base.StoreMetadata;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.util.Exceptions;
import org.apache.sis.util.UnconvertibleObjectException;
import static org.apache.sis.storage.sql.feature.Database.WILDCARD;
@@ -194,9 +195,10 @@ public class SQLStoreProvider extends DataStoreProvider {
return ProbeResult.SUPPORTED;
} catch (SQLException e) {
final String state = e.getSQLState();
- if (!"08001".equals(state) || !"3D000".equals(state)) {
- throw new CanNotProbeException(this, connector, e);
+ if (!("08001".equals(state) || "3D000".equals(state))) {
+ throw new CanNotProbeException(this, connector,
Exceptions.unwrap(e));
}
+ // SQL-client unable to establish SQL-connection, or invalid
catalog name.
}
}
return ProbeResult.UNSUPPORTED_STORAGE;
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 01b605c8da..ba0836a33d 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@ -27,7 +27,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
import org.apache.sis.storage.InternalDataStoreException;
-import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.collection.WeakValueHashMap;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -218,10 +217,8 @@ final class FeatureIterator implements
Spliterator<Feature>, AutoCloseable {
public boolean tryAdvance(final Consumer<? super Feature> action) {
try {
return fetch(action, false);
- } catch (RuntimeException e) {
- throw e;
} catch (Exception e) {
- throw new BackingStoreException(e);
+ throw FeatureStream.cannotExecute(e);
}
}
@@ -232,10 +229,8 @@ final class FeatureIterator implements
Spliterator<Feature>, AutoCloseable {
public void forEachRemaining(final Consumer<? super Feature> action) {
try {
fetch(action, true);
- } catch (RuntimeException e) {
- throw e;
} catch (Exception e) {
- throw new BackingStoreException(e);
+ throw FeatureStream.cannotExecute(e);
}
}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index 4b9c31c00e..f1485ce02f 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@ -31,12 +31,11 @@ import java.sql.Statement;
import org.apache.sis.filter.Optimization;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.stream.DeferredStream;
import org.apache.sis.util.stream.PaginedStream;
import org.apache.sis.filter.privy.SortByComparator;
-import org.apache.sis.util.privy.Strings;
import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.util.collection.BackingStoreException;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
@@ -339,7 +338,7 @@ final class FeatureStream extends DeferredStream<Feature> {
}
}
} catch (SQLException e) {
- throw new BackingStoreException(e);
+ throw cannotExecute(e);
} finally {
unlock();
}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index 4628cf934a..35f9163eab 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@ -873,6 +873,8 @@ public class InfoStatements implements Localized,
AutoCloseable {
/**
* Rethrows the given exception if the SQL state is not category 23:
integrity constraint violation.
* If the exception is a integrity constraint violation, do nothing.
+ * The current version checks more specifically for error code 23505:
+ * <q>duplicate key violates unique constraint</q>.
*
* @param e the exception to filter.
* @throws SQLException if the given exception is not of category 23.
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 322600bf1e..15ec5d6bbe 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@ -32,6 +32,7 @@ import org.apache.sis.metadata.sql.privy.Reflection;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
import org.apache.sis.pending.jdk.JDK19;
import org.apache.sis.util.Debug;
+import org.apache.sis.util.Exceptions;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.util.collection.TreeTable;
import org.apache.sis.util.iso.DefaultNameSpace;
@@ -345,7 +346,7 @@ final class Table extends AbstractFeatureSet {
isEnvelopeAnalyzed = true;
return Optional.ofNullable(database.getEstimatedExtent(name,
attributes, recall));
} catch (SQLException e) {
- throw new DataStoreException(e);
+ throw new DataStoreException(e.getMessage(), Exceptions.unwrap(e));
} else {
return Optional.empty();
}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/DeferredStream.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/DeferredStream.java
index 56894d613b..1d39b8cf7b 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/DeferredStream.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/DeferredStream.java
@@ -88,10 +88,8 @@ public abstract class DeferredStream<T> extends
StreamWrapper<T> {
try {
if (h != null) try {
h.close();
- } catch (RuntimeException e) {
- throw e;
} catch (Exception e) {
- throw new BackingStoreException(e);
+ throw cannotExecute(e);
}
} finally {
if (c != null) {
@@ -126,7 +124,7 @@ public abstract class DeferredStream<T> extends
StreamWrapper<T> {
if (cause instanceof BackingStoreException) {
ex = (BackingStoreException) cause;
} else {
- ex = new BackingStoreException(Exceptions.unwrap(cause));
+ ex = new BackingStoreException(cause.getMessage(),
Exceptions.unwrap(cause));
}
/*
* The close handler will be invoked later assuming that the user
created the stream in a
@@ -198,4 +196,19 @@ public abstract class DeferredStream<T> extends
StreamWrapper<T> {
protected final void unlock() {
closeHandler.run();
}
+
+ /**
+ * Creates an unchecked exception for an operation that cannot be executed
because of the specified cause.
+ *
+ * @param cause the cause about why the operation cannot be executed.
+ * @return the unchecked exception to throw. May be {@code cause} itself.
+ */
+ public static RuntimeException cannotExecute(final Exception cause) {
+ final Exception unwrap = Exceptions.unwrap(cause);
+ if (unwrap instanceof RuntimeException) {
+ return (RuntimeException) unwrap;
+ } else {
+ return new BackingStoreException(cause.toString(), unwrap);
+ }
+ }
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/WritableAggregate.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/WritableAggregate.java
index 1bc0d42e41..8e80bdc5db 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/WritableAggregate.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/WritableAggregate.java
@@ -28,7 +28,7 @@ package org.apache.sis.storage;
public interface WritableAggregate extends Aggregate {
/**
* Adds a new {@code Resource} in this {@code Aggregate}.
- * The given {@link Resource} will be copied, and the <i>effectively
added</i> resource returned.
+ * The given {@link Resource} may be copied, and the <i>effectively
added</i> resource returned.
* The effectively added resource may differ from the given resource in
many aspects.
* The possible changes may include the followings but not only:
*
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
index f8dbad4651..b9d249cb25 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
@@ -23,6 +23,10 @@ import java.util.Locale;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Function;
+import java.time.ZoneId;
+import java.time.Instant;
+import java.time.DateTimeException;
+import java.time.temporal.Temporal;
import org.opengis.util.CodeList;
import org.opengis.util.InternationalString;
import org.opengis.metadata.Metadata;
@@ -40,7 +44,9 @@ import org.opengis.metadata.spatial.CellGeometry;
import org.opengis.metadata.spatial.Georectified;
import org.opengis.metadata.spatial.SpatialRepresentation;
import org.opengis.metadata.spatial.GridSpatialRepresentation;
+import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.util.collection.CodeListSet;
+import org.apache.sis.temporal.TemporalDate;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.metadata.citation.Party;
@@ -49,7 +55,7 @@ import org.opengis.metadata.citation.Responsibility;
/**
* Helper methods for fetching metadata to be written by {@code DataStore}
implementations.
- * This is not a general-purpose builder suitable for public API, since the
methods provided
+ * This is not a general-purpose builder suitable for public API, because the
methods provided
* in this class are tailored for Apache SIS data store needs.
* API of this class may change in any future SIS versions.
*
@@ -58,6 +64,21 @@ import org.opengis.metadata.citation.Responsibility;
* @param <T> type of temporal objects.
*/
public abstract class MetadataFetcher<T> {
+ /**
+ * Types of date to accept as a date of last update, in preference order.
+ */
+ private static final DateType[] LAST_UPDATE_TYPES = {
+ DateType.LAST_UPDATE,
+ DateType.LAST_REVISION,
+ DateType.REVISION,
+ DateType.IN_FORCE,
+ DateType.RELEASED,
+ DateType.DISTRIBUTION,
+ DateType.PUBLICATION,
+ DateType.ADOPTED,
+ DateType.CREATION
+ };
+
/**
* Title of the resource, or {@code null} if none.
*
@@ -88,12 +109,25 @@ public abstract class MetadataFetcher<T> {
/**
* Dates when the resource has been created, or {@code null} if none.
- * Limited to a singleton by default.
*
* <p>Path: {@code metadata/identificationInfo/citation/date}</p>
*/
public List<T> creationDate;
+ /**
+ * Dates of the last update, or {@code null} if none.
+ * If there is no {@link DateType#LAST_UPDATE}, then this class fallbacks
on {@link DateType#LAST_REVISION}.
+ * If there is no last revision, then this class fallbacks on {@link
DateType#REVISION}, <i>etc.</i>
+ *
+ * @see #lastUpdate(Metadata)
+ */
+ public List<T> lastUpdate;
+
+ /**
+ * Type of the {@link #lastUpdate} values as an index in the {@link
#LAST_UPDATE_TYPES} array.
+ */
+ private int lastUpdateType;
+
/**
* Unique identification of the measuring instrument, or {@code null} if
none.
*
@@ -140,6 +174,7 @@ public abstract class MetadataFetcher<T> {
*/
public MetadataFetcher(final Locale locale) {
this.locale = locale;
+ lastUpdateType = LAST_UPDATE_TYPES.length;
}
/**
@@ -217,16 +252,22 @@ public abstract class MetadataFetcher<T> {
*
* @param info the resource citation date (not null).
* @return whether to stop iteration after the given object.
+ *
+ * @see #lastUpdate(Metadata)
*/
protected boolean accept(final CitationDate info) {
- if (creationDate == null) {
- if (info.getDateType() == DateType.CREATION) {
- creationDate = List.of(convertDate(info.getDate()));
- } else {
- return false; // Search another date.
- }
+ final DateType type = info.getDateType();
+ if (type == DateType.CREATION) {
+ creationDate = addDate(creationDate, info, false);
}
- return true;
+ final int limit = LAST_UPDATE_TYPES.length - 1;
+ int i = Math.min(lastUpdateType, limit);
+ do if (LAST_UPDATE_TYPES[i].equals(type)) {
+ lastUpdate = addDate(lastUpdate, info, i < limit);
+ lastUpdateType = i;
+ break;
+ } while (--i >= 0);
+ return false;
}
/**
@@ -357,7 +398,7 @@ public abstract class MetadataFetcher<T> {
* @return the collection where the string was added.
*/
private static List<String> addString(List<String> target, String value) {
- if (value != null && !(value = value.trim()).isEmpty()) {
+ if (value != null && !(value = value.trim()).isBlank()) {
if (target == null) {
target = new ArrayList<>(2); // We will usually have
only one element.
}
@@ -385,6 +426,28 @@ public abstract class MetadataFetcher<T> {
return target;
}
+ /**
+ * Adds the given date in the given collection.
+ *
+ * @param target where to add the date, or {@code null} if not yet
created.
+ * @param value the date to add, or {@code null} if none.
+ * @param clear whether to clear the list before to add the date.
+ * @return the collection where the date was added.
+ */
+ private List<T> addDate(List<T> target, final CitationDate value, final
boolean clear) {
+ @SuppressWarnings("deprecation")
+ final Date date = value.getDate();
+ if (date != null) {
+ if (target == null) {
+ target = new ArrayList<>(2); // We will usually have
only one element.
+ } else if (clear) {
+ target.clear();
+ }
+ target.add(convertDate(date));
+ }
+ return target;
+ }
+
/**
* Converts the given date into the object to store.
* The {@code <T>} type may be for example {@code <String>}
@@ -394,4 +457,47 @@ public abstract class MetadataFetcher<T> {
* @return subclass-dependent object representing the given date.
*/
protected abstract T convertDate(final Date date);
+
+ /**
+ * Returns the first date of type {@link DateType#LAST_UPDATE}.
+ * If there is no last update, then this method fallbacks on {@link
DateType#LAST_REVISION}.
+ * If there is no last revision, then this method fallbacks on {@link
DateType#REVISION}, <i>etc.</i>
+ *
+ * @param metadata the metadata from which to get the date of last
update.
+ * @param zone the timezone to use if the time is local, or {@code
null} if none.
+ * @param listeners where to report warnings, or {@code null} if none.
+ * @return date of last update, or {@code null} if none.
+ *
+ * @see #lastUpdate
+ */
+ public static Instant lastUpdate(final Metadata metadata, final ZoneId
zone, final StoreListeners listeners) {
+ Temporal lastUpdate = null;
+ if (metadata != null) {
+ int lastUpdateType = LAST_UPDATE_TYPES.length;
+search: for (Identification info : metadata.getIdentificationInfo()) {
+ final Citation citation = info.getCitation();
+ if (citation != null) {
+ for (CitationDate date : citation.getDates()) {
+ final DateType type = date.getDateType();
+ for (int i = lastUpdateType; --i >= 0;) {
+ if (LAST_UPDATE_TYPES[i].equals(type)) {
+ lastUpdateType = i;
+ lastUpdate = date.getReferenceDate();
+ if (i == 0) break search;
+ }
+ }
+ }
+ }
+ }
+ }
+ try {
+ return TemporalDate.toInstant(lastUpdate, zone);
+ } catch (DateTimeException e) {
+ if (listeners == null) {
+ throw e;
+ }
+ listeners.warning(e);
+ return null;
+ }
+ }
}