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"));


Reply via email to