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 3a5fcfaa4a Generalize the update of properties of operations to all
kinds of `AbstractOperation` instead of only links. This is a generalization of
the previous commit ("recreate the result types of the links if they changed").
The intent is to the CRS characteristic updated not only for links, but also
for the envelope operation.
3a5fcfaa4a is described below
commit 3a5fcfaa4a32f78737e5b4ded29aff5827b4828d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat May 24 18:46:52 2025 +0200
Generalize the update of properties of operations to all kinds of
`AbstractOperation` instead of only links.
This is a generalization of the previous commit ("recreate the result types
of the links if they changed").
The intent is to the CRS characteristic updated not only for links, but
also for the envelope operation.
---
.../apache/sis/feature/AbstractIdentifiedType.java | 90 +++++++---
.../org/apache/sis/feature/AbstractOperation.java | 65 ++++---
.../apache/sis/feature/DefaultAssociationRole.java | 15 +-
.../apache/sis/feature/DefaultAttributeType.java | 15 +-
.../org/apache/sis/feature/EnvelopeOperation.java | 196 ++++++++++++++-------
.../org/apache/sis/feature/FeatureOperations.java | 18 +-
.../main/org/apache/sis/feature/Features.java | 4 +-
.../sis/feature/GroupAsPolylineOperation.java | 42 ++++-
.../main/org/apache/sis/feature/LinkOperation.java | 16 ++
.../apache/sis/feature/StringJoinOperation.java | 80 +++++++--
.../sis/feature/builder/OperationWrapper.java | 37 ++--
.../org/apache/sis/feature/internal/Resources.java | 5 +
.../sis/feature/internal/Resources.properties | 1 +
.../sis/feature/internal/Resources_fr.properties | 1 +
.../feature/privy/FeatureProjectionBuilder.java | 49 +++---
.../main/org/apache/sis/storage/FeatureQuery.java | 13 +-
.../main/org/apache/sis/storage/FeatureSet.java | 1 -
.../main/org/apache/sis/storage/FeatureSubset.java | 4 +-
.../sis/storage/UnsupportedQueryException.java | 12 ++
.../org/apache/sis/storage/FeatureQueryTest.java | 2 +-
.../main/org/apache/sis/pending/jdk/JDK21.java | 24 ++-
.../org/apache/sis/util/privy/CollectionsExt.java | 8 +-
22 files changed, 482 insertions(+), 216 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
index d378c95ada..f2beb1c0e2 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
@@ -27,6 +27,7 @@ import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.apache.sis.system.Modules;
import org.apache.sis.util.Deprecable;
+import org.apache.sis.util.collection.Containers;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.resources.Errors;
@@ -98,6 +99,18 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
*/
public static final String DEPRECATED_KEY = "deprecated";
+ /**
+ * Optional key which can be given to the constructor for inheriting
values from an existing identified type.
+ * If a value exists, then any property that is not defined by one of the
above-cited keys will inherit its
+ * value from the given {@link IdentifiedType}.
+ *
+ * <p>This property is useful when creating a new property derived from an
existing property.
+ * An example of such derivation is {@link
AbstractOperation#updateDependencies(Map)}.</p>
+ *
+ * @since 1.5
+ */
+ public static final String INHERIT_FROM_KEY = "inheritFrom";
+
/**
* The name of this type.
*
@@ -146,7 +159,8 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
/**
* Constructs a type from the given properties. Keys are strings from the
table below.
- * The map given in argument shall contain an entry at least for the
{@value #NAME_KEY}.
+ * The map given in argument shall contain an entry at least for the
{@value #NAME_KEY} key,
+ * unless a fallback is specified with the {@value #INHERIT_FROM_KEY} key.
* Other entries listed in the table below are optional.
*
* <table class="sis">
@@ -155,32 +169,31 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
* <th>Map key</th>
* <th>Value type</th>
* <th>Returned by</th>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value #NAME_KEY}</td>
* <td>{@link GenericName} or {@link String}</td>
* <td>{@link #getName()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value #DEFINITION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDefinition()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value #DESIGNATION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDesignation()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value #DESCRIPTION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDescription()}</td>
- * <tr>
+ * </tr><tr>
* <td>{@value #DEPRECATED_KEY}</td>
* <td>{@link Boolean}</td>
* <td>{@link #isDeprecated()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
+ * <td>{@value #INHERIT_FROM_KEY}</td>
+ * <td>{@link IdentifiedType}</td>
+ * <td>(various)</td>
+ * </tr><tr>
* <td>{@value
org.apache.sis.referencing.AbstractIdentifiedObject#LOCALE_KEY}</td>
* <td>{@link Locale}</td>
* <td>(none)</td>
@@ -202,11 +215,13 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
*/
@SuppressWarnings("this-escape")
protected AbstractIdentifiedType(final Map<String,?> identification)
throws IllegalArgumentException {
- // Implicit null value check.
- Object value = identification.get(NAME_KEY);
+ final IdentifiedType inheritFrom = Containers.property(identification,
INHERIT_FROM_KEY, IdentifiedType.class);
+ Object value = identification.get(NAME_KEY); // Implicit null value
check.
if (value == null) {
- throw new
IllegalArgumentException(Errors.forProperties(identification)
- .getString(Errors.Keys.MissingValueForProperty_1,
NAME_KEY));
+ if (inheritFrom == null || (name = inheritFrom.getName()) == null)
{
+ throw new
IllegalArgumentException(Errors.forProperties(identification)
+ .getString(Errors.Keys.MissingValueForProperty_1,
NAME_KEY));
+ }
} else if (value instanceof String) {
name = createName(DefaultNameFactory.provider(), (String) value);
} else if (value instanceof GenericName) {
@@ -214,12 +229,12 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
} else {
throw illegalPropertyType(identification, NAME_KEY, value);
}
- definition = Types.toInternationalString(identification,
DEFINITION_KEY);
- designation = Types.toInternationalString(identification,
DESIGNATION_KEY);
- description = Types.toInternationalString(identification,
DESCRIPTION_KEY);
+ definition = toInternationalString(identification, DEFINITION_KEY,
inheritFrom);
+ designation = toInternationalString(identification, DESIGNATION_KEY,
inheritFrom);
+ description = toInternationalString(identification, DESCRIPTION_KEY,
inheritFrom);
value = identification.get(DEPRECATED_KEY);
if (value == null) {
- deprecated = false;
+ deprecated = (inheritFrom instanceof Deprecable) ? ((Deprecable)
inheritFrom).isDeprecated() : false;
} else if (value instanceof Boolean) {
deprecated = (Boolean) value;
} else {
@@ -227,6 +242,29 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
}
}
+ /**
+ * Returns an international string for the values in the given properties
map, or {@code null} if none.
+ *
+ * @param identification the map from which to get the string values for
an international string.
+ * @param prefix the prefix of keys to use for creating the
international string.
+ * @param inheritFrom the type from which to inherit a value if none
is specified in the map, or {@code null}.
+ * @return the international string, or {@code null} if the given map is
null or does not contain values
+ * associated to keys starting with the given prefix.
+ */
+ private static InternationalString toInternationalString(
+ final Map<String,?> identification, final String prefix, final
IdentifiedType inheritFrom)
+ {
+ InternationalString i18n = Types.toInternationalString(identification,
prefix);
+ if (i18n == null && inheritFrom != null) {
+ switch (prefix) {
+ case DEFINITION_KEY: i18n = inheritFrom.getDefinition();
break;
+ case DESIGNATION_KEY: i18n =
inheritFrom.getDesignation().orElse(null); break;
+ case DESCRIPTION_KEY: i18n =
inheritFrom.getDescription().orElse(null); break;
+ }
+ }
+ return i18n;
+ }
+
/**
* Returns the exception to be thrown when a property is of illegal type.
*/
@@ -237,6 +275,14 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
Errors.Keys.IllegalPropertyValueClass_2, key,
value.getClass()));
}
+ /**
+ * Convenience method for subclasses that create new types derived from
this type.
+ * The purpose is more to improve readability than to save a few byte
codes.
+ */
+ final Map<String,?> inherit() {
+ return Map.of(INHERIT_FROM_KEY, this);
+ }
+
/**
* Creates a name from the given string. This method is invoked at
construction time,
* so it should not use any field in this {@code AbtractIdentifiedObject}
instance.
@@ -367,7 +413,7 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
@Override
public boolean equals(final Object obj) {
if (obj != null && getClass() == obj.getClass()) {
- final AbstractIdentifiedType that = (AbstractIdentifiedType) obj;
+ final var that = (AbstractIdentifiedType) obj;
return Objects.equals(name, that.name) &&
Objects.equals(definition, that.definition) &&
Objects.equals(designation, that.designation) &&
@@ -396,7 +442,7 @@ public class AbstractIdentifiedType implements
IdentifiedType, Deprecable, Seria
}
key = Errors.Keys.EmptyProperty_1;
}
- final StringBuilder b = new
StringBuilder(40).append("Type[“").append(container.getName()).append("”].")
+ final var b = new
StringBuilder(40).append("Type[“").append(container.getName()).append("”].")
.append(argument).append('[').append(index).append("].name");
throw new IllegalArgumentException(Errors.format(key, b.toString()));
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
index cd7d311557..6c76e4c245 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
@@ -19,7 +19,6 @@ package org.apache.sis.feature;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
-import java.util.Collections;
import java.util.HashMap;
import java.util.function.BiFunction;
import java.io.IOException;
@@ -43,6 +42,7 @@ import org.opengis.feature.FeatureOperationException;
import org.opengis.feature.IdentifiedType;
import org.opengis.feature.Operation;
import org.opengis.feature.Property;
+import org.opengis.feature.PropertyType;
/**
@@ -60,11 +60,8 @@ import org.opengis.feature.Property;
* The value is computed, or the operation is executed, by {@link
#apply(Feature, ParameterValueGroup)}.
* If the value is modifiable, new value can be set by call to {@link
Attribute#setValue(Object)}.
*
- * <div class="warning"><b>Warning:</b> this class is experimental and may
change after we gained more
- * experience on this aspect of ISO 19109.</div>
- *
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.5
*
* @see DefaultFeatureType
*
@@ -95,28 +92,23 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
* <th>Map key</th>
* <th>Value type</th>
* <th>Returned by</th>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
* <td>{@link GenericName} or {@link String}</td>
* <td>{@link #getName()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
* <td>{@link org.opengis.util.InternationalString} or {@link
String}</td>
* <td>{@link #getDefinition()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
* <td>{@link org.opengis.util.InternationalString} or {@link
String}</td>
* <td>{@link #getDesignation()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
* <td>{@link org.opengis.util.InternationalString} or {@link
String}</td>
* <td>{@link #getDescription()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
* <td>{@link Boolean}</td>
* <td>{@link #isDeprecated()}</td>
@@ -138,7 +130,7 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
* @param identification the map given by user to sub-class constructor.
*/
final Map<String,Object> resultIdentification(final Map<String,?>
identification) {
- final Map<String,Object> properties = new HashMap<>(6);
+ final var properties = new HashMap<String,Object>(6);
for (final Map.Entry<String,?> entry : identification.entrySet()) {
final String key = entry.getKey();
if (key != null && key.startsWith(RESULT_PREFIX)) {
@@ -207,18 +199,47 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
* other dependencies, the returned set will contain the name of that
operation but not the names of the
* dependencies of that operation (unless they are the same that the
direct dependencies of {@code this}).
*
- * <div class="note"><b>Rational:</b>
- * this information is needed for writing the {@code SELECT} SQL statement
to send to a database server.
+ * <h4>Purpose</h4>
+ * This information is needed for writing the {@code SELECT} SQL statement
to send to a database server.
* The requested columns will typically be all attributes declared in a
{@code FeatureType}, but also
* any additional columns needed for the operation while not necessarily
included in the {@code FeatureType}.
- * </div>
*
+ * <h4>Default implementation</h4>
* The default implementation returns an empty set.
*
* @return the names of feature properties needed by this operation for
performing its task.
*/
public Set<String> getDependencies() {
- return Collections.emptySet();
+ return Set.of();
+ }
+
+ /**
+ * Returns the same operation but using different properties as inputs.
+ * The keys in the given map should be values returned by {@link
#getDependencies()},
+ * and the associated values shall be the properties to use instead of the
current dependencies.
+ * If any key in the given map is not a member of the {@linkplain
#getDependencies() dependency set},
+ * then the entry is ignored. Conversely, if any member of the dependency
set is not contained in the
+ * given map, then the associated dependency is unchanged.
+ *
+ * <h4>Purpose</h4>
+ * This method is needed by {@link
org.apache.sis.feature.builder.FeatureTypeBuilder} when some properties
+ * are operations inherited from another feature type. Even if the
dependencies are properties of the same
+ * name, some {@link DefaultAttributeType#characteristics()
characteristics} may be different.
+ * For example, the <abbr>CRS</abbr> may change as a result of a change of
<abbr>CRS</abbr>.
+ *
+ * <h4>Default implementation</h4>
+ * The default implementation returns {@code this}.
+ * This is consistent with the default implementation of {@link
#getDependencies()} returning an empty set.
+ *
+ * @param dependencies the new properties to use as operation inputs.
+ * @return the new operation, or {@code this} if unchanged.
+ *
+ * @see #INHERIT_FROM_KEY
+ *
+ * @since 1.5
+ */
+ public Operation updateDependencies(final Map<String, PropertyType>
dependencies) {
+ return this;
}
/**
@@ -246,7 +267,7 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
return true;
}
if (super.equals(obj)) {
- final AbstractOperation that = (AbstractOperation) obj;
+ final var that = (AbstractOperation) obj;
return Objects.equals(getParameters(), that.getParameters()) &&
Objects.equals(getResult(), that.getResult());
}
@@ -261,7 +282,7 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
*/
@Override
public String toString() {
- final StringBuilder buffer = new
StringBuilder(40).append(Classes.getShortClassName(this)).append('[');
+ final var buffer = new
StringBuilder(40).append(Classes.getShortClassName(this)).append('[');
final GenericName name = getName();
if (name != null) {
buffer.append('“');
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
index 3e8061aa23..2dbaf6fe30 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
@@ -96,28 +96,23 @@ public class DefaultAssociationRole extends FieldType
implements FeatureAssociat
* <th>Map key</th>
* <th>Value type</th>
* <th>Returned by</th>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
* <td>{@link GenericName} or {@link String}</td>
* <td>{@link #getName()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDefinition()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDesignation()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDescription()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
* <td>{@link Boolean}</td>
* <td>{@link #isDeprecated()}</td>
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
index b5b5c12a55..9d53cb2ba4 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
@@ -142,28 +142,23 @@ public class DefaultAttributeType<V> extends FieldType
implements AttributeType<
* <th>Map key</th>
* <th>Value type</th>
* <th>Returned by</th>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
* <td>{@link GenericName} or {@link String}</td>
* <td>{@link #getName()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDefinition()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDesignation()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
* <td>{@link InternationalString} or {@link String}</td>
* <td>{@link #getDescription()}</td>
- * </tr>
- * <tr>
+ * </tr><tr>
* <td>{@value
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
* <td>{@link Boolean}</td>
* <td>{@link #isDeprecated()}</td>
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
index fcf5897599..ca03d43069 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
@@ -21,8 +21,6 @@ import java.util.Set;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Objects;
-import java.util.Optional;
-import org.opengis.util.GenericName;
import org.opengis.util.FactoryException;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterDescriptorGroup;
@@ -37,22 +35,25 @@ import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
import org.apache.sis.util.privy.CollectionsExt;
-import org.apache.sis.referencing.CRS;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.pending.jdk.JDK21;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureInstantiationException;
import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
import org.opengis.feature.Property;
import org.opengis.feature.PropertyType;
/**
* An operation computing the envelope that encompass all geometries found in
a list of attributes.
- * Geometries can be in different coordinate reference systems; they will be
transformed to the first
- * non-null CRS in the following choices:
+ * Geometries can be in different coordinate reference systems. They will be
transformed to the first
+ * non-null <abbr>CRS</abbr> in the following choices:
*
* <ol>
* <li>the CRS specified at construction time,</li>
@@ -75,7 +76,7 @@ final class EnvelopeOperation extends AbstractOperation {
/**
* For cross-version compatibility.
*/
- private static final long serialVersionUID = 8034615858550405350L;
+ private static final long serialVersionUID = 2435142477482749321L;
/**
* The parameter descriptor for the "Envelope" operation, which does not
take any parameter.
@@ -83,7 +84,10 @@ final class EnvelopeOperation extends AbstractOperation {
private static final ParameterDescriptorGroup EMPTY_PARAMS =
parameters("Envelope");
/**
- * The names of all properties containing a geometry object.
+ * The names of all properties containing a geometry object. The
attributes are the in order specified by the user,
+ * except the default geometry (if any) which is always first. Note that
the name of the default geometry in this
+ * array is usually <em>not</em> {@value AttributeConvention#GEOMETRY},
because this class replaces links by their
+ * targets for avoiding to process the same geometries twice.
*/
private final String[] attributeNames;
@@ -96,6 +100,12 @@ final class EnvelopeOperation extends AbstractOperation {
@SuppressWarnings("serial") // Most SIS implementations
are serializable.
final CoordinateReferenceSystem targetCRS;
+ /**
+ * Whether {@link #targetCRS} has been explicitly specified by the user.
+ * If {@code false}, then the <abbr>CRS</abbr> has been inherited from the
geometries.
+ */
+ private final boolean explicitCRS;
+
/**
* The coordinate conversions or transformations from the CRS used by the
geometries to the CRS requested
* by the user, or {@code null} if there is no operation to apply. If
non-null, the length of this array
@@ -117,8 +127,11 @@ final class EnvelopeOperation extends AbstractOperation {
/**
* The property names as an unmodifiable set, created when first needed.
+ * This is simply {@link #attributeNames} copied in a unmodifiable set.
+ *
+ * @see #getDependencies()
*/
- private transient Set<String> dependencies;
+ private transient volatile Set<String> dependencies;
/**
* The type of the result returned by the envelope operation.
@@ -128,77 +141,114 @@ final class EnvelopeOperation extends AbstractOperation {
/**
* Creates a new operation computing the envelope of features of the given
type.
+ * The {@link #targetCRS} is set to the first non-null <abbr>CRS</abbr> in
the following choices:
+ *
+ * <ol>
+ * <li>the <abbr>CRS</abbr> specified to this constructor,</li>
+ * <li>the <abbr>CRS</abbr> of the default geometry, or</li>
+ * <li>the <abbr>CRS</abbr> of the first non-empty geometry.</li>
+ * </ol>
+ *
+ * <h4>Inheritance</h4>
+ * If {@code inheritFrom} is non-null, then the {@code geometryAttributes}
array must have the same length
+ * as {@code inheritFrom.attributeNames} with elements in the same order.
Any null element in the given
+ * array will be replaced by the corresponding value of {@code
inheritFrom}.
*
* @param identification the name and other information to be given
to this operation.
* @param targetCRS the coordinate reference system of envelopes
to computes, or {@code null}.
* @param geometryAttributes the operation or attribute type from which
to get geometry values.
+ * @param inheritFrom the existing operation from which to inherit
attributes, or {@code null}.
*/
- EnvelopeOperation(final Map<String,?> identification,
CoordinateReferenceSystem targetCRS,
- final PropertyType[] geometryAttributes) throws FactoryException
+ EnvelopeOperation(final Map<String,?> identification,
+ CoordinateReferenceSystem targetCRS,
+ final PropertyType[] geometryAttributes,
+ final EnvelopeOperation inheritFrom)
+ throws FactoryException
{
super(identification);
- String defaultGeometry = null;
+ explicitCRS = (targetCRS != null); // Whether the CRS was
specified by the user or inferred automatically.
+ boolean characterizedByCRS = false; // Whether "sis:crs"
characteristics exist, possibly with null values.
+ String defaultGeometry = null; // Attribute name of the
target of the "sis:geometry" property.
+ boolean defaultIsFirst = true; // Whether the default
geometry is the first entry in the `names` map.
/*
- * Get all property names without duplicated values. If a property is
a link to an attribute,
- * then the key will be the name of the referenced attribute instead
of the operation name.
- * The intent is to avoid querying the same geometry twice if the
attribute is also specified
- * explicitly in the array of properties.
- *
- * The map values will be the default Coordinate Reference System, or
null if none.
+ * Get all property names without duplicated values, including the
targets of links.
+ * The map values will be the default Coordinate Reference Systems, or
null if none.
*/
- boolean characterizedByCRS = false;
- final Map<String,CoordinateReferenceSystem> names = new
LinkedHashMap<>(4);
- for (final IdentifiedType property : geometryAttributes) {
- final Optional<AttributeType<?>> at =
Features.toAttribute(property);
- if (at.isPresent() &&
Geometries.isKnownType(at.get().getValueClass())) {
- final GenericName name = property.getName();
- final String attributeName = (property instanceof
LinkOperation)
- ? ((LinkOperation)
property).referentName : name.toString();
- final boolean isDefault =
AttributeConvention.GEOMETRY_PROPERTY.equals(name);
- if (isDefault) {
- defaultGeometry = attributeName;
+ final var names = new LinkedHashMap<String,
CoordinateReferenceSystem>(4);
+ for (int i=0; i < geometryAttributes.length; i++) {
+ final String propertyName; // Name of
`geometryAttributes[i]`, possibly inherited.
+ final String attributeName; // Name of the property after
following the link.
+ CoordinateReferenceSystem attributeCRS = null;
+ final PropertyType property = geometryAttributes[i];
+ if (property == null && inheritFrom != null) {
+ /*
+ * When this constructor is invoked by
`updateDependencies(Map)`, a null property means to inherit
+ * the property at the same index from the previous operation.
The caller is responsible to ensure
+ * that the indexes match.
+ */
+ propertyName = attributeName = inheritFrom.attributeNames[i];
+ if (inheritFrom.attributeToCRS != null) {
+ final CoordinateOperation op =
inheritFrom.attributeToCRS[i];
+ if (op != null) {
+ attributeCRS = op.getSourceCRS();
+ characterizedByCRS = true;
+ }
}
- CoordinateReferenceSystem attributeCRS = null;
+ } else {
+ final AttributeType<?> at =
Features.toAttribute(property).orElse(null);
+ if (at == null || !Geometries.isKnownType(at.getValueClass()))
{
+ continue; // Not a geometry property. Ignore as per
method contract.
+ }
+ /*
+ * If a property is a link to an attribute, then the key will
be the name of the referenced
+ * attribute instead of the operation name. This is for
avoiding to query the same geometry
+ * twice when the attribute is also specified explicitly in
the array of properties.
+ */
+ propertyName = property.getName().toString();
+ attributeName =
Features.getLinkTarget(property).orElse(propertyName);
/*
- * Set `characterizedByCRS` to true if we find at least one
attribute which may have the
- * "CRS" characteristic. Note that we cannot rely on
`attributeCRS` being non-null
+ * Set `characterizedByCRS` to `true` if we find at least one
attribute which have the
+ * "sis:crs" characteristic. Note that we cannot rely on
`attributeCRS` being non-null
* because an attribute may be characterized by a CRS without
providing default CRS.
*/
- final AttributeType<?> ct =
at.get().characteristics().get(AttributeConvention.CRS);
+ final AttributeType<?> ct =
at.characteristics().get(AttributeConvention.CRS);
if (ct != null &&
CoordinateReferenceSystem.class.isAssignableFrom(ct.getValueClass())) {
- attributeCRS = (CoordinateReferenceSystem)
ct.getDefaultValue(); // May still null.
- if (targetCRS == null && isDefault) {
- targetCRS = attributeCRS;
- }
+ attributeCRS = (CoordinateReferenceSystem)
ct.getDefaultValue(); // May still be null.
characterizedByCRS = true;
}
- names.putIfAbsent(attributeName, attributeCRS);
}
+ /*
+ * If the user did not specified a CRS explicitly, take the CRS of
the default geometry.
+ * If there is no default geometry, the CRS of the first geometry
will be taken in next loop.
+ */
+ if (AttributeConvention.GEOMETRY.equals(propertyName)) {
+ defaultGeometry = attributeName;
+ defaultIsFirst = names.isEmpty();
+ if (targetCRS == null) {
+ targetCRS = attributeCRS;
+ }
+ }
+ names.putIfAbsent(attributeName, attributeCRS);
}
/*
* Copy the names in an array with the default geometry first. If
possible, find the coordinate operations
- * now in order to avoid the potentially costly call to
CRS.findOperation(…) for each feature on which this
- * EnvelopeOperation will be applied.
+ * now in order to avoid the potentially costly calls to
`CRS.findOperation(…)` for each feature on which
+ * this `EnvelopeOperation` will be applied.
*/
- names.remove(null);
// Paranoiac safety.
+ if (!defaultIsFirst) {
+ JDK21.putFirst(names, defaultGeometry,
names.remove(defaultGeometry));
+ }
+ names.remove(null); // Paranoiac safety.
attributeNames = new String[names.size()];
attributeToCRS = characterizedByCRS ? new
CoordinateOperation[attributeNames.length] : null;
- int n = (defaultGeometry == null) ? 0 : 1;
- for (final Map.Entry<String,CoordinateReferenceSystem> entry :
names.entrySet()) {
- final int i;
- final String name = entry.getKey();
- if (name.equals(defaultGeometry)) {
- defaultGeometry = null;
- i = 0;
- } else {
- i = n++;
- }
- attributeNames[i] = name;
+ int i = 0;
+ for (final Map.Entry<String, CoordinateReferenceSystem> entry :
names.entrySet()) {
+ attributeNames[i] = entry.getKey();
if (characterizedByCRS) {
final CoordinateReferenceSystem value = entry.getValue();
if (value != null) {
if (targetCRS == null) {
- targetCRS = value; // Fallback if
default geometry has no CRS.
+ targetCRS = value; // Fallback if the default
geometry has no CRS.
}
/*
* The following operation is often identity. We do not
filter identity operations
@@ -209,6 +259,7 @@ final class EnvelopeOperation extends AbstractOperation {
attributeToCRS[i] = CRS.findOperation(value, targetCRS,
null);
}
}
+ i++;
}
resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>(
resultIdentification(identification), Envelope.class, 1, 1,
null));
@@ -242,11 +293,37 @@ final class EnvelopeOperation extends AbstractOperation {
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
- public synchronized Set<String> getDependencies() {
- if (dependencies == null) {
- dependencies = CollectionsExt.immutableSet(true, attributeNames);
+ public Set<String> getDependencies() {
+ Set<String> cached = dependencies;
+ if (cached == null) {
+ // Not really a problem if computed twice concurrently.
+ dependencies = cached = CollectionsExt.immutableSet(true,
attributeNames);
+ }
+ return cached;
+ }
+
+ /**
+ * Returns the same operation but using different properties as inputs.
+ *
+ * @param dependencies the new properties to use as operation inputs.
+ * @return the new operation, or {@code this} if unchanged.
+ */
+ @Override
+ public Operation updateDependencies(final Map<String, PropertyType>
dependencies) {
+ boolean foundAny = false;
+ final var geometryAttributes = new PropertyType[attributeNames.length];
+ for (int i=0; i < geometryAttributes.length; i++) {
+ foundAny |= (geometryAttributes[i] =
dependencies.get(attributeNames[i])) != null;
+ }
+ if (foundAny) try {
+ var op = new EnvelopeOperation(inherit(), explicitCRS ? targetCRS
: null, geometryAttributes, this);
+ if (!equals(op)) {
+ return FeatureOperations.POOL.unique(op);
+ }
+ } catch (FactoryException e) {
+ throw new FeatureInstantiationException(e.getMessage(), e);
}
- return dependencies;
+ return this;
}
/**
@@ -335,7 +412,7 @@ final class EnvelopeOperation extends AbstractOperation {
* a CRS characteristic is associated to a particular
feature, setting `op` to null
* will cause a new coordinate operation to be
searched.
*/
- final Attribute<?> at = ((Attribute<?>)
feature.getProperty(attributeNames[i]))
+ final var at = ((Attribute<?>)
feature.getProperty(attributeNames[i]))
.characteristics().get(AttributeConvention.CRS);
final Object geomCRS;
if (at != null && (geomCRS = at.getValue()) != null) {
@@ -413,10 +490,11 @@ final class EnvelopeOperation extends AbstractOperation {
public boolean equals(final Object obj) {
if (super.equals(obj)) {
// `this.result` is compared (indirectly) by the super class.
- final EnvelopeOperation that = (EnvelopeOperation) obj;
+ final var that = (EnvelopeOperation) obj;
return Arrays.equals(attributeNames, that.attributeNames) &&
Arrays.equals(attributeToCRS, that.attributeToCRS) &&
- Objects.equals(targetCRS, that.targetCRS);
+ Objects.equals(targetCRS, that.targetCRS) &&
+ explicitCRS == that.explicitCRS;
}
return false;
}
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 05210b25e4..1b9bde711c 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
@@ -215,25 +215,25 @@ public final class FeatureOperations extends Static {
}
}
}
- return POOL.unique(new StringJoinOperation(identification, delimiter,
prefix, suffix, singleAttributes));
+ return POOL.unique(new StringJoinOperation(identification, delimiter,
prefix, suffix, singleAttributes, null));
}
/**
* Creates an operation computing the envelope that encompass all
geometries found in the given attributes.
- * Geometries can be in different coordinate reference systems; they will
be transformed to the first non-null
- * CRS in the following choices:
+ * Geometries can be in different coordinate reference systems, in which
case they will be transformed to
+ * the first non-null <abbr>CRS</abbr> in the following choices:
*
* <ol>
- * <li>the CRS specified to this method,</li>
- * <li>the CRS of the default geometry, or</li>
- * <li>the CRS of the first non-empty geometry.</li>
+ * <li>the <abbr>CRS</abbr> specified to this method,</li>
+ * <li>the <abbr>CRS</abbr> of the default geometry, or</li>
+ * <li>the <abbr>CRS</abbr> of the first non-empty geometry.</li>
* </ol>
*
* The {@linkplain AbstractOperation#getResult() result} of this operation
is an {@code Attribute}
* with values of type {@link org.opengis.geometry.Envelope}. If the
{@code crs} argument given to
* this method is non-null, then the
* {@linkplain
org.apache.sis.geometry.GeneralEnvelope#getCoordinateReferenceSystem() envelope
CRS}
- * will be that CRS.
+ * will be that <abbr>CRS</abbr>.
*
* <h4>Limitations</h4>
* If a geometry contains other geometries, this operation queries only
the envelope of the root geometry.
@@ -254,13 +254,13 @@ public final class FeatureOperations extends Static {
final PropertyType... geometryAttributes) throws FactoryException
{
ArgumentChecks.ensureNonNull("geometryAttributes", geometryAttributes);
- return POOL.unique(new EnvelopeOperation(identification, crs,
geometryAttributes));
+ return POOL.unique(new EnvelopeOperation(identification, crs,
geometryAttributes, null));
}
/**
* Creates a single geometry from a sequence of points or polylines stored
in another property.
* When evaluated, this operation reads a feature property containing a
sequence of {@code Point}s or {@code Polyline}s.
- * Those geometries shall be instances of the specified geometry library
(e.g. JTS or ESRI).
+ * Those geometries shall be instances of the specified geometry library
(e.g. <abbr>JTS</abbr> or <abbr>ESRI</abbr>).
* The merged geometry is usually a {@code Polyline},
* unless the sequence of source geometries is empty or contains a single
element.
* The merged geometry is re-computed every time that the operation is
evaluated.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index 00ad38f9d1..9e7de95a16 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
@@ -134,7 +134,7 @@ public final class Features extends Static {
* result type} is another operation, then the above check is
performed recursively.</li>
* </ul>
*
- * @param type the data type to express as an attribute type.
+ * @param type the data type to express as an attribute type, or {@code
null}.
* @return the attribute type, or empty if this method cannot find any.
*
* @since 1.1
@@ -156,7 +156,7 @@ public final class Features extends Static {
* result type} is another operation, then the above check is
performed recursively.</li>
* </ul>
*
- * @param type the data type to express as an attribute type.
+ * @param type the data type to express as an attribute type, or {@code
null}.
* @return the association role, or empty if this method cannot find any.
*
* @since 1.4
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
index 242418ac1c..7f603f4ca3 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
@@ -17,7 +17,7 @@
package org.apache.sis.feature;
import java.util.Map;
-import java.util.Collection;
+import java.util.Set;
import java.util.Iterator;
import java.util.EnumMap;
import org.opengis.parameter.ParameterDescriptorGroup;
@@ -28,6 +28,7 @@ import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.geometry.wrapper.GeometryType;
import org.apache.sis.geometry.wrapper.GeometryWrapper;
import org.apache.sis.setup.GeometryLibrary;
+import org.apache.sis.util.privy.CollectionsExt;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
@@ -86,7 +87,7 @@ final class GroupAsPolylineOperation extends
AbstractOperation {
* @param library the library providing the implementations of
geometry objects to read and write.
* @param components attribute, association or operation providing
the geometries to group as a polyline.
*/
- static Operation create(final Map<String,?> identification, final
GeometryLibrary library, PropertyType components) {
+ static AbstractOperation create(final Map<String,?> identification, final
GeometryLibrary library, PropertyType components) {
if (components instanceof LinkOperation) {
components = ((LinkOperation) components).result;
}
@@ -97,8 +98,7 @@ final class GroupAsPolylineOperation extends
AbstractOperation {
}
isFeatureAssociation = false;
} else {
- isFeatureAssociation = (components instanceof
FeatureAssociationRole)
- && ((FeatureAssociationRole)
components).getMaximumOccurs() == 1;
+ isFeatureAssociation = (components instanceof
FeatureAssociationRole);
if (!isFeatureAssociation) {
throw new
IllegalArgumentException(Resources.format(Resources.Keys.IllegalPropertyType_2,
components.getName(),
components.getClass()));
@@ -129,6 +129,32 @@ final class GroupAsPolylineOperation extends
AbstractOperation {
return EMPTY_PARAMS;
}
+ /**
+ * Returns the names of feature properties that this operation needs for
performing its task.
+ */
+ @Override
+ public Set<String> getDependencies() {
+ return Set.of(propertyName);
+ }
+
+ /**
+ * Returns the same operation but using different properties as inputs.
+ *
+ * @param dependencies the new properties to use as operation inputs.
+ * @return the new operation, or {@code this} if unchanged.
+ */
+ @Override
+ public Operation updateDependencies(final Map<String, PropertyType>
dependencies) {
+ final PropertyType target = dependencies.get(propertyName);
+ if (target != null) {
+ final AbstractOperation op = create(inherit(), geometries.library,
target);
+ if (!equals(op)) {
+ return FeatureOperations.POOL.unique(op);
+ }
+ }
+ return this;
+ }
+
/**
* Returns the expected result type.
*/
@@ -199,10 +225,10 @@ final class GroupAsPolylineOperation extends
AbstractOperation {
*/
private G compute() {
/*
- * Cast to `Collection` should be safe if the constructor
- * ensured that `Features.getMaximumOccurs(property) > 1`.
+ * The property value is usually cast directly to `Collection`
when the
+ * constructor ensured that `Features.getMaximumOccurs(property) >
1`.
*/
- Iterator<?> paths = ((Collection<?>)
feature.getPropertyValue(propertyName)).iterator();
+ Iterator<?> paths =
CollectionsExt.toCollection(feature.getPropertyValue(propertyName)).iterator();
if (isFeatureAssociation) {
final Iterator<?> it = paths;
paths = new Iterator<Object>() {
@@ -240,7 +266,7 @@ final class GroupAsPolylineOperation extends
AbstractOperation {
@Override
public boolean equals(final Object obj) {
if (super.equals(obj)) {
- final GroupAsPolylineOperation that = (GroupAsPolylineOperation)
obj;
+ final var that = (GroupAsPolylineOperation) obj;
return propertyName.equals(that.propertyName) &&
geometries.equals(that.geometries);
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
index 9bf239939c..63da5f3233 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
@@ -26,6 +26,7 @@ import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
import org.opengis.feature.Property;
import org.opengis.feature.PropertyType;
@@ -103,6 +104,21 @@ final class LinkOperation extends AbstractOperation {
return Set.of(referentName);
}
+ /**
+ * Returns the same operation but using different properties as inputs.
+ *
+ * @param dependencies the new properties to use as operation inputs.
+ * @return the new operation, or {@code this} if unchanged.
+ */
+ @Override
+ public Operation updateDependencies(final Map<String, PropertyType>
dependencies) {
+ final PropertyType target = dependencies.get(referentName);
+ if (target == null || target.equals(result)) {
+ return this;
+ }
+ return FeatureOperations.POOL.unique(new LinkOperation(inherit(),
target));
+ }
+
/**
* Returns the property from the referenced attribute of feature
association.
*
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 703c02faac..c1c035a7dd 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
@@ -151,8 +151,11 @@ final class StringJoinOperation extends AbstractOperation {
/**
* The property names as an unmodifiable set, created when first needed.
+ * This is simply {@link #attributeNames} copied in a unmodifiable set.
+ *
+ * @see #getDependencies()
*/
- private transient Set<String> dependencies;
+ private transient volatile Set<String> dependencies;
/**
* The type of the result returned by the string concatenation operation.
@@ -180,6 +183,12 @@ 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.
*
+ * @param identification the name and other information to be given to
this operation.
+ * @param delimiter the characters to use as delimiter between
each single property value.
+ * @param prefix characters to use at the beginning of the
concatenated string, or {@code null} if none.
+ * @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.
+ * @param inheritFrom existing operation from which to inherit null
attributes, or {@code null} if none.
* @throws UnconvertibleObjectException if at least one attributes is not
convertible from a string.
* @throws IllegalArgumentException if the operation failed for another
reason.
*
@@ -187,7 +196,8 @@ final class StringJoinOperation extends AbstractOperation {
*/
@SuppressWarnings({"rawtypes", "unchecked"})
// Generic array creation.
StringJoinOperation(final Map<String,?> identification, final String
delimiter,
- final String prefix, final String suffix, final PropertyType[]
singleAttributes)
+ final String prefix, final String suffix, final PropertyType[]
singleAttributes,
+ final StringJoinOperation inheritFrom)
{
super(identification);
attributeNames = new String[singleAttributes.length];
@@ -206,13 +216,19 @@ final class StringJoinOperation extends AbstractOperation
{
* combinations (e.g. operation producing an association).
*/
IdentifiedType propertyType = singleAttributes[i];
- ArgumentChecks.ensureNonNullElement("singleAttributes", i,
propertyType);
+ if (inheritFrom == null) {
+ ArgumentChecks.ensureNonNullElement("singleAttributes", i,
propertyType);
+ } else if (propertyType == null) {
+ attributeNames[i] = inheritFrom.attributeNames[i];
+ converters[i] = inheritFrom.converters[i];
+ continue;
+ }
final GenericName name = propertyType.getName();
int maximumOccurs = 0; // May be a
bitwise combination; need only to know if > 1.
PropertyNotFoundException cause = null; // In case of
failure to find "sis:identifier" property.
final boolean isAssociation = (propertyType instanceof
FeatureAssociationRole);
if (isAssociation) {
- final FeatureAssociationRole role = (FeatureAssociationRole)
propertyType;
+ final var role = (FeatureAssociationRole) propertyType;
final FeatureType ft = role.getValueType();
maximumOccurs = role.getMaximumOccurs();
try {
@@ -253,8 +269,12 @@ final class StringJoinOperation extends AbstractOperation {
}
converters[i] = converter;
}
- resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>(
- resultIdentification(identification), String.class, 1, 1,
null));
+ if (inheritFrom != null) {
+ resultType = inheritFrom.resultType;
+ } else {
+ resultType = FeatureOperations.POOL.unique(new
DefaultAttributeType<>(
+ resultIdentification(identification), String.class, 1, 1,
null));
+ }
this.delimiter = delimiter;
this.prefix = (prefix == null) ? "" : prefix;
this.suffix = (suffix == null) ? "" : suffix;
@@ -282,25 +302,49 @@ final class StringJoinOperation extends AbstractOperation
{
return resultType;
}
+ /**
+ * Returns the name of the properties from which to get the values to
concatenate.
+ * This is the same information as {@link #getDependencies()}, only in a
different
+ * kind of collection.
+ */
+ final List<String> getAttributeNames() {
+ return UnmodifiableArrayList.wrap(attributeNames);
+ }
+
/**
* Returns the names of feature properties that this operation needs for
performing its task.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
- public synchronized Set<String> getDependencies() {
- if (dependencies == null) {
- dependencies = CollectionsExt.immutableSet(true, attributeNames);
+ public Set<String> getDependencies() {
+ Set<String> cached = dependencies;
+ if (cached == null) {
+ // Not really a problem if computed twice concurrently.
+ dependencies = cached = CollectionsExt.immutableSet(true,
attributeNames);
}
- return dependencies;
+ return cached;
}
/**
- * Returns the name of the properties from which to get the values to
concatenate.
- * This is the same information as {@link #getDependencies()}, only in a
different
- * kind of collection.
+ * Returns the same operation but using different properties as inputs.
+ *
+ * @param dependencies the new properties to use as operation inputs.
+ * @return the new operation, or {@code this} if unchanged.
*/
- final List<String> getAttributeNames() {
- return UnmodifiableArrayList.wrap(attributeNames);
+ @Override
+ public Operation updateDependencies(final Map<String, PropertyType>
dependencies) {
+ boolean hasNonNull = false;
+ final var singleAttributes = new PropertyType[attributeNames.length];
+ for (int i=0; i < singleAttributes.length; i++) {
+ hasNonNull |= (singleAttributes[i] =
dependencies.get(attributeNames[i])) != null;
+ }
+ if (hasNonNull) {
+ final var op = new StringJoinOperation(inherit(), delimiter,
prefix, suffix, singleAttributes, this);
+ if (!(Arrays.equals(op.attributeNames, attributeNames) &&
Arrays.equals(op.converters, converters))) {
+ return FeatureOperations.POOL.unique(op);
+ }
+ }
+ return this;
}
/**
@@ -356,7 +400,7 @@ final class StringJoinOperation extends AbstractOperation {
*/
@Override
public String getValue() throws UnconvertibleObjectException {
- final StringBuilder sb = new StringBuilder();
+ final var sb = new StringBuilder();
String sep = prefix;
String name = null;
Object value = null;
@@ -421,7 +465,7 @@ final class StringJoinOperation extends AbstractOperation {
* read them (no need to store the substrings) but do not store
them in the properties
* before we succeeded to parse all values, so we have a "all or
nothing" behavior.
*/
- final Object[] values = new Object[attributeNames.length];
+ final var values = new Object[attributeNames.length];
int lower = prefix.length();
int upper = lower;
int count = 0;
@@ -520,7 +564,7 @@ final class StringJoinOperation extends AbstractOperation {
public boolean equals(final Object obj) {
if (super.equals(obj)) {
// 'this.result' is compared (indirectly) by the super class.
- final StringJoinOperation that = (StringJoinOperation) obj;
+ final var that = (StringJoinOperation) obj;
return Arrays.equals(this.attributeNames, that.attributeNames) &&
Arrays.equals(this.converters, that.converters) &&
Objects.equals(this.delimiter, that.delimiter) &&
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
index 88a470d172..7a6a656f7e 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
@@ -16,15 +16,14 @@
*/
package org.apache.sis.feature.builder;
+import java.util.HashMap;
import java.util.Objects;
import org.opengis.util.GenericName;
-import org.apache.sis.feature.Features;
-import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.AbstractOperation;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.PropertyType;
-import org.opengis.feature.Operation;
/**
@@ -61,35 +60,37 @@ final class OperationWrapper extends PropertyTypeBuilder {
/**
* Returns the operation or an updated version of the operation.
- * Updated versions are created only for some kinds of operation,
described below.
+ * Updated versions are created for some kinds of operation, described
below.
* Otherwise, this method returns the same value as {@link #build()}.
*
- * <h4>Links</h4>
+ * <h4>Updated operations</h4>
* If the operation is a link to another property of the feature to build,
the result type
* of the original operation is replaced by the target of the link in the
feature to build.
* Even if the attribute name is the same, sometime the value class or
some characteristics
- * are different.
+ * are different. Similar updates may also be applied to other kinds of
operation.
*
* @throws IllegalStateException if the builder contains inconsistent
information.
*/
@Override
final PropertyType buildForFeature() {
final FeatureTypeBuilder owner = owner();
- try {
- return Features.getLinkTarget(operation).<PropertyType>map((name)
-> {
- final PropertyTypeBuilder target = owner.getProperty(name);
+ if (operation instanceof AbstractOperation) {
+ final var op = (AbstractOperation) operation;
+ final var dependencies = new HashMap<String, PropertyType>();
+ for (final String name : op.getDependencies()) {
+ final PropertyTypeBuilder target;
+ try {
+ target = owner.getProperty(name);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
if (target != null) {
- final PropertyType result = target.build();
- if (!result.equals(((Operation) operation).getResult())) {
- initialize(operation);
- return FeatureOperations.link(identification(),
result);
- }
+ dependencies.put(name, target.build());
}
- return null;
- }).orElse(operation);
- } catch (IllegalArgumentException e) {
- throw new IllegalStateException(e.getMessage(), e);
+ }
+ return op.updateDependencies(dependencies);
}
+ return operation;
}
/**
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index b887eda202..93914b7037 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -134,6 +134,11 @@ public class Resources extends IndexedResourceBundle {
*/
public static final short CanNotVisit_2 = 77;
+ /**
+ * Cannot rename the “{0}” property as “{1}” because it is used by an
operation.
+ */
+ public static final short CannotRenameDependency_2 = 93;
+
/**
* The two categories “{0}” and “{2}” have overlapping ranges: {1} and
{3} respectively.
*/
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index 2dd17c3f46..f03d695a80 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -30,6 +30,7 @@ CanNotCreateTwoDimensionalCRS_1 = Cannot create a
two-dimensional reference sy
CanNotEnumerateValuesInRange_1 = Cannot enumerate values in the {0} range.
CanNotInstantiateProperty_1 = Property \u201c{0}\u201d is not a type
that can be instantiated.
CanNotMapToGridDimensions = Some envelope dimensions cannot be mapped
to grid dimensions.
+CannotRenameDependency_2 = Cannot rename the \u201c{0}\u201d property
as \u201c{1}\u201d because it is used by an operation.
CanNotSetCharacteristics_2 = Cannot set a value of type \u2018{1}\u2019
to characteristic \u201c{0}\u201d.
CanNotSetDerivedGridProperty_1 = Cannot set this derived grid property
after a call to \u201c{0}\u201d method.
CanNotSetPropertyValue_1 = Type of the \u201c{0}\u201d property does
not allow to set a value.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index 13996cde79..fa880920db 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -35,6 +35,7 @@ CanNotCreateTwoDimensionalCRS_1 = Ne peut pas cr\u00e9er un
syst\u00e8me de r\
CanNotEnumerateValuesInRange_1 = Ne peut pas \u00e9num\u00e9rer les valeurs
dans la plage {0}.
CanNotInstantiateProperty_1 = La propri\u00e9t\u00e9
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas d\u2019un type qui peut \u00eatre
instanci\u00e9.
CanNotMapToGridDimensions = Certaines dimensions de l\u2019enveloppe
ne correspondent pas \u00e0 des dimensions de la grille.
+CannotRenameDependency_2 = Ne peut pas renommer la
propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb comme
\u00ab\u202f{1}\u202f\u00bb parce qu\u2019elle est utilis\u00e9e par une
op\u00e9ration.
CanNotSetCharacteristics_2 = Ne peut pas assigner une valeur de type
\u2018{1}\u2019 \u00e0 la caract\u00e9ristique \u00ab\u202f{0}\u202f\u00bb.
CanNotSetDerivedGridProperty_1 = Ne peut pas d\u00e9finir cette
propri\u00e9t\u00e9 de la grille d\u00e9riv\u00e9e apr\u00e8s un appel \u00e0
la m\u00e9thode \u00ab\u202f{0}\u202f\u00bb.
CanNotSetPropertyValue_1 = Le type de la propri\u00e9t\u00e9
\u00ab\u202f{0}\u202f\u00bb ne permet pas de d\u00e9finir une valeur.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
index c2dcf01e13..51ab93d85f 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import java.util.Objects;
@@ -34,6 +35,7 @@ import org.apache.sis.feature.builder.AssociationRoleBuilder;
import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.ArgumentCheckByAssertion;
import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.privy.Strings;
@@ -197,13 +199,13 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* This method may return {@code null} if it cannot resolve the property
type, in which case
* the caller should throw an exception (throwing an exception is left to
the caller because
* it can produces a better error message). Operation's dependencies, if
any, are added into
- * the given {@code deferred} list.
+ * the given {@code deferred} set.
*
* @param property the {@linkplain #source} property to add.
* @param deferred where to add operation's dependencies, or {@code
null} for not collecting dependencies.
* @return builder for the projected property, or {@code null} if it
cannot be resolved.
*/
- private PropertyTypeBuilder addPropertyResult(PropertyType property, final
List<String> deferred) {
+ private PropertyTypeBuilder addPropertyResult(PropertyType property, final
Collection<String> deferred) {
if (property instanceof Operation) {
final GenericName name = property.getName();
do {
@@ -246,7 +248,7 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
return null;
}
final PropertyTypeBuilder builder;
- List<String> deferred;
+ final Collection<String> deferred;
if (sourceIsDependency) {
/*
* Adding a property which is not defined in the feature type
specified at construction time,
@@ -257,19 +259,17 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
reserve(property.getName(), null);
deferred = new ArrayList<>();
builder = addPropertyResult(property, deferred);
- } else {
+ } else if (property instanceof AbstractOperation) {
/*
- * For link operations, remember the dependencies in order to
determine (after we added all properties)
+ * For operations, remember the dependencies in order to determine
(after we added all properties)
* if we can keep the property as an operation or if we will need
to copy the value in an attribute.
- * For other kind of operations, unconditionally replace the
operation by its result.
+ * If the operation is not an `AbstractOperation`, unconditionally
replace operation by its result.
*/
- deferred = Features.getLinkTargets(property);
- if (deferred.isEmpty()) {
- deferred = new ArrayList<>();
- builder = addPropertyResult(property, deferred);
- } else {
- builder = addProperty(property);
- }
+ deferred = ((AbstractOperation) property).getDependencies();
+ builder = addProperty(property);
+ } else {
+ deferred = new ArrayList<>();
+ builder = addPropertyResult(property, deferred);
}
final var item = new Item(named ? property.getName() : null, builder);
requested.add(item);
@@ -338,9 +338,9 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
private boolean preferCurrentName;
/**
- * Whether this property needs at least one dependency which is not
included in the list of properties
- * requested by the user. In such case, we cannot keep the link
operation and need to replace the link
- * by a stored attribute.
+ * Whether this property is an operation having at least one
dependency which is not included
+ * in the list of properties requested by the user. In such case, we
cannot keep the operation
+ * and need to replace it by a stored attribute.
*
* @see #replaceIfMissingDependency()
*/
@@ -453,7 +453,10 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
final Class<?> c = ((AttributeType<?>)
result).getValueClass();
final Class<?> r = type.apply(c);
if (r != null) {
- // We can be lenient for link operation, but must
be strict for other operations.
+ /*
+ * We can be lenient for link operation, but must
be strict for other operations.
+ * Example: a link to a geometry, but relaxing the
`Polygon` type to `Geometry`.
+ */
if (Features.getLinkTarget(property).isPresent() ?
r.isAssignableFrom(c) : r.equals(c)) {
return true;
}
@@ -630,6 +633,7 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* The elements added into {@code deferred} are {@linkplain #source}
properties.
*
* @param deferred where to add missing transitive dependencies (source
properties).
+ * @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
*/
private void resolveDependencies(final List<PropertyType> deferred) {
final var it = dependencies.entrySet().iterator();
@@ -640,12 +644,8 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
Item item = reservedNames.get(sourceName);
if (item != null) {
if (!sourceName.equals(item.sourceName)) {
- /*
- * If we want to support that feature in a future version,
we would need a `replace` method
- * for replacing a builder at a specific index or for a
specific property name. A difficulty
- * is that for compound identifiers, we have no API for
reusing the same prefix and suffix.
- */
- throw new UnsupportedOperationException("Renaming of
properties used in links is not yet supported.");
+ throw new
UnsupportedOperationException(Resources.forLocale(getLocale())
+
.getString(Resources.Keys.CannotRenameDependency_2, item.sourceName,
sourceName));
}
} else {
for (Item dependent : entry.getValue()) {
@@ -698,11 +698,12 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* for meaning that this projection does nothing.
*
* @return the feature types with and without dependencies, or empty if
there is no projection.
+ * @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
*/
public Optional<FeatureProjection> project() {
requested.forEach(Item::validateName);
/*
- * Add properties for all dependencies that are required by link
operations but are not already present.
+ * Add properties for all dependencies that are required by operations
but are not already present.
* If there is no need to add anything, `typeWithDependencies` will be
directly the feature type to return.
*/
final List<PropertyTypeBuilder> properties = properties();
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 80dec5de00..4412e94419 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
@@ -43,6 +43,7 @@ import org.apache.sis.pending.jdk.JDK19;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Emptiable;
+import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.iso.Names;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -51,6 +52,7 @@ import org.opengis.feature.FeatureType;
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Filter;
import org.opengis.filter.Expression;
@@ -619,6 +621,9 @@ public class FeatureQuery extends Query implements
Cloneable, Emptiable, Seriali
*
* @param builder the builder where to add the property.
* @return whether the property has been successfully added.
+ * @throws InvalidFilterValueException if {@linkplain #expression} is
invalid.
+ * @throws PropertyNotFoundException if the property was not found in
{@code builder.source()}.
+ * @throws UnconvertibleObjectException if the property default value
cannot be converted to the expected type.
*/
final boolean addTo(final FeatureProjectionBuilder builder) {
final FeatureExpression<? super Feature, ?> fex =
FeatureExpression.castOrCopy(expression);
@@ -743,8 +748,12 @@ public class FeatureQuery extends Query implements
Cloneable, Emptiable, Seriali
* <li>Otherwise the localized string "Unnamed #1" with increasing
numbers.</li>
* </ul>
*
- * @param sourceType the feature type to project.
- * @param locale locale for error messages, or {@code null} for the
default locale.
+ * @param sourceType the feature type to project.
+ * @param locale locale for error messages, or {@code null} for the
default locale.
+ * @throws InvalidFilterValueException if an {@linkplain
NamedExpression#expression expression} is invalid.
+ * @throws PropertyNotFoundException if a property referenced by an
expression was not found in {@code sourceType}.
+ * @throws UnconvertibleObjectException if a property default value cannot
be converted to the expected type.
+ * @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
*/
final Optional<FeatureProjection> project(final FeatureType sourceType,
final Locale locale) {
if (projection == null) {
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
index b500df4042..29b6fad9f1 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
@@ -101,7 +101,6 @@ public interface FeatureSet extends DataSet {
* @param query definition of feature and feature properties filtering
applied at reading time.
* @return resulting subset of features (never {@code null}).
* @throws UnsupportedQueryException if this {@code FeatureSet} cannot
execute the given query.
- * This includes query validation errors.
* @throws DataStoreException if another error occurred while processing
the query.
*
* @see GridCoverageResource#subset(CoverageQuerty)
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index c4d4e792a7..3838769fa7 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@ -97,8 +97,8 @@ final class FeatureSubset extends AbstractFeatureSet {
try {
projection = query.project(type,
listeners.getLocale()).orElse(null);
resultType = (projection != null) ? projection.typeRequested :
type;
- } catch (IllegalArgumentException e) {
- throw new
DataStoreContentException(Resources.forLocale(listeners.getLocale())
+ } catch (RuntimeException e) { // Too many exceptions for
listing them all.
+ throw new
UnsupportedQueryException(Resources.forLocale(listeners.getLocale())
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);
}
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
index 1444533915..b0a5d879ac 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
@@ -19,6 +19,18 @@ package org.apache.sis.storage;
/**
* Thrown when a resources cannot be filtered with a given query.
+ * Some examples of cases where this exception may be thrown are:
+ *
+ * <ul>
+ * <li>The {@link Query} is an instance of a class unsupported by the
resource
+ * (for example, applying a {@link CoverageQuery} on a {@link
FeatureSet}).</li>
+ * <li>The query is requesting a property that does not exist in the {@link
Resource}.</li>
+ * <li>The values in the {@link DataStore} are unconvertible to some
characteristics
+ * (e.g., type or name) requested by the query.</li>
+ * </ul>
+ *
+ * This exception may be thrown when {@link FeatureSet#subset(Query)} is
executed,
+ * but may also be deferred until another method of the returned subset is
invoked.
*
* @author Johann Sorel (Geomatys)
* @version 0.8
diff --git
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
index 6bd071f8f3..80ea1a7acf 100644
---
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
+++
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
@@ -459,7 +459,7 @@ public final class FeatureQueryTest extends TestCase {
virtualProjection(ff.property("valueMissing",
Integer.class), "renamed1"));
assertXPathsEqual("value1", "valueMissing");
- var exception = assertThrows(DataStoreContentException.class,
this::executeAndGetFirst);
+ var exception = assertThrows(UnsupportedQueryException.class,
this::executeAndGetFirst);
assertMessageContains(exception);
}
}
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
index d77a06786f..c1a56e0023 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
@@ -21,6 +21,7 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
+import java.util.LinkedHashMap;
import java.util.NoSuchElementException;
@@ -40,8 +41,8 @@ public final class JDK21 {
* Placeholder for {@code SequencedCollection.getFirst()}.
*
* @param <E> type of elements in the collection.
- * @param sequenced the sequenced collection for which to get elements
in reverse order.
- * @return elements of the given collection in reverse order.
+ * @param sequenced the sequenced collection for which from which to get
an element.
+ * @return the requested element.
*/
public static <E> E getFirst(final List<E> sequenced) {
try {
@@ -55,8 +56,8 @@ public final class JDK21 {
* Placeholder for {@code SequencedCollection.getLast()}.
*
* @param <E> type of elements in the collection.
- * @param sequenced the sequenced collection for which to get elements
in reverse order.
- * @return elements of the given collection in reverse order.
+ * @param sequenced the sequenced collection for which from which to get
an element.
+ * @return the requested element.
*/
public static <E> E getLast(final List<E> sequenced) {
try {
@@ -66,6 +67,21 @@ public final class JDK21 {
}
}
+ /**
+ * Placeholder for {@code SequencedMap.putFirst(K, V)}.
+ *
+ * @param <K> type of keys in the map.
+ * @param <V> type of values in the map.
+ * @param sequenced the sequenced map for which to put an element first.
+ */
+ public static <K,V> void putFirst(final LinkedHashMap<K,V> sequenced,
final K key, final V value) {
+ @SuppressWarnings("unchecked")
+ final var copy = (LinkedHashMap<K,V>) sequenced.clone();
+ sequenced.clear();
+ sequenced.put(key, value);
+ sequenced.putAll(copy);
+ }
+
/**
* Placeholder for {@code SequencedCollection.reversed()}.
*
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
index 9d2068257a..1b84986236 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
@@ -303,7 +303,7 @@ public final class CollectionsExt extends Static {
final Class<?> valueType = value.getClass();
if (valueType.isArray()) {
if (type.isAssignableFrom(valueType)) {
- final Set<E> set = new LinkedHashSet<>(Arrays.asList((E[])
value));
+ final var set = new LinkedHashSet<E>(Arrays.asList((E[])
value));
set.remove(null);
return set.toArray(emptyArray);
}
@@ -725,15 +725,15 @@ public final class CollectionsExt extends Static {
}
}
if (value instanceof Iterable<?>) {
- final List<Object> list = new ArrayList<>();
+ final var list = new ArrayList<Object>();
for (final Object element : (Iterable<?>) value) {
list.add(element);
}
return list;
}
if (value instanceof Iterator<?>) {
- final Iterator<?> it = (Iterator<?>) value;
- final List<Object> list = new ArrayList<>();
+ final var it = (Iterator<?>) value;
+ final var list = new ArrayList<Object>();
while (it.hasNext()) {
list.add(it.next());
}