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 76d4d2c198764528da7e9db136dfe468c81c0456 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Sep 10 16:06:27 2024 +0200 Review the way that some "multi-values map" are handled. In the netCDF case, group together the domains (set of axes) that are subset or superset of another domain. --- .../main/org/apache/sis/io/wkt/AbstractParser.java | 6 +- .../main/org/apache/sis/io/wkt/Element.java | 5 +- .../main/org/apache/sis/io/wkt/Warnings.java | 14 ++--- .../apache/sis/storage/netcdf/MetadataReader.java | 28 +++++++-- .../sis/storage/netcdf/base/NamedElement.java | 6 ++ .../org/apache/sis/storage/netcdf/base/Node.java | 6 ++ .../apache/sis/storage/netcdf/base/Variable.java | 4 +- .../sis/storage/netcdf/classic/ChannelDecoder.java | 4 +- .../sis/storage/netcdf/classic/VariableInfo.java | 5 ++ .../sis/storage/sql/feature/FeatureAnalyzer.java | 7 ++- .../sis/storage/sql/feature/TableReference.java | 2 +- .../main/org/apache/sis/storage/FeatureNaming.java | 28 +++++---- .../apache/sis/storage/base/MetadataBuilder.java | 10 +++- .../org/apache/sis/util/privy/CollectionsExt.java | 69 +++++++++++----------- .../apache/sis/util/privy/CollectionsExtTest.java | 4 +- 15 files changed, 119 insertions(+), 79 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java index 477011c6e0..416a63422f 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java @@ -18,7 +18,7 @@ package org.apache.sis.io.wkt; import java.net.URI; import java.util.Map; -import java.util.List; +import java.util.Set; import java.util.Date; import java.util.Locale; import java.util.LinkedHashMap; @@ -138,7 +138,7 @@ abstract class AbstractParser implements Parser { * <ul> * <li><b>Keys</b>: keyword of ignored elements. Note that a key may be null.</li> * <li><b>Values</b>: keywords of all elements containing an element identified by the above-cited key. - * This list is used for helping the users to locate the ignored elements.</li> + * This collection is used for helping the users to locate the ignored elements.</li> * </ul> * * Content of this map is not discarded immediately {@linkplain #getAndClearWarnings(Object) after parsing}. @@ -146,7 +146,7 @@ abstract class AbstractParser implements Parser { * * @see #getAndClearWarnings(Object) */ - final Map<String, List<String>> ignoredElements; + final Map<String, Set<String>> ignoredElements; /** * The warning (other than {@link #ignoredElements}) that occurred during the parsing. diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java index af323210cd..8a4e621d0b 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java @@ -18,6 +18,7 @@ package org.apache.sis.io.wkt; import java.util.Date; import java.util.Map; +import java.util.Set; import java.util.List; import java.util.LinkedList; import java.util.Iterator; @@ -785,13 +786,13 @@ final class Element { * <ul> * <li><b>Keys</b>: keyword of ignored elements. Note that a key may be null.</li> * <li><b>Values</b>: keywords of all elements containing an element identified by the above-cited key. - * This list is used for helping the users to locate the ignored elements.</li> + * This collection is used for helping the users to locate the ignored elements.</li> * </ul> * * @param ignoredElements the collection where to declare ignored elements. * @throws ParseException if the children list still contains some unprocessed values. */ - final void close(final Map<String, List<String>> ignoredElements) throws ParseException { + final void close(final Map<String, Set<String>> ignoredElements) throws ParseException { for (final Object value : children) { if (value instanceof Element) { CollectionsExt.addToMultiValuesMap(ignoredElements, ((Element) value).keyword, keyword); diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Warnings.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Warnings.java index dcb6bcd183..846c5f7bfe 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Warnings.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Warnings.java @@ -17,11 +17,9 @@ package org.apache.sis.io.wkt; import java.util.Locale; -import java.util.List; import java.util.Set; import java.util.Map; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Iterator; @@ -69,7 +67,7 @@ import org.apache.sis.util.resources.Vocabulary; * </ul> * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * * @see WKTFormat#getWarnings() * @@ -134,7 +132,7 @@ public final class Warnings implements Localized, Serializable { * @see AbstractParser#ignoredElements */ @SuppressWarnings("serial") // Various serializable implementations. - private Map<String, List<String>> ignoredElements; + private Map<String, Set<String>> ignoredElements; /** * {@code true} if {@link #publish()} has been invoked. @@ -148,7 +146,7 @@ public final class Warnings implements Localized, Serializable { * @param isParsing {@code false} if formatting, or {@code true} if parsing. * @param ignoredElements the {@link AbstractParser#ignoredElements} map, or an empty map (cannot be null). */ - Warnings(final Locale locale, final boolean isParsing, final Map<String, List<String>> ignoredElements) { + Warnings(final Locale locale, final boolean isParsing, final Map<String, Set<String>> ignoredElements) { this.errorLocale = locale; this.isParsing = isParsing; this.ignoredElements = ignoredElements; @@ -198,7 +196,7 @@ public final class Warnings implements Localized, Serializable { */ final void publish() { if (!published) { - ignoredElements = ignoredElements.isEmpty() ? Collections.emptyMap() : new LinkedHashMap<>(ignoredElements); + ignoredElements = Map.copyOf(ignoredElements); published = true; } } @@ -313,7 +311,7 @@ public final class Warnings implements Localized, Serializable { * @param element the keyword of the unknown element. * @return the keywords of elements where the given unknown element was found. */ - public Collection<String> getUnknownElementLocations(final String element) { + public Set<String> getUnknownElementLocations(final String element) { return ignoredElements.get(element); } @@ -377,7 +375,7 @@ public final class Warnings implements Localized, Serializable { if (!ignoredElements.isEmpty()) { final Vocabulary vocabulary = Vocabulary.forLocale(locale); buffer.append(lineSeparator).append(" • ").append(resources.getString(Messages.Keys.UnknownElementsInText)); - for (final Map.Entry<String, List<String>> entry : ignoredElements.entrySet()) { + for (final Map.Entry<String, Set<String>> entry : ignoredElements.entrySet()) { buffer.append(lineSeparator).append(" ‣ ").append(vocabulary.getString(Vocabulary.Keys.Quoted_1, entry.getKey())); String separator = vocabulary.getString(Vocabulary.Keys.InBetweenWords); for (final String p : entry.getValue()) { diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java index f718b5c233..3940e00ac3 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/MetadataReader.java @@ -23,7 +23,6 @@ import java.util.Set; import java.util.Map; import java.util.LinkedHashSet; import java.util.LinkedHashMap; -import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.io.IOException; @@ -862,23 +861,39 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt /** * Adds information about all netCDF variables. This is the {@code <mdb:contentInfo>} element in XML. - * This method groups variables by their domains, i.e. variables having the same set of axes are grouped together. + * This method groups variables by their domains, i.e. variables having the same set of axes, ignoring order, + * are grouped together. Variables having only a subset of axes are also grouped together with the variables + * having more dimension. + * + * <p><b>Example:</b> a netCDF file may contain variables for both static and dynamic phenomenons. + * The dynamic phenomenons are associated to (<var>x</var>, <var>y</var>, <var>t</var>) axes, + * while the static phenomenons have only the (<var>x</var>, <var>y</var>) axes. + * But we still want to group them together. Not doing so appear to be confusing.</p> */ private void addContentInfo() { /* * Prepare a list of features and coverages, but without writing metadata now. * We differ metadata writing for giving us a chance to group related contents. */ - final Set<Dimension> features = new LinkedHashSet<>(); - final Map<List<String>, List<Variable>> coverages = new LinkedHashMap<>(4); + final var features = new LinkedHashSet<Dimension>(); + final var coverages = new LinkedHashMap<Set<String>, List<Variable>>(); for (final Variable variable : decoder.getVariables()) { if (VariableRole.isCoverage(variable)) { - final List<org.apache.sis.storage.netcdf.base.Dimension> dimensions = variable.getGridDimensions(); + final var dimensions = variable.getGridDimensions(); final String[] names = new String[dimensions.size()]; for (int i=0; i<names.length; i++) { names[i] = dimensions.get(i).getName(); } - CollectionsExt.addToMultiValuesMap(coverages, Arrays.asList(names), variable); + coverages.computeIfAbsent(Set.of(names), (key) -> { + for (Map.Entry<Set<String>, List<Variable>> entry : coverages.entrySet()) { + final Set<String> previous = entry.getKey(); + if (previous.containsAll(key) || key.containsAll(previous)) { + // Share with all keys that are subset or superset. + return entry.getValue(); + } + } + return new ArrayList<>(); + }).add(variable); hasGridCoverages = true; } else if (variable.getRole() == VariableRole.FEATURE_PROPERTY) { /* @@ -901,6 +916,7 @@ split: while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt addFeatureType(decoder.nameFactory.createLocalName(decoder.namespace, name), feature.length()); } } + newFeatureTypes(); // See Javadoc about confusing ordering. final String processingLevel = stringValue(PROCESSING_LEVEL); for (final List<Variable> group : coverages.values()) { /* diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java index d23be01c0c..07221908bc 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/NamedElement.java @@ -126,4 +126,10 @@ public abstract class NamedElement { public String toString() { return Strings.bracket(getClass().getSimpleName(), getName()); } + + /* + * Do not override `equals(Object)` and `hashCode()`. Some subclasses are + * used in `HashSet` and the identity comparison is well suited for them. + * For example, `Variable` is used as keys in `GridMapping.forVariable(…)`. + */ } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java index faac3873f2..fded643327 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Node.java @@ -304,4 +304,10 @@ public abstract class Node extends NamedElement { protected final void error(final Class<?> caller, final String method, final Exception exception, final short key, final Object... arguments) { warning(decoder.listeners, caller, method, exception, errors(), key, arguments); } + + /* + * Do not override `equals(Object)` and `hashCode()`. Some subclasses are + * used in `HashSet` and the identity comparison is well suited for them. + * For example, `Variable` is used as keys in `GridMapping.forVariable(…)`. + */ } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java index 26c696a390..8153d7dced 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java @@ -1310,7 +1310,7 @@ public abstract class Variable extends Node { } /* - * Do not override Object.equals(Object) and Object.hashCode(), - * because Variables are used as keys by GridMapping.forVariable(…). + * Do not override `Object.equals(Object)` and `Object.hashCode()`, + * because variables are used as keys by `GridMapping.forVariable(…)`. */ } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java index 356bc6f213..dfb99b4786 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/ChannelDecoder.java @@ -961,7 +961,7 @@ public final class ChannelDecoder extends Decoder { * from grid coordinates to CRS coordinates). For each key there is usually only one value, but * more complicated netCDF files (e.g. using two-dimensional localisation grids) also exist. */ - final Map<DimensionInfo, List<VariableInfo>> dimToAxes = new IdentityHashMap<>(); + final var dimToAxes = new IdentityHashMap<DimensionInfo, Set<VariableInfo>>(); for (final VariableInfo variable : variables) { switch (variable.getRole()) { case COVERAGE: @@ -1011,7 +1011,7 @@ nextVar: for (final VariableInfo variable : variables) { for (int i=variable.dimensions.length; --i >= 0;) { // Reverse of netCDF order. final DimensionInfo dimension = variable.dimensions[i]; if (usedDimensions.add(dimension)) { - final List<VariableInfo> axis = dimToAxes.get(dimension); // Should have only 1 element. + final Set<VariableInfo> axis = dimToAxes.get(dimension); // Should have only 1 element. if (axis == null) { continue nextVar; } diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java index a051dbdbc0..5473325f93 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/classic/VariableInfo.java @@ -848,4 +848,9 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { if (c == 0) c = name.compareTo(other.name); // Should not happen, but we are paranoiac. return c; } + + /* + * Do not override `Object.equals(Object)` and `Object.hashCode()`, + * because variables are used as keys by `GridMapping.forVariable(…)`. + */ } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java index d817e1940f..a3f3375915 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAnalyzer.java @@ -17,6 +17,7 @@ package org.apache.sis.storage.sql.feature; import java.util.List; +import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.HashMap; @@ -34,7 +35,6 @@ import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.util.CharSequences; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; -import org.apache.sis.util.privy.CollectionsExt; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.feature.FeatureType; @@ -45,6 +45,9 @@ import org.opengis.feature.FeatureType; * This is used by {@link Analyzer} for creating {@link Table} instances. * A view or a custom query is considered as a "virtual" table. * + * <p>Instances of this class are created temporarily when starting the analysis + * of a database structure, and discarded after the analysis is finished.</p> + * * <h2>Side effects</h2> * Methods shall be invoked as below, in that order. The order is important because some * methods have values computed as side-effects and which are required by a subsequent method. @@ -171,7 +174,7 @@ abstract class FeatureAnalyzer { */ final void addForeignerKeys(Relation relation) { for (final String column : relation.getOwnerColumns()) { - CollectionsExt.addToMultiValuesMap(foreignerKeys, column, relation); + foreignerKeys.computeIfAbsent(column, (key) -> new ArrayList<>()).add(relation); relation = null; // Only the first column will be associated. } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java index 5d272580e7..b19f5a5130 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/TableReference.java @@ -102,7 +102,7 @@ public class TableReference { /** * Returns {@code true} if the given object is a {@code TableReference} with equal table, schema and catalog names. - * All other properties that may be defined in subclasses (column names, action on delete, etc.) are ignored; this + * All other properties that may be defined in subclasses (column names, action on delete, etc.) are ignored. This * method is <strong>not</strong> for testing if two {@link Relation} are fully equal. The purpose of this method * is only to use {@code TableReference} as keys in a {@code HashSet} for remembering full coordinates of tables * that may need to be analyzed later. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureNaming.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureNaming.java index 92d0951bc9..466257c02e 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureNaming.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureNaming.java @@ -18,8 +18,9 @@ package org.apache.sis.storage; import java.util.Map; import java.util.HashMap; -import java.util.List; +import java.util.Set; import java.util.ConcurrentModificationException; +import java.util.Iterator; import java.util.Locale; import java.util.Objects; import org.opengis.util.GenericName; @@ -86,7 +87,7 @@ import org.apache.sis.storage.internal.Resources; * The caller is typically the {@code DataStore} implementation which contains this {@code FeatureNaming} instance. * * @author Martin Desruisseaux (Geomatys) - * @version 0.8 + * @version 1.5 * * @param <E> the type of elements associated with the names. * @@ -100,13 +101,13 @@ public class FeatureNaming<E> { /** * All aliases found for all names given to the {@link #add(DataStore, GenericName, Object)} method. * Keys are aliases and values are the complete names for which the key is an alias. - * Each {@code List<String>} instance contains exactly one name if there is no ambiguity. + * Each {@code Set<String>} instance contains exactly one name if there is no ambiguity. * If the list contains more than one name, this means that the alias is ambiguous. * - * <p>For saving space in common cases, a missing entry in this map is interpreted as synonymous of - * {@code (name, Collections.singletonList(name))} entry.</p> + * <p>For saving space in common cases, a missing entry in this map is interpreted + * as synonymous of the {@code (name, Collections.singleton(name))} entry.</p> */ - private final Map<String, List<String>> aliases; + private final Map<String, Set<String>> aliases; /** * The user-specified values associated to names and aliases. If a value is absent, it may means either that the @@ -160,13 +161,14 @@ public class FeatureNaming<E> { } final short key; final Object[] params; - final List<String> nc = aliases.get(name); + final Set<String> nc = aliases.get(name); if (nc == null) { key = Resources.Keys.FeatureNotFound_2; params = new CharSequence[] {name(store), name}; } else if (nc.size() >= 2) { key = Resources.Keys.AmbiguousName_4; - params = new CharSequence[] {name(store), nc.get(0), nc.get(1), name}; + final Iterator<String> it = nc.iterator(); + params = new CharSequence[] {name(store), it.next(), it.next(), name}; } else { return null; // Name was explicitly associated to null value (actually not allowed by current API). } @@ -186,7 +188,7 @@ public class FeatureNaming<E> { final String key = name.toString(); final E previous = values.put(key, Objects.requireNonNull(value)); if (previous != null) { - final List<String> fullNames = aliases.get(key); // Null is synonymous of singletonList(key). + final Set<String> fullNames = aliases.get(key); // Null is synonymous of singleton(key). if (fullNames == null || fullNames.contains(key)) { /* * If we already had a value for the given name and if that value was associated to a user-specified @@ -206,7 +208,7 @@ public class FeatureNaming<E> { while (name instanceof ScopedName) { name = ((ScopedName) name).tail(); final String alias = name.toString(); - final List<String> fullNames = CollectionsExt.addToMultiValuesMap(aliases, alias, key); + final Set<String> fullNames = CollectionsExt.addToMultiValuesMap(aliases, alias, key); if (fullNames.size() > 1) { /* * If there is more than one GenericName for the same alias, we have an ambiguity. @@ -245,13 +247,13 @@ public class FeatureNaming<E> { if (values.remove(key) == null) { return false; } - List<String> remaining = CollectionsExt.removeFromMultiValuesMap(aliases, key, key); + Set<String> remaining = CollectionsExt.removeFromMultiValuesMap(aliases, key, key); if (remaining != null && remaining.size() == 1) { /* * If there is exactly one remaining element, that element is a non-ambiguous alias. * So we can associate the value to that alias. */ - final String select = remaining.get(0); + final String select = remaining.iterator().next(); assert !select.equals(key) : select; // Should have been removed by removeFromMultiValuesMap(…). if (values.put(key, values.get(select)) != null) { throw new ConcurrentModificationException(name(store).toString()); // Paranoiac check. @@ -271,7 +273,7 @@ public class FeatureNaming<E> { if (remaining == null || remaining.isEmpty()) { error |= (values.remove(alias) == null); } else if (remaining.size() == 1) { - final String select = remaining.get(0); + final String select = remaining.iterator().next(); assert !select.equals(key) : select; // Should have been removed by removeFromMultiValuesMap(…). error |= (values.putIfAbsent(alias, values.get(select)) != null); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java index 3500f6deb3..dd81894447 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java @@ -716,7 +716,10 @@ public class MetadataBuilder { * If there is no pending feature description, then invoking this method has no effect. * If new feature descriptions are added after this method call, they will be stored in a new element. * - * <p>This method does not need to be invoked unless a new "feature catalog description" node is desired.</p> + * <p>This method does not need to be invoked unless a new "feature catalog description" node is desired. + * It may also be useful when switching from writing feature types to writing coverage descriptions, + * because both classes appear under the same "content information" node. + * Invoking this method may avoid confusing ordering of those elements.</p> */ public final void newFeatureTypes() { if (featureDescription != null) { @@ -732,7 +735,10 @@ public class MetadataBuilder { * If new coverage descriptions are added after this method call, they will be stored in a new element. * * <p>This method does not need to be invoked unless a new "coverage description" node is desired, - * or the {@code electromagnetic} flag needs to be set to {@code true}.</p> + * or the {@code electromagnetic} flag needs to be set to {@code true}. It may also be useful when + * switching from writing coverage descriptions to writing feature types, + * because both classes appear under the same "content information" node. + * Invoking this method may avoid confusing ordering of those elements.</p> * * @param electromagnetic {@code true} if the next {@code CoverageDescription} to create * will be a description of measurements in the electromagnetic spectrum. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java index 31c0c3c43e..d332b9b431 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java @@ -746,61 +746,58 @@ public final class CollectionsExt extends Static { } /** - * Adds a value in a pseudo multi-values map. The multi-values map is simulated by a map of lists. - * The map can be initially empty - lists will be created as needed. + * Adds a value in a pseudo multi-values map. The multi-values map is simulated by a map of sets. + * The map can be initially empty: sets will be created as needed, with an optimization for the + * common case where the majority of keys are associated to exactly one value. + * Null values are accepted. * * @param <K> the type of key elements in the map. - * @param <V> the type of value elements in the lists. + * @param <V> the type of value elements in the sets. * @param map the multi-values map where to add an element. * @param key the key of the element to add. Can be null if the given map supports null keys. * @param value the value of the element to add. Can be null. - * @return the list where the given value has been added. May be unmodifiable. + * @return the set where the given value has been added. May be unmodifiable. */ - public static <K,V> List<V> addToMultiValuesMap(final Map<K,List<V>> map, final K key, final V value) { - final List<V> singleton = Collections.singletonList(value); - List<V> values = map.put(key, singleton); - if (values == null) { - return singleton; // This is the most common case. - } - if (values.size() <= 1) { - values = new ArrayList<>(values); - } - if (map.put(key, values) != singleton) { - throw new ConcurrentModificationException(); - } - values.add(value); - return values; + public static <K,V> Set<V> addToMultiValuesMap(final Map<K,Set<V>> map, final K key, final V value) { + return map.merge(key, Collections.singleton(value), (values, singleton) -> { + final Set<V> dest = (values.size() > 1) ? values : new LinkedHashSet<>(values); + return dest.addAll(singleton) ? dest : values; + }); } /** - * Removes a value in a pseudo multi-values map. The multi-values map is simulated by a map of lists. - * If more than one occurrence of the given value is found in the list, only the first occurrence is - * removed. If the list become empty after this method call, that list is removed from the map. + * Removes a value in a pseudo multi-values map. The multi-values map is simulated by a map of sets. + * If the set become empty after this method call, that set is removed from the map and this method + * returns the empty set. * * @param <K> the type of key elements in the map. * @param <V> the type of value elements in the lists. * @param map the multi-values map where to remove an element. * @param key the key of the element to remove. Can be null if the given map supports null keys. * @param value the value of the element to remove. Can be null. - * @return list of remaining elements after the removal, or {@code null} if no list is mapped to the given key. + * @return set of remaining elements after the removal, or {@code null} if no set is mapped to the given key. */ - public static <K,V> List<V> removeFromMultiValuesMap(final Map<K,List<V>> map, final K key, final V value) { - List<V> values = map.get(key); - if (values != null) { - final boolean isEmpty; - switch (values.size()) { - case 0: isEmpty = true; break; - case 1: isEmpty = Objects.equals(value, values.get(0)); break; - default: isEmpty = values.remove(value) && values.isEmpty(); break; - } - if (isEmpty) { - if (map.remove(key) != values) { - throw new ConcurrentModificationException(); + public static <K,V> Set<V> removeFromMultiValuesMap(final Map<K,Set<V>> map, final K key, final V value) { + final Set<V> remaining = map.compute(key, (k, values) -> { + if (values != null) { + final boolean isEmpty; + switch (values.size()) { + case 0: isEmpty = true; break; + case 1: isEmpty = values.contains(value); break; + default: isEmpty = values.remove(value) && values.isEmpty(); break; + } + if (isEmpty) { + return Collections.emptySet(); } - values = Collections.emptyList(); + } + return values; + }); + if (remaining != null && remaining.isEmpty()) { + if (map.remove(key) != remaining) { + throw new ConcurrentModificationException(); } } - return values; + return remaining; } /** diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/CollectionsExtTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/CollectionsExtTest.java index f2be3db691..f8133cdfb4 100644 --- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/CollectionsExtTest.java +++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/privy/CollectionsExtTest.java @@ -102,7 +102,7 @@ public final class CollectionsExtTest extends TestCase { */ @Test public void testAddAndRemoveToMultiValuesMap() { - final Map<String, List<Integer>> map = new LinkedHashMap<>(); + final var map = new LinkedHashMap<String, Set<Integer>>(); final Integer A1 = 2; final Integer A2 = 4; final Integer B1 = 3; @@ -130,7 +130,7 @@ public final class CollectionsExtTest extends TestCase { */ @Test public void testToCaseInsensitiveNameMap() { - final List<Map.Entry<String,String>> elements = new ArrayList<>(); + final var elements = new ArrayList<Map.Entry<String,String>>(); elements.add(new AbstractMap.SimpleEntry<>("AA", "AA")); elements.add(new AbstractMap.SimpleEntry<>("Aa", "Aa")); elements.add(new AbstractMap.SimpleEntry<>("BB", "BB"));