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 2448d007cd Add converters between strings to various kinds of `java.time` objects. This is needed by `SQLStore` when a column is mapped to e.g. `LocalDate`. 2448d007cd is described below commit 2448d007cd4d2ec3505abdf18510c230c20af37a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Oct 22 17:53:50 2024 +0200 Add converters between strings to various kinds of `java.time` objects. This is needed by `SQLStore` when a column is mapped to e.g. `LocalDate`. --- .../org/apache/sis/feature/FeatureOperations.java | 7 +- .../apache/sis/feature/StringJoinOperation.java | 14 +++- .../org/apache/sis/metadata/sql/privy/Syntax.java | 2 +- .../apache/sis/storage/sql/feature/Analyzer.java | 2 +- .../apache/sis/storage/sql/feature/Database.java | 1 - .../sis/storage/DataStoreContentException.java | 2 +- .../src/org.apache.sis.util/main/module-info.java | 11 +++ .../apache/sis/converter/ConverterRegistry.java | 12 ++-- .../org/apache/sis/converter/DateConverter.java | 51 ++++++++----- .../org/apache/sis/converter/InstantConverter.java | 83 ++++++++++++++++++++++ .../org/apache/sis/converter/StringConverter.java | 82 +++++++++++++++++++++ .../main/org/apache/sis/math/FunctionProperty.java | 4 +- 12 files changed, 234 insertions(+), 37 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java index f88ee704ac..4ab41f8df7 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java @@ -24,7 +24,6 @@ import org.opengis.util.InternationalString; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Static; -import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.util.collection.WeakHashSet; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.privy.Strings; @@ -199,17 +198,15 @@ public final class FeatureOperations extends Static { * @param suffix characters to use at the end of the concatenated string, or {@code null} if none. * @param singleAttributes identification of the single attributes (or operations producing attributes) to concatenate. * @return an operation which concatenates the string representations of all referenced single property values. - * @throws UnconvertibleObjectException if at least one of the given {@code singleAttributes} uses a - * {@linkplain DefaultAttributeType#getValueClass() value class} which is not convertible from a {@link String}. * @throws IllegalArgumentException if {@code singleAttributes} is an empty sequence, or contains a property which * is neither an {@code AttributeType} or an {@code Operation} computing an attribute, or an attribute has - * a {@linkplain DefaultAttributeType#getMaximumOccurs() maximum number of occurrences} greater than 1. + * a {@linkplain DefaultAttributeType#getMaximumOccurs() maximum number of occurrences} greater than 1, or + * uses a {@linkplain DefaultAttributeType#getValueClass() value class} not convertible from a {@link String}. * * @see <a href="https://en.wikipedia.org/wiki/Compound_key">Compound key on Wikipedia</a> */ public static Operation compound(final Map<String,?> identification, final String delimiter, final String prefix, final String suffix, final PropertyType... singleAttributes) - throws UnconvertibleObjectException { ArgumentChecks.ensureNonEmpty("delimiter", delimiter); if (delimiter.indexOf(StringJoinOperation.ESCAPE) >= 0) { diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java index 7b075094e9..b702dc10e9 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java @@ -176,12 +176,14 @@ final class StringJoinOperation extends AbstractOperation { * It is caller's responsibility to ensure that {@code delimiter} and {@code singleAttributes} are not null. * This private constructor does not verify that condition on the assumption that the public API did. * + * @throws UnconvertibleObjectException if at least one attributes is not convertible from a string. + * @throws IllegalArgumentException if the operation failed for another reason. + * * @see FeatureOperations#compound(Map, String, String, String, PropertyType...) */ @SuppressWarnings({"rawtypes", "unchecked"}) // Generic array creation. StringJoinOperation(final Map<String,?> identification, final String delimiter, final String prefix, final String suffix, final PropertyType[] singleAttributes) - throws UnconvertibleObjectException { super(identification); attributeNames = new String[singleAttributes.length]; @@ -234,8 +236,14 @@ final class StringJoinOperation extends AbstractOperation { * We need only their names and how to convert from String to their values. */ attributeNames[i] = name.toString(); - ObjectConverter<? super String, ?> converter = ObjectConverters.find( - String.class, ((AttributeType<?>) propertyType).getValueClass()); + final Class<?> valueClass = ((AttributeType<?>) propertyType).getValueClass(); + ObjectConverter<? super String, ?> converter; + try { + converter = ObjectConverters.find(String.class, valueClass); + } catch (UnconvertibleObjectException e) { + throw new UnconvertibleObjectException(Resources.forProperties(identification) + .getString(Resources.Keys.IllegalPropertyType_2, name, valueClass), e); + } if (isAssociation) { converter = new ForFeature(converter); } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java index ab3e64bac5..ef21416737 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java @@ -51,7 +51,7 @@ public class Syntax { * The string that can be used to escape wildcard characters. * This is the value returned by {@link DatabaseMetaData#getSearchStringEscape()}. */ - final String escape; + protected final String escape; /** * Creates a new {@code Syntax} initialized from the given database metadata. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java index e38ddd9b51..645ea0bd47 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Analyzer.java @@ -165,7 +165,7 @@ public final class Analyzer { * Finds the keyword used for identifying tables and views. * Derby, HSQLDB and PostgreSQL uses the "TABLE" type, but H2 uses "BASE TABLE". */ - final Set<String> types = new HashSet<>(4); + final var types = new HashSet<String>(4); try (ResultSet reflect = metadata.getTableTypes()) { while (reflect.next()) { final String type = reflect.getString(Reflection.TABLE_TYPE); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java index 6d9a1f9593..b09cfed294 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java @@ -304,7 +304,6 @@ public class Database<G> extends Syntax { * @return names of the standard tables defined by the spatial schema. */ final Set<String> detectSpatialSchema(final DatabaseMetaData metadata, final String[] tableTypes) throws SQLException { - final String escape = metadata.getSearchStringEscape(); /* * The following tables are defined by ISO 19125 / OGC Simple feature access part 2. * Note that the standard specified those names in upper-case letters, which is also diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreContentException.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreContentException.java index 3528b8ec0d..cec525b44e 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreContentException.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreContentException.java @@ -20,7 +20,7 @@ import java.util.Locale; /** - * Thrown when a store cannot be read because the stream contains invalid data. + * Thrown when a store cannot be read because the stream or database contains invalid data. * It may be for example a logical inconsistency, or a reference not found, * or an unsupported file format version, <i>etc.</i> * diff --git a/endorsed/src/org.apache.sis.util/main/module-info.java b/endorsed/src/org.apache.sis.util/main/module-info.java index 040cfe0af5..9264ad41fe 100644 --- a/endorsed/src/org.apache.sis.util/main/module-info.java +++ b/endorsed/src/org.apache.sis.util/main/module-info.java @@ -45,6 +45,15 @@ module org.apache.sis.util { org.apache.sis.converter.StringConverter.Double, org.apache.sis.converter.StringConverter.BigInteger, org.apache.sis.converter.StringConverter.BigDecimal, + org.apache.sis.converter.StringConverter.Instant, + org.apache.sis.converter.StringConverter.ZonedDateTime, + org.apache.sis.converter.StringConverter.OffsetDateTime, + org.apache.sis.converter.StringConverter.LocalDateTime, + org.apache.sis.converter.StringConverter.LocalDate, + org.apache.sis.converter.StringConverter.LocalTime, + org.apache.sis.converter.StringConverter.Year, + org.apache.sis.converter.StringConverter.YearMonth, + org.apache.sis.converter.StringConverter.MonthDay, org.apache.sis.converter.StringConverter.Boolean, org.apache.sis.converter.StringConverter.Locale, org.apache.sis.converter.StringConverter.Charset, @@ -72,6 +81,8 @@ module org.apache.sis.util { org.apache.sis.converter.DateConverter.Long, org.apache.sis.converter.DateConverter.SQL, org.apache.sis.converter.DateConverter.Timestamp, + org.apache.sis.converter.DateConverter.Instant, + org.apache.sis.converter.InstantConverter.Date, org.apache.sis.converter.CollectionConverter.List, org.apache.sis.converter.CollectionConverter.Set, org.apache.sis.converter.FractionConverter, diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java index 487fe9ce8b..8fc1453f9e 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java @@ -245,7 +245,7 @@ public class ConverterRegistry { * ConverterRegistry), unwraps it and registers its component individually. */ if (converter instanceof FallbackConverter<?,?>) { - final FallbackConverter<S,T> fc = (FallbackConverter<S,T>) converter; + final var fc = (FallbackConverter<S,T>) converter; register(fc.primary); register(fc.fallback); return; @@ -282,12 +282,14 @@ public class ConverterRegistry { */ continue; } - if (i.getName().startsWith("java.lang.constant")) { + switch (i.getPackageName()) { /* - * The Constable and ConstantDesc interfaces (introduced in Java 12) - * are internal mechanic for handling byte codes. + * The Constable and ConstantDesc interfaces (introduced in Java 12) are internal mechanic + * for handling byte codes. The temporal interfaces are unusual in that users are advised + * to use a specific implementation class instead of the interface. */ - continue; + case "java.lang.constant": + case "java.time.temporal": continue; } if (Cloneable.class.isAssignableFrom(i)) { /* diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java index da07589113..a14b1cfe57 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java @@ -21,18 +21,13 @@ import java.util.Set; import java.util.EnumSet; import org.apache.sis.util.ObjectConverter; import org.apache.sis.math.FunctionProperty; +import org.apache.sis.util.UnconvertibleObjectException; /** * Handles conversions from {@link Date} to various objects. - * - * <h2>String representation</h2> - * There is currently no converter between {@link String} and {@link java.util.Date} because the - * date format is not yet defined (we are considering the ISO format for a future SIS version). - * - * <h2>Special cases</h2> - * The converter from dates to timestamps is not injective, because the same date could be mapped - * to many timestamps since timestamps have an additional nanoseconds field. + * Note that there is no converter between {@link String} and {@link java.util.Date}. + * The {@link java.time.Instant} class should be used instead. * * <h2>Immutability and thread safety</h2> * This base class and all inner classes are immutable, and thus inherently thread-safe. @@ -61,12 +56,14 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> { } /** - * Returns the function properties. + * Returns the function properties. The function from {@code Date} instances to {@code Timestamp} or + * {@code Instant} instances is <em>injective</em> because each instant is either unrelated to dates + * (if the instant contains a nanosecond field), or is the output of exactly one {@code Date} with + * nanoseconds assumed to be zero. */ @Override public Set<FunctionProperty> properties() { - return EnumSet.of(FunctionProperty.SURJECTIVE, FunctionProperty.ORDER_PRESERVING, - FunctionProperty.INVERTIBLE); + return EnumSet.of(FunctionProperty.INJECTIVE, FunctionProperty.ORDER_PRESERVING, FunctionProperty.INVERTIBLE); } /** @@ -171,12 +168,30 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> { } } - /* - * We do not yet provide converter to java.time.Instant. If we do so, we need to create an InstantConverter class - * doing the inverse conversion. Reminder: java.sql.Date and java.sql.Time are not convertible to Instant (their - * Date.toInstant() method throws UnsupportedOperationException), but java.sql.Timestamp is. - * - * If conversion to/from java.time.Instant is added, see if some code can be shared with - * org.apache.sis.filter.ComparisonFilter. + /** + * From {@code Date} to {@code Instant}. */ + public static final class Instant extends DateConverter<java.time.Instant> { + private static final long serialVersionUID = 5727173560137117677L; + + static final Instant INSTANCE = new Instant(); // Invoked by ServiceLoader when using module-path. + public static Instant provider() { + return INSTANCE; + } + + public Instant() { // Instantiated by ServiceLoader when using class-path. + super(java.time.Instant.class); + inverse = InstantConverter.Date.INSTANCE; + } + + @Override public java.time.Instant apply(final Date source) { + if (source != null) try { + return source.toInstant(); + } catch (UnsupportedOperationException e) { + // Thrown by `java.sql.Date` and `java.sql.Time`, but not `java.sql.Timestamp`. + throw new UnconvertibleObjectException(formatErrorMessage(source), e); + } + return null; + } + } } diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/InstantConverter.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/InstantConverter.java new file mode 100644 index 0000000000..d49b6c9bf0 --- /dev/null +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/InstantConverter.java @@ -0,0 +1,83 @@ +/* + * 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.converter; + +import java.util.Set; +import java.util.EnumSet; +import java.time.Instant; +import org.apache.sis.util.ObjectConverter; +import org.apache.sis.math.FunctionProperty; + + +/** + * Handles conversions from {@link Instant} to various objects. + * + * <h2>Immutability and thread safety</h2> + * This base class and all inner classes are immutable, and thus inherently thread-safe. + * + * @author Martin Desruisseaux (Geomatys) + * + * @param <T> the base type of converted objects. + */ +abstract class InstantConverter<T> extends SystemConverter<Instant,T> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -7219681557586687605L; + + /** + * Creates a converter for the given target type. + * Subclasses must initialize {@link #inverse}. + */ + InstantConverter(final Class<T> targetClass) { + super(Instant.class, targetClass); + } + + /** + * Returns the function properties. The function is <em>surjective</em> because any {@code Date} instances + * can be created from one or many {@code Instant} instances. The same date may be created from many instants + * because the nanosecond field is dropped. + */ + @Override + public Set<FunctionProperty> properties() { + return EnumSet.of(FunctionProperty.SURJECTIVE, FunctionProperty.ORDER_PRESERVING, FunctionProperty.INVERTIBLE); + } + + /** + * From {@code Instant} to {@code Date}. + */ + public static final class Date extends InstantConverter<java.util.Date> { + private static final long serialVersionUID = -9192665378798185400L; + + static final Date INSTANCE = new Date(); // Invoked by ServiceLoader when using module-path. + public static Date provider() { + return INSTANCE; + } + + public Date() { // Instantiated by ServiceLoader when using class-path. + super(java.util.Date.class); + } + + @Override public ObjectConverter<java.util.Date, Instant> inverse() { + return DateConverter.Instant.INSTANCE; + } + + @Override public java.util.Date apply(final Instant source) { + return (source != null) ? java.util.Date.from(source) : null; + } + } +} diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java index 73f5eae546..98227e9dfc 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java @@ -19,6 +19,7 @@ package org.apache.sis.converter; import java.util.Set; import java.util.EnumSet; import java.util.IllformedLocaleException; +import java.time.format.DateTimeParseException; import java.nio.charset.UnsupportedCharsetException; import java.net.URISyntaxException; import java.net.MalformedURLException; @@ -232,6 +233,87 @@ abstract class StringConverter<T> extends SystemConverter<String, T> { } } + public static final class Instant extends StringConverter<java.time.Instant> { + private static final long serialVersionUID = -786622578610861924L; + public Instant() {super(java.time.Instant.class);} + + @Override java.time.Instant doConvert(String source) throws DateTimeParseException { + return java.time.Instant.parse(source); + } + } + + public static final class ZonedDateTime extends StringConverter<java.time.ZonedDateTime> { + private static final long serialVersionUID = 4547600422615778462L; + public ZonedDateTime() {super(java.time.ZonedDateTime.class);} + + @Override java.time.ZonedDateTime doConvert(String source) throws DateTimeParseException { + return java.time.ZonedDateTime.parse(source); + } + } + + public static final class OffsetDateTime extends StringConverter<java.time.OffsetDateTime> { + private static final long serialVersionUID = 6438936715171368273L; + public OffsetDateTime() {super(java.time.OffsetDateTime.class);} + + @Override java.time.OffsetDateTime doConvert(String source) throws DateTimeParseException { + return java.time.OffsetDateTime.parse(source); + } + } + + public static final class LocalDateTime extends StringConverter<java.time.LocalDateTime> { + private static final long serialVersionUID = 4020225109842204445L; + public LocalDateTime() {super(java.time.LocalDateTime.class);} + + @Override java.time.LocalDateTime doConvert(String source) throws DateTimeParseException { + return java.time.LocalDateTime.parse(source); + } + } + + public static final class LocalDate extends StringConverter<java.time.LocalDate> { + private static final long serialVersionUID = -2160961842632015681L; + public LocalDate() {super(java.time.LocalDate.class);} + + @Override java.time.LocalDate doConvert(String source) throws DateTimeParseException { + return java.time.LocalDate.parse(source); + } + } + + public static final class LocalTime extends StringConverter<java.time.LocalTime> { + private static final long serialVersionUID = -4872647331214579728L; + public LocalTime() {super(java.time.LocalTime.class);} + + @Override java.time.LocalTime doConvert(String source) throws DateTimeParseException { + return java.time.LocalTime.parse(source); + } + } + + public static final class Year extends StringConverter<java.time.Year> { + private static final long serialVersionUID = 9014595771888427112L; + public Year() {super(java.time.Year.class);} + + @Override java.time.Year doConvert(String source) throws DateTimeParseException { + return java.time.Year.parse(source); + } + } + + public static final class YearMonth extends StringConverter<java.time.YearMonth> { + private static final long serialVersionUID = -8552019996811990307L; + public YearMonth() {super(java.time.YearMonth.class);} + + @Override java.time.YearMonth doConvert(String source) throws DateTimeParseException { + return java.time.YearMonth.parse(source); + } + } + + public static final class MonthDay extends StringConverter<java.time.MonthDay> { + private static final long serialVersionUID = 7647193120429326557L; + public MonthDay() {super(java.time.MonthDay.class);} + + @Override java.time.MonthDay doConvert(String source) throws DateTimeParseException { + return java.time.MonthDay.parse(source); + } + } + public static final class Boolean extends StringConverter<java.lang.Boolean> { private static final long serialVersionUID = 4689076223535035309L; public Boolean() {super(java.lang.Boolean.class);} // Instantiated by ServiceLoader. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/FunctionProperty.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/FunctionProperty.java index 262780a669..f86a62cb1b 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/FunctionProperty.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/FunctionProperty.java @@ -80,7 +80,7 @@ public enum FunctionProperty { /** * A function is <i>injective</i> if each value of <var>T</var> is either unrelated * to <var>S</var>, or is the output of exactly one value of <var>S</var>. - * For example an {@link org.apache.sis.util.ObjectConverter} doing conversions from {@link Integer} + * For example, an {@link org.apache.sis.util.ObjectConverter} doing conversions from {@link Integer} * to {@link String} is an injective function, because no pair of integers can produce the same string. * * <p>A function which is both injective and {@linkplain #SURJECTIVE surjective} is a @@ -95,7 +95,7 @@ public enum FunctionProperty { /** * A function is <i>surjective</i> if any value of <var>T</var> can be created * from one or many values of <var>S</var>. - * For example an {@link org.apache.sis.util.ObjectConverter} doing conversions from {@link String} + * For example, an {@link org.apache.sis.util.ObjectConverter} doing conversions from {@link String} * to {@link Integer} is a surjective function, because there is always at least one string for each integer value. * Note that such function cannot be injective since many different strings can represent the same integer value. *