This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit c0a92a731189492536debd2697aca0c841a7e6a5 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Nov 19 20:05:27 2023 +0100 Add `MetadataStandard.asNilReasonMap(…)` method, together with support for classes that cannot implement the `NilObject` interface. As a side-effect of this work, other map views were refactored for putting more common code in the `PropertyMap` base class. --- .../org/apache/sis/metadata/AbstractMetadata.java | 30 +++ .../main/org/apache/sis/metadata/IndexMap.java | 33 +-- .../org/apache/sis/metadata/InformationMap.java | 31 +-- .../org/apache/sis/metadata/MetadataStandard.java | 35 +++ .../main/org/apache/sis/metadata/NameMap.java | 34 +-- .../main/org/apache/sis/metadata/NilReasonMap.java | 181 ++++++++++++++ .../org/apache/sis/metadata/PropertyAccessor.java | 26 +- .../main/org/apache/sis/metadata/PropertyMap.java | 265 ++++++++++++++++----- .../main/org/apache/sis/metadata/TypeMap.java | 33 +-- .../main/org/apache/sis/metadata/ValueMap.java | 228 ++---------------- .../org/apache/sis/metadata/iso/ISOMetadata.java | 2 +- .../org/apache/sis/metadata/NilReasonMapTest.java | 102 ++++++++ 12 files changed, 593 insertions(+), 407 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 5e5ac1ffcc..f3333c9a39 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 @@ -17,7 +17,9 @@ package org.apache.sis.metadata; import java.util.Map; +import java.util.HashMap; import jakarta.xml.bind.annotation.XmlTransient; +import org.apache.sis.xml.NilReason; import org.apache.sis.util.Emptiable; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.LenientComparable; @@ -77,6 +79,14 @@ import org.apache.sis.util.collection.TreeTable; */ @XmlTransient public abstract class AbstractMetadata implements LenientComparable, Emptiable { + /** + * The reasons why some mandatory properties are absent. This map is used only for values of + * classes that cannot be represented as instances of {@link org.apache.sis.xml.NilObject}. + * + * @see NilReasonMap + */ + HashMap<Integer,NilReason> nilReasons; + /** * Creates an initially empty metadata. */ @@ -332,4 +342,24 @@ public abstract class AbstractMetadata implements LenientComparable, Emptiable { public String toString() { return asTreeTable().toString(); } + + /** + * Returns a <em>shallow</em> clone of this metadata. The properties values and the children are not cloned. + * This method is for internal usage by {@link ModifiableMetadata} and should usually not be invoked directly. + * + * @return a shallow clone of this metadata. + * @throws CloneNotSupportedException if this metadata is not cloneable. + * + * @see ModifiableMetadata#deepCopy(ModifiableMetadata.State) + * @see MetadataCopier + */ + @Override + @SuppressWarnings("unchecked") + protected AbstractMetadata clone() throws CloneNotSupportedException { + final var clone = (AbstractMetadata) super.clone(); + if (clone.nilReasons != null) { + clone.nilReasons = (HashMap<Integer,NilReason>) clone.nilReasons.clone(); + } + return clone; + } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java index 379a6d3999..c7602348cf 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java @@ -16,10 +16,6 @@ */ package org.apache.sis.metadata; -import java.util.Map; -import java.util.Iterator; -import java.util.NoSuchElementException; - /** * Map of property indices for a given implementation class. This map is read-only. @@ -40,33 +36,10 @@ final class IndexMap extends PropertyMap<Integer> { } /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - */ - @Override - public Integer get(final Object key) { - if (key instanceof String) { - final int i = accessor.indexOf((String) key, false); - if (i >= 0) return i; - } - return null; - } - - /** - * Returns an iterator over the entries contained in this map. + * Returns the index for the property at the specified index. */ @Override - final Iterator<Map.Entry<String,Integer>> iterator() { - return new Iter() { - @Override - public Map.Entry<String,Integer> next() { - final int i = index++; - final String name = accessor.name(i, keyPolicy); - if (name != null) { - return new SimpleImmutableEntry<>(name, i); - } - throw new NoSuchElementException(); - } - }; + final Integer getReflectively(final int index) { + return (index >= 0) ? index : null; } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java index 3e621e0777..49fafb0eba 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java @@ -16,9 +16,6 @@ */ package org.apache.sis.metadata; -import java.util.Map; -import java.util.Iterator; -import java.util.NoSuchElementException; import org.opengis.metadata.ExtendedElementInformation; import org.opengis.metadata.citation.Citation; @@ -51,32 +48,10 @@ final class InformationMap extends PropertyMap<ExtendedElementInformation> { } /** - * Returns the information to which the specified key is mapped, - * or {@code null} if this map contains no mapping for the key. + * Returns the information for the property at the specified index. */ @Override - public ExtendedElementInformation get(final Object key) { - if (key instanceof String) { - return accessor.information(standard, accessor.indexOf((String) key, false)); - } - return null; - } - - /** - * Returns an iterator over the entries contained in this map. - */ - @Override - final Iterator<Map.Entry<String,ExtendedElementInformation>> iterator() { - return new Iter() { - @Override - public Map.Entry<String,ExtendedElementInformation> next() { - final ExtendedElementInformation value = accessor.information(standard, index); - if (value == null) { - // PropertyAccessor.information(int) never return null if the index is valid. - throw new NoSuchElementException(); - } - return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value); - } - }; + final ExtendedElementInformation getReflectively(final int index) { + return accessor.information(standard, index); } } 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 cd0d10aeea..0240680033 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 @@ -895,6 +895,41 @@ public class MetadataStandard implements Serializable { return new ValueMap(metadata, getAccessor(new CacheKey(metadata.getClass(), baseType), true), keyPolicy, valuePolicy); } + /** + * Returns the reasons for all missing values of mandatory properties. + * The map is backed by the metadata object using Java reflection, so changes in the + * underlying metadata object are immediately reflected in the map and conversely. + * Nil reasons are determined by calls to {@link NilReason#forObject(Object)}, + * potentially completed by an internal storage for + * {@linkplain NilReason#isSupported(Class) unsupported value types}. + * + * <h4>Mandatory and optional properties</h4> + * If a {@linkplain org.opengis.annotation.Obligation#MANDATORY mandatory} property has no value, + * then the property will have an entry in the map even if the associated {@link NilReason} is null. + * By contrast, {@linkplain org.opengis.annotation.Obligation#OPTIONAL optional} properties have + * entries in the map only if they have a non-null {@link NilReason}. + * + * <h4>Supported operations</h4> + * The map supports the {@link Map#put(Object, Object) put(…)} and {@link Map#remove(Object) + * remove(…)} operations if the underlying metadata object contains setter methods. + * The {@code remove(…)} method is implemented by a call to {@code put(…, null)}. + * + * @param metadata the metadata object to view as a map. + * @param baseType base type of the metadata of interest, or {@code null} if unspecified. + * @param keyPolicy determines the string representation of map keys. + * @return a map view over the metadata object. + * @throws ClassCastException if the metadata object does not implement a metadata interface of the expected package. + * + * @since 1.5 + */ + public Map<String,NilReason> asNilReasonMap(final Object metadata, final Class<?> baseType, + final KeyNamePolicy keyPolicy) throws ClassCastException + { + ensureNonNull("metadata", metadata); + ensureNonNull("keyPolicy", keyPolicy); + return new NilReasonMap(metadata, getAccessor(new CacheKey(metadata.getClass(), baseType), true), keyPolicy); + } + /** * Returns the specified metadata object as a tree table. * The tree table is backed by the metadata object using Java reflection, so changes in the diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java index c76cb17706..1aee26e8e8 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java @@ -16,10 +16,6 @@ */ package org.apache.sis.metadata; -import java.util.Map; -import java.util.Iterator; -import java.util.NoSuchElementException; - /** * Map of property names for a given implementation class. This map is read-only. @@ -32,7 +28,7 @@ final class NameMap extends PropertyMap<String> { /** * Determines the string representation of values in this map. */ - final KeyNamePolicy valuePolicy; + private final KeyNamePolicy valuePolicy; /** * Creates a name map for the specified accessor. @@ -47,32 +43,10 @@ final class NameMap extends PropertyMap<String> { } /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - */ - @Override - public String get(final Object key) { - if (key instanceof String) { - return accessor.name(accessor.indexOf((String) key, false), valuePolicy); - } - return null; - } - - /** - * Returns an iterator over the entries contained in this map. + * Returns the name for the property at the specified index. */ @Override - final Iterator<Map.Entry<String,String>> iterator() { - return new Iter() { - @Override - public Map.Entry<String,String> next() { - final String value = accessor.name(index, valuePolicy); - if (value == null) { - // PropertyAccessor.name(int) never return null if the index is valid. - throw new NoSuchElementException(); - } - return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value); - } - }; + final String getReflectively(final int index) { + return accessor.name(index, valuePolicy); } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java new file mode 100644 index 0000000000..9ceed8c7aa --- /dev/null +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.metadata; + +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import org.opengis.annotation.Obligation; +import org.apache.sis.xml.NilReason; +import org.apache.sis.xml.NilObject; + + +/** + * A view of nil reasons as a map. Keys are property names and each value is the reason + * why the value returned by the {@code getFoo()} method (using reflection) is missing. + * This map contains only the properties that are mandatory. + * + * <p>Contrarily to other map views, this map may contain entries associated to null values. + * It happens when a mandatory property is missing, but nevertheless no reason is provided. + * So {@code containsValue(null)} can be used for checking if a metadata is invalid.</p> + * + * <p>Contrarily to other map views, this map is state-full. + * Only one instance should be created per metadata object.</p> + * + * @author Martin Desruisseaux (Geomatys) + * + * @see MetadataStandard#asNilReasonMap(Object, Class, KeyNamePolicy) + */ +final class NilReasonMap extends PropertyMap<NilReason> { + /** + * The metadata object to wrap. + */ + private final Object metadata; + + /** + * The reasons why a mandatory property is missing when that reason cannot be expressed by {@link NilObject}. + * This is needed for value objects such as {@link Integer}. This map shall always be checked in last resort, + * after we verified that the {@linkplain #metadata} does not have any value for the corresponding property. + * If {@link #metadata} changed without updating this {@code reasons} map, the metadata value prevails. + */ + private final Map<Integer,NilReason> nilReasons; + + /** + * Creates a map of nil reasons for the specified metadata and accessor. + * + * @param metadata the metadata object to wrap. + * @param accessor the accessor to use for the metadata. + * @param keyPolicy determines the string representation of keys in the map. + */ + NilReasonMap(final Object metadata, final PropertyAccessor accessor, final KeyNamePolicy keyPolicy) { + super(accessor, keyPolicy); + this.metadata = metadata; + if (metadata instanceof AbstractMetadata) { + final var c = (AbstractMetadata) metadata; + synchronized (c) { + if (c.nilReasons == null) { + c.nilReasons = new HashMap<>(4); + } + nilReasons = c.nilReasons; + } + } else { + nilReasons = new HashMap<>(4); + } + } + + /** + * Returns {@code true} if this map contains no key-value mappings. + */ + @Override + public boolean isEmpty() { + final int count = accessor.count(); + for (int i=0; i<count; i++) { + if (contains(i)) { + return false; + } + } + return true; + } + + /** + * Returns the number of entries in this map. + * Note that some entries may be associated to null values. + */ + @Override + public int size() { + int n = 0; + final int count = accessor.count(); + for (int i=0; i<count; i++) { + if (contains(i)) n++; + } + return n; + } + + /** + * Returns whether this map contains an entry for the property at the given index. + * The value associated to that entry may be null. + * + * @param index property index, using the numbering for all properties. + * @return whether this map contains a value, potentially null, for the specified property. + */ + @Override + final boolean contains(final int index) { + final Object value = accessor.get(index, metadata); + if (value != null) { + nilReasons.remove(index); + return NilReason.forObject(value) != null; + } + return nilReasons.containsKey(index) || accessor.obligation(index) == Obligation.MANDATORY; + } + + /** + * Returns the nil reason for the property at the specified index. + */ + @Override + final NilReason getReflectively(final int index) { + final Object value = accessor.get(index, metadata); + if (value != null) { + nilReasons.remove(index); + return NilReason.forObject(value); + } + return nilReasons.get(index); + } + + /** + * Associates the specified nil reason with the specified key in this map. + * The given value will replace any previous value including non-nil ones, + * unless the given value is null while the previous value is non-nil. + * In the latter case, this method does nothing (i.e. the non-nil value is not discarded). + * + * <p>Note that the latter exception is not really an exception from the point of view of + * a {@code NilReasonMap} user, because non-nil values are returned as {@code null} values. + * Consequently, {@code put(key, null)} behaves as if {@code null} has been stored in this + * map even if the underlying {@linkplain #metadata} object has a non-nil value.</p> + * + * @throws IllegalArgumentException if the given key is not the name of a property in the metadata. + * @throws ClassCastException if the given value is not of the expected type. + * @throws UnmodifiableMetadataException if the property for the given key is read-only. + */ + @Override + final NilReason setReflectively(final int index, final NilReason value) { + if (value == null) { + final Object oldObject = accessor.get(index, metadata); + final NilReason oldReason = NilReason.forObject(oldObject); + if (oldReason == null) { + NilReason oldStored = nilReasons.remove(index); + return (oldObject == null) ? oldStored : null; + } + accessor.set(index, metadata, null, PropertyAccessor.RETURN_NULL); + nilReasons.remove(index); // Shall do only if `set(…)` succeeded. + return oldReason; + } + final Class<?> type = accessor.type(index, TypeValuePolicy.PROPERTY_TYPE); + final Object nilObject = NilReason.isSupported(type) ? value.createNilObject(type) : null; + final Object oldObject = accessor.set(index, metadata, nilObject, PropertyAccessor.RETURN_PREVIOUS); + final NilReason oldReason = NilReason.forObject(oldObject); + final NilReason oldStored = (nilObject == null) ? nilReasons.put(index, value) : nilReasons.remove(index); + return (oldReason != null) ? oldReason : oldStored; + } + + /** + * Returns an iterator over the entries contained in this map. + */ + @Override + final Iterator<Map.Entry<String,NilReason>> iterator() { + return new ReflectiveIterator(); + } +} 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 f753bcbd3b..248bd3afdd 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 @@ -238,8 +238,8 @@ class PropertyAccessor { this.type = type; this.implementation = implementation; this.getters = getGetters(type, implementation, standardImpl); - int allCount = getters.length; - int standardCount = allCount; + int allCount = getters.length; + int standardCount = allCount; if (allCount != 0 && getters[allCount-1] == EXTRA_GETTER) { if (!EXTRA_GETTER.getDeclaringClass().isAssignableFrom(implementation)) { allCount--; // The extra getter method does not exist. @@ -311,7 +311,7 @@ class PropertyAccessor { * be a parent type. * * It is a necessary condition that the type returned by the getter is assignable - * to the type expected by the setter. This contract is required by the 'FINAL' + * to the type expected by the setter. This contract is required by the `FINAL` * state among others. */ try { @@ -330,7 +330,7 @@ class PropertyAccessor { } catch (NoSuchMethodException ignore) { /* * There is no setter, which may be normal. At this stage - * the 'setter' variable should still have the null value. + * the `setter` variable should still have the null value. */ } } @@ -768,7 +768,7 @@ class PropertyAccessor { } } catch (IllegalAccessException e) { /* - * Should never happen since 'getters' should contain only public methods. + * Should never happen since `getters` should contain only public methods. */ throw new AssertionError(method.toString(), e); } catch (InvocationTargetException e) { @@ -867,7 +867,7 @@ class PropertyAccessor { } /* * Converts the new value to a type acceptable for the setter method (if possible). - * If the new value is a singleton while the expected type is a collection, then the 'convert' + * If the new value is a singleton while the expected type is a collection, then the `convert` * method added the singleton in the existing collection, which may result in no change if the * collection is a Set and the new value already exists in that Set. If we detect that there is * no change, then we don't need to invoke the setter method. Note that we conservatively assume @@ -879,7 +879,7 @@ class PropertyAccessor { changed = (mode == RETURN_NULL) || (mode == IGNORE_READ_ONLY) || (newValues[0] != oldValue); if (changed && mode == APPEND && !isNullOrEmpty(oldValue)) { /* - * If 'convert' did not added the value in a collection and if a value already + * If `convert` did not added the value in a collection and if a value already * exists, do not modify the existing value. Exit now with "no change" status. */ return null; @@ -921,7 +921,7 @@ class PropertyAccessor { try { setter.invoke(metadata, newValues); } catch (IllegalAccessException e) { - // Should never happen since 'setters' should contain only public methods. + // Should never happen since `setters` should contain only public methods. throw new AssertionError(e); } catch (InvocationTargetException e) { final Throwable cause = e.getTargetException(); @@ -1053,11 +1053,11 @@ class PropertyAccessor { } /* * We now have objects of the appropriate type. If we have a singleton to be added - * in an existing collection, add it now. In that case the 'newValue' should refer - * to the 'addTo' collection. We rely on the ModifiableMetadata.writeCollection(…) + * in an existing collection, add it now. In that case the `newValue` should refer + * to the `addTo` collection. We rely on the ModifiableMetadata.writeCollection(…) * optimization for detecting that the new collection is the same instance than * the old one so there is nothing to do. We could exit from the method, but let - * it continues in case the user override the 'setFoo(…)' method. + * it continues in case the user override the `setFoo(…)` method. */ if (addTo != null) { /* @@ -1148,7 +1148,7 @@ class PropertyAccessor { return count(); } int count = 0; - // Use 'standardCount' instead of 'allCount' for ignoring deprecated methods. + // Use `standardCount` instead of `allCount` for ignoring deprecated methods. for (int i=0; i<standardCount; i++) { final Object value = get(getters[i], metadata); if (!valuePolicy.isSkipped(value)) { @@ -1163,7 +1163,7 @@ class PropertyAccessor { case COUNT_DEEP: { /* * Count always at least one element because if the user wanted to skip null or empty - * collections, then 'valuePolicy.isSkipped(value)' above would have returned 'true'. + * collections, then `valuePolicy.isSkipped(value)` above would have returned `true`. */ count += isCollectionOrMap(i) ? Math.max(CollectionsExt.size(value), 1) : 1; break; diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java index 80ad8c1cc4..90f4fb2042 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java @@ -21,6 +21,9 @@ import java.util.Set; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Iterator; +import java.util.NoSuchElementException; +import org.apache.sis.util.internal.AbstractMapEntry; +import org.apache.sis.util.resources.Errors; /** @@ -37,6 +40,7 @@ import java.util.Iterator; * @see NameMap * @see TypeMap * @see InformationMap + * @see NilReasonMap */ abstract class PropertyMap<V> extends AbstractMap<String,V> { /** @@ -47,12 +51,12 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> { /** * Determines the string representation of keys in the map. */ - final KeyNamePolicy keyPolicy; + protected final KeyNamePolicy keyPolicy; /** * A view of the mappings contained in this map. */ - transient Set<Map.Entry<String,V>> entrySet; + private transient Set<Map.Entry<String,V>> entrySet; /** * Creates a new map backed by the given accessor. @@ -73,24 +77,90 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> { return accessor.count(); } + /** + * Returns {@code true} if this map contains a mapping for the property at the specified index. + * The default implementation is okay only if all metadata defined by the standard are included in the map. + * Subclasses shall override this method if their map contain only a subset of all possible metadata elements. + * + * @param index index of the property to test, possibly negative. + * @return whether this map contains a property for the specified index. + */ + boolean contains(int index) { + return index >= 0; + } + + /** + * Returns the value for the property at the specified index. + * + * @param index index of the property to get, possibly negative. + * @return value at the given index, or {@code null} if none. + */ + abstract V getReflectively(int index); + + /** + * Sets the value for the property at the specified index. + * + * @param index index of the property to set. + * @param value new property value to set. + * @return old property value, or {@code null} if none. + * @throws UnsupportedOperationException if this map is read-only. + */ + V setReflectively(int index, V value) { + throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Map.class)); + } + /** * Returns {@code true} if this map contains a mapping for the specified key. - * The default implementation is okay only if all metadata defined by the standard are included - * in the map. Subclasses shall override this method if their map contain only a subset of all - * possible metadata elements. + * Note that the associated value may be null. */ @Override - public boolean containsKey(final Object key) { - return (key instanceof String) && accessor.indexOf((String) key, false) >= 0; + public final boolean containsKey(final Object key) { + return (key instanceof String) && contains(accessor.indexOf((String) key, false)); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} if none. + */ + @Override + public final V get(final Object key) { + if (key instanceof String) { + return getReflectively(accessor.indexOf((String) key, false)); + } + return null; + } + + /** + * Associates the specified value with the specified key in this map. + * + * @throws IllegalArgumentException if the given key is not the name of a property in the metadata. + * @throws ClassCastException if the given value is not of the expected type. + * @throws UnmodifiableMetadataException if the property for the given key is read-only. + */ + @Override + public final V put(final String key, final V value) { + return setReflectively(accessor.indexOf(key, true), value); + } + + /** + * Removes the mapping for a key from this map if it is present. + * + * @throws UnmodifiableMetadataException if the property for the given key is read-only. + */ + @Override + public final V remove(final Object key) throws UnsupportedOperationException { + if (key instanceof String) { + return setReflectively(accessor.indexOf((String) key, false), null); + } + return null; } /** - * Returns a view of the mappings contained in this map. Subclasses shall override this method - * if they define a different entries set class than the default {@link Entries} inner class. + * Returns a view of the mappings contained in this map. + * The entries are provided by {@link #iterator()}. */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") // Intentionally modifiable set. - public Set<Map.Entry<String,V>> entrySet() { + public final Set<Map.Entry<String,V>> entrySet() { if (entrySet == null) { entrySet = new Entries(); } @@ -98,81 +168,152 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> { } /** - * Returns an iterator over the entries in this map. + * Views of the entries contained in the map. + * The entries are provided by {@link PropertyMap#iterator()}. */ - abstract Iterator<Map.Entry<String,V>> iterator(); + private final class Entries extends AbstractSet<Map.Entry<String,V>> { + /** Creates a new entries set. */ + Entries() { + } + + /** Returns true if this collection contains no elements. */ + @Override public final boolean isEmpty() { + return PropertyMap.this.isEmpty(); + } + /** Returns the number of elements in this collection. */ + @Override public final int size() { + return PropertyMap.this.size(); + } + /** Returns an iterator over the elements contained in this collection. */ + @Override public final Iterator<Map.Entry<String,V>> iterator() { + return PropertyMap.this.iterator(); + } + /** Returns {@code true} if this collection contains the specified element. */ + @Override public boolean contains(final Object object) { + if (object instanceof Map.Entry<?,?>) { + final Map.Entry<?,?> entry = (Map.Entry<?,?>) object; + final Object key = entry.getKey(); + final Object value = entry.getValue(); + if (value != null) { + return value.equals(get(key)); + } else { + return containsKey(key); + } + } + return false; + } + } /** - * The iterator over the elements contained in a {@link Entries} set. - * - * @author Martin Desruisseaux (Geomatys) + * Returns an iterator over the entries in this map. + * The default implementation is okay only if all metadata defined by the standard are included in the map. + * Subclasses shall override this method if their map contain only a subset of all possible metadata elements. + * The {@link ReflectiveIterator} may be used for those maps. */ - abstract class Iter implements Iterator<Map.Entry<String,V>> { - /** - * Index of the next element to return. - */ - int index; + Iterator<Map.Entry<String,V>> iterator() { + return new Iterator<>() { + /** Index of the next property to return. */ + private int index; - /** - * Creates a new iterator. - */ - Iter() { - } + /** Returns true if there is more entries to return. */ + @Override public boolean hasNext() { + return index < accessor.count(); + } + + /** Returns the next entry. */ + @Override public Map.Entry<String,V> next() { + final int i = index++; + final String name = accessor.name(i, keyPolicy); + if (name != null) { + return new SimpleImmutableEntry<>(name, getReflectively(i)); + } + throw new NoSuchElementException(); + } + }; + } + + /** + * The iterator over the {@link ReflectiveEntry} elements contained in an {@link Entries} set. + * Can be used as an alternative to the default {@link PropertyMap#iterator()} when the map + * contains only a subset of all possible properties. + */ + final class ReflectiveIterator implements Iterator<Map.Entry<String,V>> { + /** The current and the next property, or {@code null} if the iteration is over. */ + private ReflectiveEntry current, next; - /** - * Returns {@code true} if there is more elements to return. - */ - @Override - public final boolean hasNext() { - return index < accessor.count(); + /** Creates en iterator. */ + ReflectiveIterator() { + move(0); } - /* - * remove() is an unsupported operation since we assume that the underlying map is unmodifiable. - * So we inherit the default implementation from Iterator. - */ - } + /** Moves to the first property with a valid value, starting at the specified index. */ + private void move(int index) { + final int count = accessor.count(); + while (index < count) { + if (contains(index)) { + next = new ReflectiveEntry(index); + return; + } + index++; + } + next = null; + } + /** Returns {@code true} if the iteration has more elements. */ + @Override public boolean hasNext() { + return next != null; + } + /** Returns the next element in the iteration. */ + @Override public Map.Entry<String,V> next() { + if (next != null) { + current = next; + move(next.index + 1); + return current; + } else { + throw new NoSuchElementException(); + } + } + /** Removes from the underlying collection the last element returned by the iterator. */ + @Override public void remove() { + if (current != null) { + current.setValue(null); + current = null; + } else { + throw new IllegalStateException(); + } + } + } /** - * Base class of views of the entries contained in the map. - * - * @author Martin Desruisseaux (Geomatys) + * A map entry for a property whose key and value are fetched by reflection. */ - class Entries extends AbstractSet<Map.Entry<String,V>> { - /** - * Creates a new entries set. - */ - Entries() { + private final class ReflectiveEntry extends AbstractMapEntry<String,V> { + /** The property index. */ + final int index; + + /** Creates an entry for the property at the given index. */ + ReflectiveEntry(final int index) { + this.index = index; } - /** - * Returns true if this collection contains no elements. - */ - @Override - public final boolean isEmpty() { - return PropertyMap.this.isEmpty(); + /** Returns the key corresponding to this entry. */ + @Override public String getKey() { + return accessor.name(index, keyPolicy); } - /** - * Returns the number of elements in this collection. - */ - @Override - public final int size() { - return PropertyMap.this.size(); + /** Returns the value corresponding to this entry. */ + @Override public V getValue() { + return getReflectively(index); } - /** - * Returns an iterator over the elements contained in this collection. - */ - @Override - public final Iterator<Map.Entry<String,V>> iterator() { - return PropertyMap.this.iterator(); + /** Replaces the value corresponding to this entry with the specified value. */ + @Override public V setValue(final V value) { + return setReflectively(index, value); } } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java index 034a58d9ac..4969a02e1a 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java @@ -16,10 +16,6 @@ */ package org.apache.sis.metadata; -import java.util.Map; -import java.util.Iterator; -import java.util.NoSuchElementException; - /** * Map of property valuePolicy for a given implementation class. This map is read-only. @@ -32,7 +28,7 @@ final class TypeMap extends PropertyMap<Class<?>> { /** * The kind of values in this map. */ - final TypeValuePolicy valuePolicy; + private final TypeValuePolicy valuePolicy; /** * Creates a type map for the specified accessor. @@ -47,31 +43,10 @@ final class TypeMap extends PropertyMap<Class<?>> { } /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. - */ - @Override - public Class<?> get(final Object key) { - if (key instanceof String) { - return accessor.type(accessor.indexOf((String) key, false), valuePolicy); - } - return null; - } - - /** - * Returns an iterator over the entries contained in this map. + * Returns the type for the property at the specified index. */ @Override - final Iterator<Map.Entry<String,Class<?>>> iterator() { - return new Iter() { - @Override - public Map.Entry<String,Class<?>> next() { - if (index >= accessor.count()) { - throw new NoSuchElementException(); - } - final Class<?> value = accessor.type(index, valuePolicy); - return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value); - } - }; + final Class<?> getReflectively(final int index) { + return accessor.type(index, valuePolicy); } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java index 5beb484e89..2605d3eb81 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java @@ -17,10 +17,7 @@ package org.apache.sis.metadata; import java.util.Map; -import java.util.Set; import java.util.Iterator; -import java.util.NoSuchElementException; -import org.apache.sis.util.internal.AbstractMapEntry; import static org.apache.sis.metadata.PropertyAccessor.RETURN_NULL; import static org.apache.sis.metadata.PropertyAccessor.RETURN_PREVIOUS; @@ -50,7 +47,7 @@ final class ValueMap extends PropertyMap<Object> { * * @param metadata the metadata object to wrap. * @param accessor the accessor to use for the metadata. - * @param keyPolicy determines the string representation of keys in the map.. + * @param keyPolicy determines the string representation of keys in the map. * @param valuePolicy the behavior of this map toward null or empty values. */ ValueMap(final Object metadata, final PropertyAccessor accessor, @@ -78,26 +75,23 @@ final class ValueMap extends PropertyMap<Object> { } /** - * Returns {@code true} if this map contains a mapping for the specified key. + * Returns {@code true} if this map contains a mapping for the property at the specified index. */ @Override - public boolean containsKey(final Object key) { - return get(key) != null; + final boolean contains(final int index) { + if (valuePolicy == ValueExistencePolicy.ALL) { + return super.contains(index); + } + return !valuePolicy.isSkipped(accessor.get(index, metadata)); } /** - * Returns the value to which the specified key is mapped, or {@code null} - * if this map contains no mapping for the key. + * Returns the value for the property at the specified index. */ @Override - public Object get(final Object key) { - if (key instanceof String) { - final Object value = accessor.get(accessor.indexOf((String) key, false), metadata); - if (!valuePolicy.isSkipped(value)) { - return value; - } - } - return null; + final Object getReflectively(final int index) { + final Object value = accessor.get(index, metadata); + return valuePolicy.isSkipped(value) ? null : value; } /** @@ -108,8 +102,8 @@ final class ValueMap extends PropertyMap<Object> { * @throws UnmodifiableMetadataException if the property for the given key is read-only. */ @Override - public Object put(final String key, final Object value) { - final Object old = accessor.set(accessor.indexOf(key, true), metadata, value, RETURN_PREVIOUS); + final Object setReflectively(final int index, final Object value) { + final Object old = accessor.set(index, metadata, value, RETURN_PREVIOUS); return valuePolicy.isSkipped(old) ? null : old; } @@ -146,205 +140,11 @@ final class ValueMap extends PropertyMap<Object> { } } - /** - * Removes the mapping for a key from this map if it is present. - * - * @throws UnmodifiableMetadataException if the property for the given key is read-only. - */ - @Override - public Object remove(final Object key) throws UnsupportedOperationException { - if (key instanceof String) { - final Object old = accessor.set(accessor.indexOf((String) key, false), metadata, null, RETURN_PREVIOUS); - if (!valuePolicy.isSkipped(old)) { - return old; - } - } - return null; - } - - /** - * Returns a view of the mappings contained in this map. - */ - @Override - public Set<Map.Entry<String,Object>> entrySet() { - if (entrySet == null) { - entrySet = new Entries(); - } - return entrySet; - } - /** * Returns an iterator over the entries contained in this map. */ @Override final Iterator<Map.Entry<String,Object>> iterator() { - return new Iter(); - } - - - - - /** - * A map entry for a given property. - * - * @author Martin Desruisseaux (Geomatys) - */ - final class Property extends AbstractMapEntry<String,Object> { - /** - * The property index. - */ - final int index; - - /** - * Creates an entry for the property at the given index. - */ - Property(final int index) { - this.index = index; - } - - /** - * Returns the key corresponding to this entry. - */ - @Override - public String getKey() { - return accessor.name(index, keyPolicy); - } - - /** - * Returns value type as declared in the interface method signature. - * It may be a primitive type. - */ - public Class<?> getValueType() { - return accessor.type(index, TypeValuePolicy.PROPERTY_TYPE); - } - - /** - * Returns the value corresponding to this entry. - */ - @Override - public Object getValue() { - final Object value = accessor.get(index, metadata); - return valuePolicy.isSkipped(value) ? null : value; - } - - /** - * Replaces the value corresponding to this entry with the specified value. - * - * @throws ClassCastException if the given value is not of the expected type. - * @throws UnmodifiableMetadataException if the property for this entry is read-only. - */ - @Override - public Object setValue(final Object value) { - return accessor.set(index, metadata, value, RETURN_PREVIOUS); - } - } - - - - - /** - * The iterator over the {@link Property} elements contained in an {@link Entries} set. - * - * @author Martin Desruisseaux (Geomatys) - */ - private final class Iter implements Iterator<Map.Entry<String,Object>> { - /** - * The current and the next property, or {@code null} if the iteration is over. - */ - private Property current, next; - - /** - * Creates en iterator. - */ - Iter() { - move(0); - } - - /** - * Moves {@link #next} to the first property with a valid value, - * starting at the specified index. - */ - private void move(int index) { - final int count = accessor.count(); - while (index < count) { - if (!valuePolicy.isSkipped(accessor.get(index, metadata))) { - next = new Property(index); - return; - } - index++; - } - next = null; - } - - /** - * Returns {@code true} if the iteration has more elements. - */ - @Override - public boolean hasNext() { - return next != null; - } - - /** - * Returns the next element in the iteration. - */ - @Override - public Map.Entry<String,Object> next() { - if (next != null) { - current = next; - move(next.index + 1); - return current; - } else { - throw new NoSuchElementException(); - } - } - - /** - * Removes from the underlying collection the last element returned by the iterator. - * - * @throws UnmodifiableMetadataException if the property for this entry is read-only. - */ - @Override - public void remove() { - if (current != null) { - current.setValue(null); - current = null; - } else { - throw new IllegalStateException(); - } - } - } - - - - - /** - * View of the entries contained in the map. - * - * @author Martin Desruisseaux (Geomatys) - */ - private final class Entries extends PropertyMap<Object>.Entries { - /** - * Creates an entry set. - */ - Entries() { - } - - /** - * Returns {@code true} if this collection contains the specified element. - */ - @Override - public boolean contains(final Object object) { - if (object instanceof Map.Entry<?,?>) { - final Map.Entry<?,?> entry = (Map.Entry<?,?>) object; - final Object key = entry.getKey(); - if (key instanceof String) { - final int index = accessor.indexOf((String) key, false); - if (index >= 0) { - return new Property(index).equals(entry); - } - } - } - return false; - } + return new ReflectiveIterator(); } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java index b92afbecec..bb0b40ad4c 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java @@ -220,7 +220,7 @@ public class ISOMetadata extends ModifiableMetadata implements IdentifiedObject, // -------------------------------------------------------------------------------------- /** - * {@inheritDoc} + * @hidden */ @Override public boolean transitionTo(final State target) { diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java new file mode 100644 index 0000000000..a45f7afb5f --- /dev/null +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.metadata; + +import java.util.Map; +import org.opengis.metadata.citation.Citation; +import org.apache.sis.metadata.iso.citation.DefaultCitation; +import org.apache.sis.xml.NilReason; +import org.apache.sis.xml.NilObject; + +// Test dependencies +import org.junit.Test; +import org.apache.sis.test.TestCase; +import org.apache.sis.test.DependsOn; +import org.apache.sis.util.SimpleInternationalString; + +import static org.junit.jupiter.api.Assertions.*; +import static org.apache.sis.test.TestUtilities.getSingleton; + + +/** + * Tests the {@link NilReasonMap} class. + * + * @author Martin Desruisseaux (Geomatys) + */ +@DependsOn(PropertyAccessorTest.class) +public final class NilReasonMapTest extends TestCase { + /** + * Creates a new test case. + */ + public NilReasonMapTest() { + } + + /** + * Creates a metadata instance to use for the tests. + * This metadata has a mandatory property, which is the citation title. + * + * @param title a value for the mandatory property, of {@code null} if none. + */ + private static DefaultCitation metadata(final String title) { + final var citation = new DefaultCitation(title); + citation.getAlternateTitles().add(new SimpleInternationalString("Another title")); + return citation; + } + + /** + * Tests a map with no + */ + @Test + public void testCitation() { + final DefaultCitation citation = metadata("A title"); + final Map<String,NilReason> map = MetadataStandard.ISO_19115.asNilReasonMap(citation, Citation.class, KeyNamePolicy.UML_IDENTIFIER); + assertTrue(map.isEmpty()); + /* + * Sets the edition date to a nil value. + */ + assertNull (map.get ("editionDate")); + assertFalse(map.containsKey("editionDate")); + assertNull (map.put ("editionDate", NilReason.TEMPLATE)); + assertFalse(map.isEmpty()); + assertEquals(1, map.size()); + assertEquals("editionDate", getSingleton(map.keySet())); + assertEquals(NilReason.TEMPLATE, getSingleton(map.values())); + assertEquals(NilReason.TEMPLATE, map.put("editionDate", null)); + /* + * Set the title to nil value instead of edition date. + */ + assertTrue (map.isEmpty()); + assertFalse (map.containsKey("title")); + assertNull (map.put("title", NilReason.MISSING)); + assertTrue (map.containsKey("title")); + assertEquals(NilReason.MISSING, map.get("title")); + assertEquals(1, map.size()); + assertEquals("title", getSingleton(map.keySet())); + assertEquals(NilReason.MISSING, getSingleton(map.values())); + assertInstanceOf(NilObject.class, citation.getTitle()); + /* + * Even if we clear the citation title, because that property is mandatory, + * it should still be present in the map of nil reasons. + */ + assertEquals(NilReason.MISSING, map.put("title", null)); + assertNull (citation.getTitle()); + assertTrue (map.containsKey("title")); + assertEquals(1, map.size()); + assertEquals("title", getSingleton(map.keySet())); + assertNull ( getSingleton(map.values())); + } +}