This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits 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 5a3b53f3e1 Fix of a `ClassCastException` during the merge of two
metadata. If the source metadata is a class that implements more than one
metadata interface (for example, the GPX metadata), use the type of the target
class or use the type of the property for resolving the ambiguity.
5a3b53f3e1 is described below
commit 5a3b53f3e1ee7980c6433f5fdb4f40157ff6cf05
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jul 19 19:50:13 2026 +0200
Fix of a `ClassCastException` during the merge of two metadata.
If the source metadata is a class that implements more than one metadata
interface
(for example, the GPX metadata), use the type of the target class or use
the type
of the property for resolving the ambiguity.
---
.../org/apache/sis/metadata/AbstractMetadata.java | 5 +-
.../org/apache/sis/metadata/MetadataStandard.java | 74 ++++++++++++++++---
.../apache/sis/metadata/ModifiableMetadata.java | 46 +++++++++++-
.../org/apache/sis/metadata/PropertyAccessor.java | 3 +-
.../sis/metadata/internal/shared/Merger.java | 82 +++++++++++++++++----
.../apache/sis/metadata/MetadataStandardTest.java | 12 +--
.../sis/metadata/internal/shared/MergerTest.java | 24 +++---
.../org/apache/sis/storage/gpx/MetadataTest.java | 8 +-
.../org/apache/sis/storage/gpx/ReaderTest.java | 85 +++++++++++++++++-----
.../sis/storage/metadata/MetadataBuilder.java | 2 +-
.../main/org/apache/sis/util/Classes.java | 9 ++-
11 files changed, 278 insertions(+), 72 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
index 44b6af2dce..076d3b13f1 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
@@ -357,11 +357,12 @@ public abstract class AbstractMetadata implements
LenientComparable, Emptiable {
* @param mode the strictness level of the comparison.
* @return {@code true} if the given object is equal to this metadata.
*
- * @see MetadataStandard#equals(Object, Object, ComparisonMode)
+ * @see MetadataStandard#equals(Object, Object, Class, ComparisonMode)
*/
@Override
public boolean equals(final Object object, final ComparisonMode mode) {
- return getStandard().equals(this, object, mode);
+ final MetadataStandard standard = getStandard();
+ return standard.equals(this, object,
standard.getInterface(getClass()), mode);
}
/**
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
index 5746d44b2f..da2e62ba32 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
@@ -374,6 +374,8 @@ public class MetadataStandard implements Serializable {
* @param propertyType base class of the metadata object, or {@code
null} if unknown.
* @param mandatory whether this method shall throw an exception if
no accessor is found.
* @return the accessor for the given object, or {@code null} if none and
{@code mandatory} is {@code false}.
+ * @throws ClassCastException if the metadata object does not implement an
expected metadata interface
+ * and {@code mandatory} is {@code true}.
*/
final PropertyAccessor getInstanceAccessor(final Object metadata, Class<?>
propertyType, final boolean mandatory) {
if (propertyType == null) {
@@ -637,19 +639,21 @@ public class MetadataStandard implements Serializable {
/**
* Returns the metadata interface implemented by the specified
implementation class.
- * If the given type is already an interface from this standard, then it
is returned
- * unchanged.
+ * If the given type is already an interface from this standard, then that
type is returned directly.
+ * If the given type does not implement an interface from this standard,
then a {@link ClassCastException} is thrown.
+ * If the given type implements more than one interface from this
standard, then a {@link ClassCastException} is also
+ * thrown because of the ambiguity.
*
* <div class="note"><b>Note:</b>
- * The word "interface" may be taken in a looser sense than the usual Java
sense because
- * if the given type is defined in this standard package, then it is
returned unchanged.
- * The standard package is usually made of interfaces and code lists only,
but this is
- * not verified by this method.</div>
+ * in this context, "interface" should be understood as
+ * "the type which is defining the public <abbr>API</abbr>".
+ * This is usually an interface in the Java sense, but not always.</div>
*
* @param <T> the compile-time {@code type}.
* @param type the implementation class.
* @return the interface implemented by the given implementation class.
- * @throws ClassCastException if the specified implementation class does
not implement an interface of this standard.
+ * @throws ClassCastException if the specified implementation class does
not implement exactly one interface
+ * of this standard,
*
* @see AbstractMetadata#getStandardType()
*/
@@ -657,6 +661,32 @@ public class MetadataStandard implements Serializable {
return getInterface(new CacheKey(Objects.requireNonNull(type),
Object.class), null);
}
+ /**
+ * Returns the metadata interface by choosing in a subset of the
interfaces implemented by the given class.
+ * This method does the same work as {@link #getInterface(Class)} except
that it ignores all interfaces that
+ * are not assignable to {@code baseType}.
+ * This filtering can avoid some ambiguities when the same class
implements many interfaces.
+ *
+ * @param <T> the compile-time {@code type}.
+ * @param type the implementation class.
+ * @param baseType base type of the metadata of interest, or {@code
null} if unspecified.
+ * @return the interface implemented by the given implementation class.
+ * @throws ClassCastException if the specified implementation class is not
assignable to {@code baseType},
+ * or if the class does not implement exactly one interface which
is a subtype of {@code baseType}.
+ *
+ * @since 1.7
+ */
+ public <T> Class<? super T> getInterface(final Class<T> type, Class<?>
baseType) {
+ if (baseType == null) {
+ baseType = Object.class;
+ }
+ final var key = new CacheKey(Objects.requireNonNull(type), baseType);
+ if (key.isValid()) {
+ return getInterface(key, null);
+ }
+ throw new ClassCastException(key.invalid());
+ }
+
/**
* Implementation of {@link #getInterface(Class)} with the possibility to
specify the property type.
* We do not provide the additional functionality of this method in public
API on the assumption that
@@ -1143,9 +1173,35 @@ public class MetadataStandard implements Serializable {
* @return {@code true} if the given metadata objects are equals or if the
two arguments are {@code null}.
* @throws ClassCastException if {@code metadata1} does not implement an
expected metadata interface.
*
- * @see AbstractMetadata#equals(Object, ComparisonMode)
+ * @deprecated Replaced by {@link #equals(Object, Object, Class,
ComparisonMode)} because this method
+ * is ambiguous when one of the given metadata instances implements more
than one metadata interface.
*/
+ @Deprecated(since="1.7", forRemoval=true)
public boolean equals(final Object metadata1, final Object metadata2,
final ComparisonMode mode) {
+ return equals(metadata1, metadata2, null, mode);
+ }
+
+ /**
+ * Compares the two specified metadata instances for equality.
+ * Unless the {@code mode} argument is {@link ComparisonMode#STRICT},
+ * the two metadata objects do not need to be instances of the same class.
+ * However, {@code metadata1} shall implement an interface defined by this
{@code MetadataStandard},
+ * otherwise a {@link ClassCastException} may be thrown. If {@code
metadata1} implements more than
+ * one interface managed by this {@code MetadataStandard}, the ambiguity
can be resolved by specifying
+ * the interface of interest (or a non-ambiguous super-type of it) in the
{@code baseType} argument.
+ *
+ * @param metadata1 the first metadata object to compare, or {@code
null}.
+ * @param metadata2 the second metadata object to compare, or {@code
null}.
+ * @param baseType base type of the metadata of interest, or {@code
null} if unspecified.
+ * @param mode the strictness level of the comparison.
+ * @return {@code true} if the given metadata objects are equals or if the
two arguments are {@code null}.
+ * @throws ClassCastException if {@code metadata1} does not implement an
expected metadata interface.
+ *
+ * @see AbstractMetadata#equals(Object, ComparisonMode)
+ *
+ * @since 1.7
+ */
+ public boolean equals(final Object metadata1, final Object metadata2,
final Class<?> baseType, final ComparisonMode mode) {
if (metadata1 == metadata2) {
return true;
}
@@ -1157,7 +1213,7 @@ public class MetadataStandard implements Serializable {
if (type1 != type2 && mode == ComparisonMode.STRICT) {
return false;
}
- final PropertyAccessor accessor = getInstanceAccessor(metadata1, null,
true);
+ final PropertyAccessor accessor = getInstanceAccessor(metadata1,
baseType, true);
if (type1 != type2) {
final var key = new CacheKey(type2, accessor.type);
// Not strictly necessary, but can avoid the relatively costly
creation of new `PropertyAccessor`.
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
index f91f1d7001..aff8483b40 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
@@ -30,8 +30,10 @@ import jakarta.xml.bind.annotation.XmlTransient;
import org.opengis.util.CodeList;
import org.opengis.metadata.Metadata; // For javadoc
import org.apache.sis.util.Classes;
+import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.collection.Containers;
import org.apache.sis.metadata.internal.Resources;
+import org.apache.sis.metadata.internal.shared.Merger;
import org.apache.sis.system.Semaphores;
import org.apache.sis.pending.jdk.JDK19;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
@@ -80,7 +82,7 @@ import static
org.apache.sis.metadata.internal.shared.ImplementationHelper.value
* }
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
* @since 0.3
*/
@XmlTransient
@@ -145,6 +147,48 @@ public abstract class ModifiableMetadata extends
AbstractMetadata {
super(source);
}
+ /**
+ * Merges the data from the given source into this metadata.
+ * The result of the merge is stored in this metadata.
+ * The given {@code source} metadata is not modified.
+ *
+ * <p>For each non-null and {@linkplain ValueExistencePolicy#NON_EMPTY
non-empty} property
+ * value from the <var>source</var> metadata, the merge operation is
defined as below:</p>
+ *
+ * <ul>
+ * <li>If this metadata does not have a non-null and non-empty value for
the same property, then the
+ * reference to the value from the source metadata is stored
<em>as-is</em> in this metadata.</li>
+ * <li>Otherwise, if the target property value is a collection, then:
+ * <ul>
+ * <li>All source elements that are {@link
ComparisonMode#BY_CONTRACT equal by contract}
+ * to an existing target element are ignored.</li>
+ * <li>For each element of the source collection, a corresponding
element of the target collection is searched.
+ * A pair of source and target elements is established if the pair
meets all of the following conditions:
+ * <ul>
+ * <li>The {@linkplain MetadataStandard#getInterface(Class)
standard type} of the source element
+ * is assignable to the type of the target element.</li>
+ * <li>There is no conflict, <i>i.e.</i> no property value that
are not collection and not equal.</li>
+ * </ul>
+ * If such pair is found, then the merge operation if performed
recursively
+ * for that pair of source and target elements.</li>
+ * <li>All other source elements will be added as new elements in
the target collection.</li>
+ * </ul>
+ * </li>
+ * <li>Otherwise, the merge operation is performed recursively on
property values.</li>
+ * </ul>
+ *
+ * @param source the source metadata to merge into this metadata. Will
never be modified.
+ * @throws ClassCastException if {@code source} is not an instance
compatible with this metadata.
+ * @throws InvalidMetadataException if this metadata cannot hold all
{@code source} properties,
+ * for example because the source class is a more specialized type
than this metadata class.
+ * @throws IllegalArgumentException if this method detects a
cross-reference between source and this metadata.
+ *
+ * @since 1.7
+ */
+ public void merge(final Object source) {
+ new Merger(null).copy(source, this);
+ }
+
/**
* Whether the metadata is still editable or has been made final.
* New {@link ModifiableMetadata} instances are initially {@link #EDITABLE}
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
index 09d9eba38f..eee084fabe 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
@@ -65,7 +65,8 @@ import static
org.apache.sis.metadata.ValueExistencePolicy.isNullOrEmpty;
* <ul>
* <li>The standard properties defined by the GeoAPI (or other standard)
interfaces.
* Those properties are the only ones accessible by most methods in this
class, except
- * {@link #equals(Object, Object, ComparisonMode)} and {@link
#walkWritable(MetadataVisitor, Object, Object)}.</li>
+ * {@link #equals(Object, Object, ComparisonMode)} and
+ * {@link #walkWritable(MetadataVisitor, Object, Object)}.</li>
*
* <li>Extra properties defined by the {@link IdentifiedObject} interface.
Those properties
* invisible in the ISO 19115-1 model, but appears in ISO 19115-3 XML
marshalling. So we
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
index f2dac5a0e2..7cf88b7193 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
@@ -32,6 +32,9 @@ import org.apache.sis.metadata.KeyNamePolicy;
import org.apache.sis.metadata.ValueExistencePolicy;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.util.Classes;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.internal.shared.Unsafe;
import org.apache.sis.util.resources.Errors;
@@ -44,14 +47,16 @@ import org.apache.sis.util.resources.Errors;
* <ul>
* <li>If the target metadata does not have a non-null and non-empty value
for the same property, then the
* reference to the value from the source metadata is stored
<em>as-is</em> in the target metadata.</li>
- * <li>Otherwise if the target value is a collection, then:
+ * <li>Otherwise, if the target property value is a collection, then:
* <ul>
+ * <li>All source elements that are {@link ComparisonMode#BY_CONTRACT
equal by contract}
+ * to an existing target element are ignored.</li>
* <li>For each element of the source collection, a corresponding
element of the target collection is searched.
* A pair of source and target elements is established if the pair
meets all of the following conditions:
* <ul>
* <li>The {@linkplain MetadataStandard#getInterface(Class) standard
type} of the source element
* is assignable to the type of the target element.</li>
- * <li>There is no conflict, i.e. no property value that are not
collection and not equal.
+ * <li>There is no conflict, <i>i.e.</i> no property value that are
not collection and not equal.
* This condition can be modified by overriding {@link
#resolve(Object, ModifiableMetadata)}.</li>
* </ul>
* If such pair is found, then the merge operation if performed
recursively
@@ -59,12 +64,14 @@ import org.apache.sis.util.resources.Errors;
* <li>All other source elements will be added as new elements in the
target collection.</li>
* </ul>
* </li>
- * <li>Otherwise the {@link #copy(Object, ModifiableMetadata) copy(…)}
method is invoked.</li>
+ * <li>Otherwise, the {@link #copy(Object, ModifiableMetadata) copy(…)}
method is invoked.</li>
* </ul>
*
* @author Johann Sorel (Geomatys)
* @author Benjamin Garcia (Geomatys)
* @author Martin Desruisseaux (Geomatys)
+ *
+ * @see ModifiableMetadata#merge(Object)
*/
public class Merger {
/**
@@ -118,8 +125,11 @@ public class Merger {
*/
public final void copy(final Object source, final ModifiableMetadata
target) {
if (!copy(source, target, false)) {
- throw new
InvalidMetadataException(errors().getString(Errors.Keys.IllegalArgumentClass_3,
"target",
- target.getStandard().getInterface(source.getClass()),
Classes.getClass(target)));
+ throw new InvalidMetadataException(errors().getString(
+ Errors.Keys.IllegalArgumentClass_3,
+ "source",
+ target.getStandardType(),
+ Classes.getClass(source)));
}
}
@@ -139,13 +149,25 @@ public class Merger {
* taken will depend on the caller: it may either skips the value or
throws an exception.
*/
final MetadataStandard standard = target.getStandard();
- if (!standard.getInterface(source.getClass()).isInstance(target)) {
+ Class<?> baseType = Classes.getRawClass(target.getStandardType());
+deeper: if (!baseType.isInstance(source)) {
+ // The target class is too speclalized. Search for a more general
type.
+ for (final Class<?> parent : Classes.getAllInterfaces(baseType)) {
+ if (standard.isMetadata(parent) && parent.isInstance(source)) {
+ baseType = parent;
+ break deeper;
+ }
+ }
+ return false;
+ }
+ if (!standard.getInterface(source.getClass(),
baseType).isInstance(target)) {
return false;
}
/*
* Only after we verified that the merge operation is theoretically
allowed, remember that
* we are going to merge those two metadata and verify that we are not
in an infinite loop.
* We will also verify that the target metadata does not contain a
source, or vice-versa.
+ * Reminder: `done` values are `FALSE` for sources and `TRUE` for
targets.
*/
{ // For keeping `sourceDone` and `targetDone` more local.
final Boolean sourceDone = done.put(source, Boolean.FALSE);
@@ -171,7 +193,7 @@ public class Merger {
if (source instanceof AbstractMetadata) {
sourceMap = ((AbstractMetadata) source).asMap(); // Gives
to subclasses a chance to override.
} else {
- sourceMap = standard.asValueMap(source, null,
KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
+ sourceMap = standard.asValueMap(source, baseType,
KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
}
/*
* Iterate on source values in order to find the objects that need to
be copied or merged.
@@ -209,17 +231,49 @@ public class Merger {
/*
* If the target value is a collection, then the source
value should be a collection too
* (otherwise the two objects would not be implementing
the same standard, in which case
- * a ClassCastException is conform to this method
contract). The loop tries to merge the
- * source elements to target elements that are specialized
enough.
+ * a ClassCastException is conform to this method
contract). The list of source elements
+ * will exclude elements that are already present in the
target.
*/
final var targetList = (Collection<?>) targetValue;
final var sourceList = new LinkedList<>((Collection<?>)
sourceValue);
+ for (Iterator<?> it = sourceList.iterator();
it.hasNext();) {
+ final Object value = it.next();
+ for (final Object element : targetList) {
+ if (value != element) {
+ /*
+ * The comparisons must be done with
`BY_CONTRACT` mode because the source `value`
+ * may implement many interfaces, while we are
interrested only in the interface
+ * implemented by the target `element`.
+ */
+ final boolean equals;
+ final var criteria =
ComparisonMode.BY_CONTRACT;
+ if (element instanceof AbstractMetadata) {
+ equals = ((AbstractMetadata)
element).equals(value, criteria);
+ } else if (targetList instanceof
CheckedContainer<?>) {
+ Class<?> propertyType =
((CheckedContainer<?>) targetList).getElementType();
+ equals = standard.equals(element, value,
propertyType, criteria);
+ } else {
+ equals = Utilities.deepEquals(element,
value, criteria);
+ }
+ if (!equals) continue;
+ }
+ it.remove();
+ break;
+ }
+ }
+ if (sourceList.isEmpty()) {
+ continue;
+ }
+ /*
+ * Try to merge the source elements into target elements
that are specialized enough.
+ */
for (final Object element : targetList) {
if (element instanceof ModifiableMetadata) {
+ final var targetElement = (ModifiableMetadata)
element;
final Iterator<?> it = sourceList.iterator();
distribute: while (it.hasNext()) {
final Object value = it.next();
- switch (resolve(value, (ModifiableMetadata)
element)) {
+ switch (resolve(value, targetElement)) {
default: throw new
UnsupportedOperationException();
case SEPARATE: break; // do
nothing.
case MERGE: {
@@ -228,7 +282,7 @@ distribute: while (it.hasNext()) {
* by recursive checks in all
children. The intent is to have a "all or nothing"
* behavior, before the copy(…, false)
call below starts to modify the values.
*/
- if (!copy(value, (ModifiableMetadata)
element, false)) break;
+ if (!copy(value, targetElement,
false)) break;
// Fall through
}
case IGNORE: {
@@ -248,7 +302,7 @@ distribute: while (it.hasNext()) {
for (final Object element : sourceList) {
final Object old = targetMap.put(propertyName,
element);
if (old instanceof Collection<?>) {
- final Collection<?> oldList = (Collection<?>) old;
+ final var oldList = (Collection<?>) old;
if (oldList.size() <= targetList.size()) {
/*
* Above was only a cheap check based on
collection size only.
@@ -399,8 +453,8 @@ distribute: while (it.hasNext()) {
/**
* Invoked when a source metadata element is about to be written in an
existing target element.
* The default implementation returns {@link Resolution#MERGE} if writing
in the given target
- * would only fill holes, without overwriting any existing value.
Otherwise this method returns
- * {@code Resolution#SEPARATE}.
+ * would only fill holes (i.e. without overwriting any existing value),
+ * or returns {@code Resolution#SEPARATE} otherwise.
*
* @param source the source metadata to copy.
* @param target where the source metadata would be copied if this
method returns {@link Resolution#MERGE}.
diff --git
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
index b1df00a083..94d2da09df 100644
---
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
+++
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
@@ -186,7 +186,7 @@ public final class MetadataStandardTest extends TestCase {
}
/**
- * Tests the {@link MetadataStandard#equals(Object, Object,
ComparisonMode)} method.
+ * Tests the {@link MetadataStandard#equals(Object, Object, Class,
ComparisonMode)} method.
*/
@Test
public void testEquals() {
@@ -194,17 +194,17 @@ public final class MetadataStandardTest extends TestCase {
// Self equality test
DefaultCitation instance = HardCodedCitations.EPSG;
- assertFalse(standard.equals(instance, HardCodedCitations.SIS,
ComparisonMode.STRICT));
- assertTrue (standard.equals(instance, HardCodedCitations.EPSG,
ComparisonMode.STRICT));
+ assertFalse(standard.equals(instance, HardCodedCitations.SIS, null,
ComparisonMode.STRICT));
+ assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null,
ComparisonMode.STRICT));
// Test comparison with a copy
instance = new DefaultCitation(HardCodedCitations.EPSG);
- assertFalse(standard.equals(instance, HardCodedCitations.SIS,
ComparisonMode.STRICT));
- assertTrue (standard.equals(instance, HardCodedCitations.EPSG,
ComparisonMode.STRICT));
+ assertFalse(standard.equals(instance, HardCodedCitations.SIS, null,
ComparisonMode.STRICT));
+ assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null,
ComparisonMode.STRICT));
// test comparison with a modified copy
instance.setTitle(new SimpleInternationalString("A dummy title"));
- assertFalse(standard.equals(instance, HardCodedCitations.EPSG,
ComparisonMode.STRICT));
+ assertFalse(standard.equals(instance, HardCodedCitations.EPSG, null,
ComparisonMode.STRICT));
}
/**
diff --git
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
index e33959f466..9c79d7677b 100644
---
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
+++
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
@@ -60,10 +60,10 @@ public final class MergerTest extends TestCase {
* Creates a metadata sample with 3 content information of different kind.
*/
private static DefaultMetadata createSample1() {
- final DefaultFeatureCatalogueDescription features = new
DefaultFeatureCatalogueDescription();
- final DefaultCoverageDescription coverage = new
DefaultCoverageDescription();
- final DefaultImageDescription image = new
DefaultImageDescription();
- final DefaultMetadata metadata = new
DefaultMetadata();
+ final var features = new DefaultFeatureCatalogueDescription();
+ final var coverage = new DefaultCoverageDescription();
+ final var image = new DefaultImageDescription();
+ final var metadata = new DefaultMetadata();
features.setFeatureCatalogueCitations(Set.of(new
DefaultCitation("Shapefile")));
features.setIncludedWithDataset(Boolean.TRUE);
metadata.getContentInfo().add(features);
@@ -84,9 +84,9 @@ public final class MergerTest extends TestCase {
* than the one created by {@link #createSample1()}.
*/
private static DefaultMetadata createSample2() {
- final DefaultFeatureCatalogueDescription features = new
DefaultFeatureCatalogueDescription();
- final DefaultImageDescription image = new
DefaultImageDescription();
- final DefaultMetadata metadata = new
DefaultMetadata();
+ final var features = new DefaultFeatureCatalogueDescription();
+ final var image = new DefaultImageDescription();
+ final var metadata = new DefaultMetadata();
image.setProcessingLevelCode(new DefaultIdentifier("Level 2"));
metadata.getContentInfo().add(image);
@@ -106,7 +106,7 @@ public final class MergerTest extends TestCase {
public void testDeepMerge() {
final DefaultMetadata source = createSample1();
final DefaultMetadata target = createSample2();
- final Merger merger = new Merger(null);
+ final var merger = new Merger(null);
merger.copy(source, target);
assertSetEquals(List.of(Locale.JAPANESE, Locale.FRENCH),
@@ -114,10 +114,10 @@ public final class MergerTest extends TestCase {
assertSetEquals(List.of(StandardCharsets.UTF_16,
StandardCharsets.UTF_8),
target.getLocalesAndCharsets().values());
- final Iterator<ContentInformation> it =
target.getContentInfo().iterator();
- final ImageDescription image = (ImageDescription)
it.next();
- final FeatureCatalogueDescription features =
(FeatureCatalogueDescription) it.next();
- final CoverageDescription coverage = (CoverageDescription)
it.next();
+ final Iterator<ContentInformation> it =
target.getContentInfo().iterator();
+ final var image = assertInstanceOf(ImageDescription.class,
it.next());
+ final var features =
assertInstanceOf(FeatureCatalogueDescription.class, it.next());
+ final var coverage = assertInstanceOf(CoverageDescription.class,
it.next());
assertFalse(it.hasNext());
assertEquals(ImagingCondition.CLOUD, image .getImagingCondition());
diff --git
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
index bc10977143..7db1eba6ec 100644
---
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
+++
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
@@ -74,23 +74,23 @@ public final class MetadataTest extends TestCase {
* Creates a metadata instance with the same data as the one in the {@code
"1.1/metadata.xml"} test file.
*/
static Metadata create() throws URISyntaxException {
- final Person person = new Person();
+ final var person = new Person();
person.name = "Jean-Pierre";
person.email = "[email protected]";
person.link = new Link(new URI("http://someone-site.org"));
- final Copyright copyright = new Copyright();
+ final var copyright = new Copyright();
copyright.author = "Apache";
copyright.year = 2004;
copyright.license = new
URI("http://www.apache.org/licenses/LICENSE-2.0");
- final Bounds bounds = new Bounds();
+ final var bounds = new Bounds();
bounds.westBoundLongitude = -20;
bounds.eastBoundLongitude = 30;
bounds.southBoundLatitude = 10;
bounds.northBoundLatitude = 40;
- final Metadata metadata = new Metadata();
+ final var metadata = new Metadata();
metadata.name = "Sample";
metadata.description = "GPX test file";
metadata.author = person;
diff --git
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
index 4b5a69fc3b..56fcd3e904 100644
---
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
+++
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
@@ -24,15 +24,19 @@ import java.time.Instant;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.Polyline;
import org.opengis.geometry.Envelope;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.metadata.identification.Identification;
import org.apache.sis.setup.GeometryLibrary;
import org.apache.sis.storage.OptionKey;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.gps.Fix;
+import org.apache.sis.metadata.iso.DefaultMetadata;
// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.sis.test.Assertions.assertSingleton;
import static org.apache.sis.test.Assertions.assertSingletonFeature;
import org.apache.sis.test.TestCase;
@@ -90,13 +94,20 @@ public final class ReaderTest extends TestCase {
}
/**
- * Verifies that the given {@code actual} envelope has the expected values.
+ * Verifies that an envelope is two-dimensional and has the expected
values.
* A strict equality is requested.
+ *
+ * @param xmin the expected minimum value of the first coordinate.
+ * @param xmax the expected maximum value of the first coordinate.
+ * @param ymin the expected minimum value of the second coordinate.
+ * @param ymax the expected maximum value of the second coordinate.
+ * @param actual the feature from which to get the envelope to compare
with the expected values.
*/
private static void assertEnvelopeEquals(final double xmin, final double
xmax,
final double ymin, final double
ymax,
- final Envelope actual)
+ final Feature f)
{
+ final Envelope actual = assertInstanceOf(Envelope.class,
f.getPropertyValue("sis:envelope"));
assertEquals(2, actual.getDimension(), "dimension");
assertEquals(actual.getMinimum(0), xmin, "xmin");
assertEquals(actual.getMaximum(0), xmax, "xmax");
@@ -114,6 +125,13 @@ public final class ReaderTest extends TestCase {
assertEquals(expected, (actual != null) ? actual.toString() : null);
}
+ /**
+ * Gets the metadata from the given store.
+ */
+ private static Metadata getMetadata(final Store reader) throws
DataStoreException {
+ return assertInstanceOf(Metadata.class, reader.getMetadata());
+ }
+
/**
* Tests parsing of GPX version 1.0.0 metadata.
*
@@ -122,7 +140,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testMetadata100() throws DataStoreException {
try (Store reader = create(TestData.V1_0, TestData.METADATA)) {
- final Metadata md = (Metadata) reader.getMetadata();
+ final Metadata md = getMetadata(reader);
verifyMetadata(md, 1);
assertNull(md.author.link);
assertNull(md.copyright);
@@ -138,7 +156,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testMetadata110() throws DataStoreException {
try (Store reader = create(TestData.V1_1, TestData.METADATA)) {
- final Metadata md = (Metadata) reader.getMetadata();
+ final Metadata md = getMetadata(reader);
verifyMetadata(md, 3);
assertStringEquals("http://someone-site.org", md.author.link);
assertEquals("Apache", md.copyright.author);
@@ -199,6 +217,36 @@ public final class ReaderTest extends TestCase {
assertFalse(it.hasNext());
}
+ /**
+ * Tests the merging of two {@link Metadata} object.
+ * The difficulty is that, for implementation convenience, the same class
implements many metadata interfaces.
+ * The merge process should detect which interface to use based on the
type of the {@code merged} object as a
+ * starting point, then based on the type of each property to merge.
+ *
+ * @throws DataStoreException if reader failed to be created or failed at
reading.
+ */
+ @Test
+ public void testMetadataMerge() throws DataStoreException {
+ final Metadata first, second;
+ final DefaultMetadata merged;
+ try (Store reader = create(TestData.V1_1, TestData.METADATA)) {
+ first = getMetadata(reader);
+ try (Store other = create(TestData.V1_1, TestData.ROUTE)) {
+ second = getMetadata(other);
+ merged = new DefaultMetadata(first);
+ merged.merge(second);
+ }
+ }
+ // Both metadata declare this scope, only one instance should be
retained.
+ assertEquals(ScopeCode.DATASET,
assertSingleton(merged.getMetadataScopes()).getResourceScope());
+
+ // Should find the identification info of each dataset.
+ final Iterator<Identification> it =
merged.getIdentificationInfo().iterator();
+ assertSame(assertSingleton(first .getIdentificationInfo()), it.next());
+ assertSame(assertSingleton(second.getIdentificationInfo()), it.next());
+ assertFalse(it.hasNext());
+ }
+
/**
* Tests parsing of GPX version 1.0.0 way point.
*
@@ -207,7 +255,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testWayPoint100() throws DataStoreException {
try (Store reader = create(TestData.V1_0, TestData.WAYPOINT)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_0, reader.getVersion());
try (Stream<Feature> features = reader.features(false)) {
final Iterator<Feature> it = features.iterator();
@@ -227,7 +275,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testWayPoint110() throws DataStoreException {
try (Store reader = create(TestData.V1_1, TestData.WAYPOINT)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_1, reader.getVersion());
try (Stream<Feature> features = reader.features(false)) {
final Iterator<Feature> it = features.iterator();
@@ -247,7 +295,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testRoute100() throws DataStoreException {
try (Store reader = create(TestData.V1_0, TestData.ROUTE)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_0, reader.getVersion());
try (Stream<Feature> features = reader.features(false)) {
final Iterator<Feature> it = features.iterator();
@@ -266,7 +314,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testRoute110() throws DataStoreException {
try (Store reader = create(TestData.V1_1, TestData.ROUTE)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_1, reader.getVersion());
verifyRoute110(reader);
}
@@ -322,7 +370,7 @@ public final class ReaderTest extends TestCase {
assertEquals(new Point(15, 10), p.getPoint(0));
assertEquals(new Point(25, 20), p.getPoint(1));
assertEquals(new Point(35, 30), p.getPoint(2));
- assertEnvelopeEquals(15, 35, 10, 30, (Envelope)
f.getPropertyValue("sis:envelope"));
+ assertEnvelopeEquals(15, 35, 10, 30, f);
}
/**
@@ -352,7 +400,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testTrack100() throws DataStoreException {
try (Store reader = create(TestData.V1_0, TestData.TRACK)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_0, reader.getVersion());
try (Stream<Feature> features = reader.features(false)) {
final Iterator<Feature> it = features.iterator();
@@ -371,7 +419,7 @@ public final class ReaderTest extends TestCase {
@Test
public void testTrack110() throws DataStoreException {
try (Store reader = create(TestData.V1_1, TestData.TRACK)) {
- verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+ verifyAlmostEmptyMetadata(getMetadata(reader));
assertEquals(StoreProvider.V1_1, reader.getVersion());
try (Stream<Feature> features = reader.features(false)) {
final Iterator<Feature> it = features.iterator();
@@ -424,7 +472,7 @@ public final class ReaderTest extends TestCase {
assertEquals(new Point(15, 10), p.getPoint(0));
assertEquals(new Point(25, 20), p.getPoint(1));
assertEquals(new Point(35, 30), p.getPoint(2));
- assertEnvelopeEquals(15, 35, 10, 30, (Envelope)
f.getPropertyValue("sis:envelope"));
+ assertEnvelopeEquals(15, 35, 10, 30, f);
}
/**
@@ -464,7 +512,7 @@ public final class ReaderTest extends TestCase {
assertStringEquals("http://first-address2.org",
links.get(1));
assertStringEquals("http://first-address3.org",
links.get(2));
}
- assertEnvelopeEquals(15, 15, 10, 10, (Envelope)
f.getPropertyValue("sis:envelope"));
+ assertEnvelopeEquals(15, 15, 10, 10, f);
break;
}
case 1: {
@@ -488,7 +536,7 @@ public final class ReaderTest extends TestCase {
assertNull(f.getPropertyValue("ageofdgpsdata"));
assertNull(f.getPropertyValue("dgpsid"));
assertTrue(assertInstanceOf(List.class,
f.getPropertyValue("link")).isEmpty());
- assertEnvelopeEquals(25, 25, 20, 20, (Envelope)
f.getPropertyValue("sis:envelope"));
+ assertEnvelopeEquals(25, 25, 20, 20, f);
break;
}
case 2: {
@@ -517,7 +565,7 @@ public final class ReaderTest extends TestCase {
if (v11) {
assertStringEquals("http://third-address2.org",
links.get(1));
}
- assertEnvelopeEquals(35, 35, 30, 30, (Envelope)
f.getPropertyValue("sis:envelope"));
+ assertEnvelopeEquals(35, 35, 30, 30, f);
break;
}
default: {
@@ -534,7 +582,7 @@ public final class ReaderTest extends TestCase {
* @throws DataStoreException if reader failed to be created or failed at
reading.
*/
@Test
- public void testRouteSkipMetadata() throws DataStoreException {
+ public void testRouteIgnoringMetadata() throws DataStoreException {
try (Store reader = create(TestData.V1_1, TestData.ROUTE)) {
verifyRoute110(reader);
}
@@ -545,7 +593,7 @@ public final class ReaderTest extends TestCase {
* Using the URL makes easier for the data store to read the same data
more than once.
*/
private Store createFromURL() throws DataStoreException {
- final StorageConnector connector = new
StorageConnector(TestData.V1_1.getURL(TestData.ROUTE));
+ final var connector = new
StorageConnector(TestData.V1_1.getURL(TestData.ROUTE));
connector.setOption(OptionKey.GEOMETRY_LIBRARY, GeometryLibrary.ESRI);
connector.setOption(OptionKey.URL_ENCODING, "UTF-8");
return new Store(provider, connector);
@@ -568,7 +616,7 @@ public final class ReaderTest extends TestCase {
* The new 'features()' call should reuse the reader created by
'getMetadata()' - this can
* be verified by stepping in the code with a debugger.
*/
- md = (Metadata) reader.getMetadata();
+ md = getMetadata(reader);
verifyRoute110(reader);
/*
* One more check.
@@ -586,6 +634,7 @@ public final class ReaderTest extends TestCase {
* @throws DataStoreException if reader failed to be created or failed at
reading.
*/
@Test
+ @SuppressWarnings("ConvertToTryWithResources")
public void testConcurrentReads() throws DataStoreException {
try (Store reader = createFromURL()) {
final Stream<Feature> f1 = reader.features(false);
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
index 117500d026..878a6a8e9f 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
@@ -3411,7 +3411,7 @@ public class MetadataBuilder {
} else {
return false;
}
- final Merger merger = new Merger(locale);
+ final var merger = new Merger(locale);
merger.copy(source, target);
useParentElements();
return true;
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
index 13c3175627..6ae22a43c5 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
@@ -450,13 +450,14 @@ public final class Classes {
/**
* Returns every interfaces implemented, directly or indirectly, by the
given class or interface.
* This is similar to {@link Class#getInterfaces()} except that this
method searches recursively
- * in the super-interfaces. For example, if the given type is {@link
java.util.ArrayList}, then
- * the returned set will contain {@link java.util.List} (which is
implemented directly)
+ * in the super-interfaces, omitting duplicated values.
+ * For example, if the given type is {@link java.util.ArrayList},
+ * then the returned set will contain {@link java.util.List} (which is
implemented directly)
* together with its parent interfaces {@link Collection} and {@link
Iterable}.
*
* <h4>Elements ordering</h4>
* All interfaces implemented directly by the given type are first and in
the order they are declared
- * in the {@code implements} or {@code extends} clause. Parent interfaces
are next.
+ * in the {@code implements} or {@code extends} clause. Interfaces
implemented by parent interfaces are next.
*
* @param <T> the compile-time type of the {@code Class} argument.
* @param type the class or interface for which to get all implemented
interfaces, or {@code null}.
@@ -622,7 +623,7 @@ next: for (final Class<?> candidate : candidates) {
* @param c1 the first class, or {@code null}.
* @param c2 the second class, or {@code null}.
* @return the interfaces common to both classes, or an empty set if none.
- * Callers can freely modify the returned set.
+ * CThe returned set supports remove operation.
*/
public static Set<Class<?>> findCommonInterfaces(final Class<?> c1, final
Class<?> c2) {
final Class<?> baseType = Object.class; // Not configurable for
now, but could be an argument.