This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 96061e870a `TableInfo` needs to support the new types used in EPSG 
version 10+. In particular, we use `DynamicReferenceFrance` as a mixin 
interface. Also be more precise in the number of dimensions of geographic CRS.
96061e870a is described below

commit 96061e870a96bbff8a85c3969ba291ff2ed51487
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Aug 30 23:58:42 2025 +0200

    `TableInfo` needs to support the new types used in EPSG version 10+.
    In particular, we use `DynamicReferenceFrance` as a mixin interface.
    Also be more precise in the number of dimensions of geographic CRS.
---
 .../sis/metadata/privy/NameToIdentifier.java       |   2 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |   3 +-
 .../sis/referencing/datum/DatumOrEnsemble.java     |  17 +-
 .../referencing/factory/sql/AuthorityCodes.java    |  27 +--
 .../referencing/factory/sql/EPSGCodeFinder.java    |  26 ++-
 .../referencing/factory/sql/EPSGDataAccess.java    |  66 ++++----
 .../sis/referencing/factory/sql/EPSGInstaller.java |   9 +-
 .../sis/referencing/factory/sql/SQLTranslator.java |  11 --
 .../sis/referencing/factory/sql/TableInfo.java     | 182 +++++++++++++++++----
 .../referencing/factory/sql/EPSGFactoryTest.java   |  23 ++-
 .../referencing/factory/sql/epsg/DebugTools.sql    |  10 ++
 11 files changed, 252 insertions(+), 124 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
index 6e154515c4..678b159013 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/privy/NameToIdentifier.java
@@ -230,7 +230,7 @@ public final class NameToIdentifier implements Identifier {
      * @return {@code true} if the primary name or at least one alias matches 
the given {@code name}.
      */
     public static boolean isHeuristicMatchForName(final Identifier name, final 
Collection<GenericName> aliases,
-            CharSequence toSearch, final Simplifier simplifier)
+                                                  CharSequence toSearch, final 
Simplifier simplifier)
     {
         if (toSearch != null) {
             CharSequence code = (name != null) ? name.getCode() : null;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
index 9822787bbf..67e0e6a6ae 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/IdentifiedObjects.java
@@ -768,7 +768,8 @@ public final class IdentifiedObjects extends Static {
              */
             return ((AbstractIdentifiedObject) 
object).isHeuristicMatchForName(name);
         } else {
-            return NameToIdentifier.isHeuristicMatchForName(object.getName(), 
object.getAlias(), name,
+            return NameToIdentifier.isHeuristicMatchForName(
+                    object.getName(), object.getAlias(), name,
                     NameToIdentifier.Simplifier.DEFAULT);
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
index b6a813a112..d08ce7f653 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
@@ -427,18 +427,27 @@ public final class DatumOrEnsemble extends Static {
         if (match != null) {
             return match;
         }
-        String name = ensemble.getName().getCode();
-        if (IdentifiedObjects.isHeuristicMatchForName(datum, name)) {
+        /*
+         * We could not answer the question using identifiers. Try using the 
names.
+         * The primary name is likely to not match, because ensemble names in 
EPSG
+         * dataset often ends with "ensemble" while datum names often do not. 
But
+         * we are more interrested in the ensemble's aliases in the next line.
+         */
+        if (IdentifiedObjects.isHeuristicMatchForName(ensemble, 
datum.getName().getCode())) {
             return true;
         }
+        /*
+         * Try to remove the "ensemble" prefix in the datum ensemble name and 
try again.
+         * This time, the comparison will also check `datum` aliases instead 
of `ensemble`.
+         */
+        String name = ensemble.getName().getCode();
         if (name.endsWith(ENSEMBLE)) {
             int i = name.length() - ENSEMBLE.length();
             if (i > (i = CharSequences.skipTrailingWhitespaces(name, 0, i))) {
                 name = name.substring(0, i);    // Remove the "ensemble" 
suffix.
-                return IdentifiedObjects.isHeuristicMatchForName(datum, name);
             }
         }
-        return false;
+        return IdentifiedObjects.isHeuristicMatchForName(datum, name);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
index afa766de0e..5328065288 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
@@ -24,6 +24,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.PreparedStatement;
 import java.sql.Statement;
+import org.opengis.referencing.IdentifiedObject;
 import org.apache.sis.metadata.sql.privy.SQLUtilities;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.IntegerList;
@@ -79,10 +80,10 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
     private final transient EPSGDataAccess factory;
 
     /**
-     * The interface of referencing objects for which this map contains the 
code.
-     * May be a super-interface of the type specified to the constructor.
+     * The key to use for caching this set of authority codes.
+     * May be a generalization of the key given at construction time.
      */
-    final Class<?> type;
+    final Object cacheKey;
 
     /**
      * The SQL commands that this {@code AuthorityCodes} may need to execute.
@@ -122,13 +123,20 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
     private transient IntegerList codes;
 
     /**
-     * Creates a new map of authority codes for the specified type.
+     * Creates a new map of authority codes for the specified object instance 
of class.
+     * The {@code object} argument shall be one of the following types:
+     *
+     * <ul>
+     *   <li>An {@link IdentifiedObject} instance.</li>
+     *   <li>The {@link Class} of an {@code IdentifiedObject}. It may be an 
implementation class.</li>
+     *   <li>An opaque key computed by {@link 
TableInfo#toCacheKey(IdentifiedObject)} (useful for caching).</li>
+     * </ul>
      *
      * @param table    the table to query.
-     * @param type     the type to query.
+     * @param object   an {@link IdentifiedObject}, a {@code Class} or an 
opaque cache key.
      * @param factory  the factory originator.
      */
-    AuthorityCodes(final TableInfo table, final Class<?> type, final 
EPSGDataAccess factory) throws SQLException {
+    AuthorityCodes(final TableInfo table, final Object object, final 
EPSGDataAccess factory) throws SQLException {
         this.factory = factory;
         sql = new String[NUM_QUERIES];
         statements = new Statement[NUM_QUERIES];
@@ -141,7 +149,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
         final int columnNameStart = buffer.append("SELECT ").length();
         final int columnNameEnd = buffer.append(table.codeColumn).length();
         buffer.append(" FROM ").append(table.fromClause);
-        this.type = table.where(factory, type, buffer);
+        cacheKey = table.appendWhere(factory, object, buffer);
         final int conditionStart = buffer.length();
         if (table.showColumn != null) {
             buffer.append(table.showColumn).append("=TRUE AND ");
@@ -311,8 +319,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
 
     /**
      * Returns the object name associated to the given authority code, or 
{@code null} if none.
-     * If there is no name for the {@linkplain #type} of object being queried, 
then this method
-     * returns {@code null}.
+     * If there is no name for the object being queried, then this method 
returns {@code null}.
      *
      * @param  code  the code for which to get the description. May be a 
string or an integer.
      * @return the description for the given code, or {@code null} if none.
@@ -399,7 +406,7 @@ final class AuthorityCodes extends 
AbstractMap<String,String> implements Seriali
                 size = "size" + (results != null ? " ≥ " : " = ") + 
codes.size();
             }
         }
-        return Strings.toString(getClass(), "type", type.getSimpleName(), 
null, size);
+        return Strings.toString(getClass(), "cacheKey", cacheKey, null, size);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
index ebe9fe21ef..5c95acd0bc 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
@@ -158,16 +158,16 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder 
{
             final Class<T> type, final T dependency, final boolean ignoreAxes) 
throws FactoryException
     {
         if (dependency != null) try {
-            final Class<? extends IdentifiedObject> pt = declaredType;
-            final boolean previous = isIgnoringAxes();
+            final Class<? extends IdentifiedObject> previousType = 
declaredType;
+            final boolean previousAxes = isIgnoringAxes();
             final Set<IdentifiedObject> find;
             try {
-                setIgnoringAxes(ignoreAxes | previous);
+                setIgnoringAxes(ignoreAxes | previousAxes);
                 declaredType = type;
                 find = find(dependency);
             } finally {
-                declaredType = pt;
-                setIgnoringAxes(previous);
+                declaredType = previousType;
+                setIgnoringAxes(previousAxes);
             }
             final Set<Number> filters = JDK19.newLinkedHashSet(find.size());
             for (final IdentifiedObject dep : find) {
@@ -416,7 +416,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             };
         } else try {
             // Not a supported type. Returns all codes if not too expensive.
-            return dao.getAuthorityCodes(declaredType, addTo);
+            return dao.getAuthorityCodes(object, addTo);
         } catch (SQLException exception) {
             throw databaseFailure(exception);
         }
@@ -462,7 +462,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
          * It may be absent (typically, only datums or reference frames have 
that condition).
          */
         buffer.append("SELECT ").append(source.codeColumn).append(" FROM 
").append(source.fromClause);
-        source.where(dao, object, buffer);          // Unconditionally append 
a "WHERE" clause.
+        source.appendWhere(dao, object, buffer);    // Unconditionally append 
a "WHERE" clause.
         boolean isNext = false;
         for (final Condition filter : filters) {
             isNext |= filter.appendToWhere(buffer, isNext);
@@ -638,9 +638,6 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
         /** Snapshot of the search domain as it was at collection construction 
time. */
         private final Domain domain;
 
-        /** Whether to try to easy search methods before the expansive method. 
*/
-        private final boolean optimize;
-
         /** Sequential number of the algorithm used for filling the {@link 
#codes} collection so far. */
         private byte searchMethod;
 
@@ -658,8 +655,7 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             this.source = source;
             this.domain = getSearchDomain();
             this.codes  = new LinkedHashSet<>();
-            optimize = (domain != Domain.EXHAUSTIVE_VALID_DATASET);
-            if (optimize) {
+            if (domain != Domain.EXHAUSTIVE_VALID_DATASET) {
                 for (final Identifier id : object.getIdentifiers()) {
                     if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) 
try {
                         codes.add(Integer.valueOf(id.getCode()));
@@ -699,17 +695,17 @@ crs:    if (isInstance(CoordinateReferenceSystem.class, 
object)) {
             do {
                 switch (searchMethod) {
                     case 0: {   // Fetch codes from the name.
-                        if (optimize) {
+                        if (domain != Domain.EXHAUSTIVE_VALID_DATASET) {
                             name = getName(object);
                             if (name != null) {     // Should never be null, 
but we are paranoiac.
                                 namePattern = dao.toLikePattern(name);
-                                dao.findCodesFromName(source, 
object.getClass(), namePattern, name, addTo);
+                                dao.findCodesFromName(source, 
TableInfo.toCacheKey(object), namePattern, name, addTo);
                             }
                         }
                         break;
                     }
                     case 1: {   // Fetch codes from the aliases.
-                        if (optimize) {
+                        if (domain != Domain.EXHAUSTIVE_VALID_DATASET) {
                             if (namePattern != null) {
                                 dao.findCodesFromAlias(source, namePattern, 
name, addTo);
                             }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index f3e64af99e..d25d4afe10 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -241,7 +241,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * and returns {@code false} if some are found (thus blocking the call to 
{@link #close()}
      * by the {@link 
org.apache.sis.referencing.factory.ConcurrentAuthorityFactory} timer).</p>
      */
-    private final Map<Class<?>, CloseableReference> authorityCodes = new 
HashMap<>();
+    private final Map<Object, CloseableReference> authorityCodes = new 
HashMap<>();
 
     /**
      * Cache for axis names, conventional reference systems, realization 
methods or naming systems.
@@ -561,19 +561,14 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * This method may do nothing if getting all codes would be too expensive
      * (especially since the caller would instantiate all enumerated objects).
      *
-     * @param  type   the spatial reference objects type, or {@code null} if 
unspecified.
-     * @param  addTo  the collection where to add all codes.
+     * @param  object  the object to search in the database.
+     * @param  addTo   the collection where to add all codes.
      * @return whether the collection has changed as a result of this method 
call.
      * @throws SQLException if an error occurred while querying the database.
      */
-    final boolean getAuthorityCodes(final Class<? extends IdentifiedObject> 
type, final Collection<Integer> addTo) throws SQLException {
-        if (type != null) {
-            final AuthorityCodes codes = getCodeMap(type, null, false);
-            if (codes != null) {
-                return codes.getAllCodes(addTo);
-            }
-        }
-        return false;
+    final boolean getAuthorityCodes(final IdentifiedObject object, final 
Collection<Integer> addTo) throws SQLException {
+        final AuthorityCodes codes = getCodeMap(TableInfo.toCacheKey(object), 
null, false);
+        return (codes != null) && codes.getAllCodes(addTo);
     }
 
     /**
@@ -581,17 +576,19 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * The cautions documented in {@link #getAuthorityCodes(Class)} apply also 
to this map.
      * If the given type is unsupported or too generic, returns {@code null}.
      *
-     * @param  type     the spatial reference objects type.
-     * @param  source   the table from which to get the authority codes, or 
{@code null} for automatic.
-     * @param  publish  whether the returned authority codes will be given to 
a user outside this package.
+     * @param  cacheKey  object class or {@link 
TableInfo#toCacheKey(IdentifiedObject)} value.
+     * @param  source    the table from which to get the authority codes, or 
{@code null} for automatic.
+     * @param  publish   whether the returned authority codes will be given to 
a user outside this package.
      * @return the map of authority codes associated to their names, or {@code 
null} if unsupported.
      * @throws FactoryException if access to the underlying database failed.
      *
      * @see #getAuthorityCodes(Class)
      * @see #getDescriptionText(Class, String)
      */
-    private synchronized AuthorityCodes getCodeMap(final Class<?> type, 
TableInfo source, boolean publish) throws SQLException {
-        CloseableReference reference = authorityCodes.get(type);
+    private synchronized AuthorityCodes getCodeMap(final Object cacheKey, 
TableInfo source, boolean publish)
+            throws SQLException
+    {
+        CloseableReference reference = authorityCodes.get(cacheKey);
         if (reference != null) {
             AuthorityCodes existing = reference.get();
             if (existing != null) {
@@ -599,26 +596,29 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
                 return existing;
             }
         }
-        if (source == null) {
-            for (TableInfo c : TableInfo.values()) {
-                if (c.isSpecificEnough() && c.type.isAssignableFrom(type)) {
+        if (source != null) {
+            assert source.isSpecificEnough() && 
source.type.isAssignableFrom(TableInfo.typeOfCacheKey(cacheKey)) : source;
+        } else {
+            final Class<?> userType = TableInfo.typeOfCacheKey(cacheKey);
+            for (TableInfo candidate : TableInfo.values()) {
+                if (candidate.isSpecificEnough() && 
candidate.type.isAssignableFrom(userType)) {
                     if (source != null) {
                         return null;        // The specified type is too 
generic.
                     }
-                    source = c;
+                    source = candidate;
                 }
             }
             if (source == null) {
                 return null;                // The specified type is 
unsupported.
             }
         }
-        AuthorityCodes codes = new AuthorityCodes(source, type, this);
+        AuthorityCodes codes = new AuthorityCodes(source, cacheKey, this);
         /*
          * Maybe an instance already existed but was not found above because 
the user specified some
          * implementation class instead of an interface class. Before to 
return a newly created map,
          * check again in the cached maps using the type computed by 
AuthorityCodes itself.
          */
-        reference = authorityCodes.get(codes.type);
+        reference = authorityCodes.get(codes.cacheKey);
         if (reference != null) {
             AuthorityCodes existing = reference.get();
             if (existing != null) {
@@ -629,10 +629,10 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
         }
         if (reference == null) {
             reference = codes.createReference();
-            authorityCodes.put(codes.type, reference);
+            authorityCodes.put(codes.cacheKey, reference);
         }
-        if (type != codes.type) {
-            authorityCodes.put(type, reference);
+        if (cacheKey != codes.cacheKey) {
+            authorityCodes.put(cacheKey, reference);
         }
         reference.published |= publish;
         return codes;
@@ -780,17 +780,17 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
     /**
      * Finds the authority codes for the given name.
      *
-     * @param  source   information about the table where the code should 
appear.
-     * @param  type     the type of object to search. Should be assignable to 
{@code source.type}.
-     * @param  pattern  the name to search as a pattern that can be used with 
{@code LIKE}.
-     * @param  name     the original name. This is a temporary workaround for 
a Derby bug (see {@code filterFalsePositive(…)}).
-     * @param  addTo    the collection where to add the codes that have been 
found.
+     * @param  source    information about the table where the code should 
appear.
+     * @param  cacheKey  object class or {@link 
TableInfo#toCacheKey(IdentifiedObject)} value.
+     * @param  pattern   the name to search as a pattern that can be used with 
{@code LIKE}.
+     * @param  name      the original name. This is a temporary workaround for 
a Derby bug (see {@code filterFalsePositive(…)}).
+     * @param  addTo     the collection where to add the codes that have been 
found.
      * @throws SQLException if an error occurred while querying the database.
      */
-    final void findCodesFromName(final TableInfo source, final Class<?> type, 
final String pattern, final String name, final Collection<Integer> addTo)
-            throws SQLException
+    final void findCodesFromName(final TableInfo source, final Object 
cacheKey, final String pattern, final String name,
+                                 final Collection<Integer> addTo) throws 
SQLException
     {
-        AuthorityCodes codes = getCodeMap(type, source, false);
+        AuthorityCodes codes = getCodeMap(cacheKey, source, false);
         if (codes != null) {
             codes.findCodesFromName(pattern, name, addTo);
         }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
index 1b4deea485..82828676ab 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
@@ -74,17 +74,10 @@ final class EPSGInstaller extends ScriptRunner {
                     "CRS Kind",          "VARCHAR(13)",    // Original: 
VARCHAR(24) for column "coord_ref_sys_kind".
                     "CS Kind",           "VARCHAR(15)",    // Original: 
VARCHAR(24) for column "coord_sys_type".
                     "Supersession Type", "VARCHAR(12)",    // Original: 
VARCHAR(50) for column "supersession_type".
-                    "Table Name",     ENUM_REPLACEMENT);   // Original: 
VARCHAR(80) for columns "object_table_name".
+                    "Table Name",        "VARCHAR(36)");   // Original: 
VARCHAR(80) for columns "object_table_name".
         }
     }
 
-    /**
-     * The <abbr>SQL</abbr> type to use as a replacement for enumerated values 
in databases that do not
-     * support enumerations. The maximal length declared in this constant 
should be the greatest length
-     * declared in {@code VARCHAR(…)} substitutions done when {@link 
#isEnumTypeSupported} is false.
-     */
-    static final String ENUM_REPLACEMENT = "VARCHAR(36)";
-
     /**
      * Invoked for each text found in a SQL statement. This method replaces 
{@code ''} by {@code Null}.
      * The intent is to consistently use the null value for meaning "no 
information", which is not the
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
index b275964bab..a7ec894923 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java
@@ -231,8 +231,6 @@ public class SQLTranslator implements UnaryOperator<String> 
{
      * than character varying. In such case, this field contains the 
enumeration type. If {@code null}, then
      * then column type is {@code VARCHAR} and the cast can be omitted.
      * If non-null, this string should contain the identifier quotes.
-     *
-     * @see #useEnumerations()
      */
     private String tableNameEnum;
 
@@ -596,15 +594,6 @@ check:  for (;;) {
         return useBoolean;
     }
 
-    /**
-     * Returns {@code true} if the database uses enumeration values where 
applicable.
-     * This method use the {@value #ENUMERATION_COLUMN} column as a sentinel 
value for
-     * detecting whether enumerations are used for the whole <abbr>EPSG</abbr> 
database.
-     */
-    final boolean useEnumerations() {
-        return tableNameEnum != null;
-    }
-
     /**
      * Converts a mixed-case table name to the convention used in the database.
      * The names of the tables for the two conventions are listed in a table 
in the Javadoc of this class.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
index b6d6726fa9..2622020443 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.referencing.factory.sql;
 
+import java.util.Map;
 import javax.measure.Unit;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.referencing.IdentifiedObject;
@@ -47,7 +48,7 @@ import org.apache.sis.referencing.crs.DefaultGeocentricCRS;
  * As of ISO 19111:2019, we have no standard way to identify the geocentric 
case from a {@link Class} argument
  * because the standard does not provide the {@code GeocentricCRS} interface. 
This implementation fallbacks on
  * the <abbr>SIS</abbr>-specific geocentric <abbr>CRS</abbr> class. This 
special case is implemented in the
- * {@link #where(EPSGDataAccess, IdentifiedObject, StringBuilder)} method.
+ * {@link #appendWhere(EPSGDataAccess, Object, StringBuilder)} method.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  */
@@ -63,7 +64,7 @@ enum TableInfo {
             new Class<?>[] { ProjectedCRS.class,   GeographicCRS.class,   
DefaultGeocentricCRS.class,
                              VerticalCRS.class,    CompoundCRS.class,     
EngineeringCRS.class,
                              DerivedCRS.class,     TemporalCRS.class,     
ParametricCRS.class},     // See comment below
-            new String[]   {"projected",          "geographic",          
"geocentric",
+            new String[]   {"projected",          "geographic 2D",       
"geocentric",              // 3D case handled below
                             "vertical",           "compound",            
"engineering",
                             "derived",            "temporal",            
"parametric"},             // See comment below
             "SHOW_CRS", true),
@@ -82,9 +83,11 @@ enum TableInfo {
             "DATUM_CODE",
             "DATUM_NAME",
             "DATUM_TYPE",
-            new Class<?>[] { GeodeticDatum.class,  VerticalDatum.class,   
EngineeringDatum.class,
+            new Class<?>[] { DatumEnsemble.class,  // Need to be first because 
Apache SIS uses as mixin interface.
+                             GeodeticDatum.class,  VerticalDatum.class,   
EngineeringDatum.class,
                              TemporalDatum.class,  ParametricDatum.class},
-            new String[]   {"geodetic",           "vertical",            
"engineering",
+            new String[]   {"ensemble",
+                            "geodetic",           "vertical",            
"engineering",
                             "temporal",           "parametric"},         // 
Same comment as in the CRS case above.
             null, true),
 
@@ -192,6 +195,28 @@ enum TableInfo {
             "UNIT_OF_MEAS_NAME",
             null, null, null, null, false);
 
+    /**
+     * Types to consider as synonymous for searching purposes. This map exists 
for historical reasons,
+     * because dynamic datum and datum ensemble did not existed in older 
<abbr>ISO</abbr> 19111 standards.
+     * If an object to search is "geodetic", there is a possibility that it is 
defined in the old way and
+     * actually appears as a "dynamic geodetic" or "ensemble" in the 
<abbr>EPSG</abbr> geodetic dataset.
+     *
+     * <p>The "geographic 3D" case is handled in a special way. It is 
considered as a synonymous of
+     * "geographic 2D" only when we don't know the number of dimensions.</p>
+     */
+    private static final Map<String, String[]> SYNONYMOUS_TYPES = Map.of(
+            "geodetic",      new String[] {"dynamic geodetic", "ensemble"},
+            "geographic 2D", new String[] {"geographic 3D"});
+
+    /**
+     * Types to replace by specialized types when the user-specified instance 
implements a mixin interface.
+     * For example, {@link DynamicReferenceFrame} means to not search for any 
geodetic datum, but only for
+     * dynamic geodetic datum.
+     */
+    private static final Map<String, String> DYNAMIC_TYPES = Map.of(
+            "geodetic", "dynamic geodetic");
+            // We would expect a "dynamic vertical" as well, but we don't see 
it yet in EPSG database.
+
     /**
      * The class of object to be created (usually a GeoAPI interface).
      */
@@ -232,6 +257,7 @@ enum TableInfo {
 
     /**
      * Names of {@link #subTypes} in the database, or {@code null} if none.
+     * This array must have the same length as {@link #subTypes}.
      */
     private final String[] typeNames;
 
@@ -286,61 +312,145 @@ enum TableInfo {
     }
 
     /**
-     * Appends a {@code WHERE} clause together with a condition for searching 
the specified object.
-     * This method delegates to {@link #where(EPSGDataAccess, Class, 
StringBuilder)} with the type
-     * of the given object, except that some object properties may be 
inspected for resolving ambiguities.
+     * Returns a key which describes the type and/or the number of dimensions 
of the given object.
+     * The current implementation relies on the fact that {@link 
GeographicCRS} is the only type
+     * for which the current <abbr>EPSG</abbr> database distinguishes the 
number of dimensions,
+     * but callers should not depend on this assumption as it may change in 
any future version.
      *
-     * @param  factory  the factory which is writing a <abbr>SQL</abbr> 
statement.
-     * @param  object   the object to search in the database.
-     * @param  buffer   where to append the {@code WHERE} clause.
+     * <h4>Maintenance note</h4>
+     * If the implementation of this method is modified, then the extraction 
of {@code dimension} and
+     * {@code userType} properties in the {@link #appendWhere(EPSGDataAccess, 
Object, StringBuilder)}
+     * method body must be updated accordingly.
      */
-    final void where(final EPSGDataAccess factory, final IdentifiedObject 
object, final StringBuilder buffer) {
-        Class<?> userType = object.getClass();
+    static Object toCacheKey(final IdentifiedObject object) {
         if (object instanceof GeodeticCRS) {
             final CoordinateSystem cs = ((GeodeticCRS) 
object).getCoordinateSystem();
             if (cs instanceof EllipsoidalCS) {
-                userType = GeographicCRS.class;
-            } else if (cs instanceof CartesianCS || cs instanceof SphericalCS) 
{
-                userType = DefaultGeocentricCRS.class;
+                return cs.getDimension();
             }
         }
-        where(factory, userType, buffer);
+        return object.getClass();
     }
 
     /**
-     * Appends a {@code WHERE} clause together with a condition for searching 
the most specific subtype,
-     * if such condition can be added. The clause appended by this method 
looks like the following example
-     * (details may vary because of enumeration values):
+     * Extracts the type from a value computed by {@link 
#toCacheKey(IdentifiedObject)}.
+     *
+     * @param  object  value computed by {@link #toCacheKey(IdentifiedObject)}.
+     * @return the class of the object to search, ignoring the number of 
dimensions.
+     * @throws ClassCastException if the given object has not been created by 
{@link #toCacheKey(IdentifiedObject)}.
+     */
+    static Class<?> typeOfCacheKey(final Object object) {
+        if (object instanceof Integer) {
+            return GeographicCRS.class;
+        }
+        return (Class<?>) object;
+    }
+
+    /**
+     * Appends a {@code WHERE} clause together with a condition for searching 
the most specific subtype.
+     * The clause appended by this method looks like the following example:
      *
      * {@snippet lang="sql" :
-     *     WHERE COORD_REF_SYS_KIND LIKE 'geographic%' AND
+     *     WHERE (COORD_REF_SYS_KIND = 'geographic 2D' OR COORD_REF_SYS_KIND = 
'geographic 3D') AND
      *     }
      *
-     * The caller shall add at least one condition after this method call.
+     * The <abbr>SQL</abbr> fragment will have a trailing {@code WHERE} or 
{@code AND} keyword.
+     * Therefore, the caller shall add at least one condition after this 
method call.
+     *
+     * <h4>Object type</h4>
+     * The {@code object} argument shall be one of the following types:
+     *
+     * <ul>
+     *   <li>An {@link IdentifiedObject} instance.</li>
+     *   <li>The {@link Class} of an {@code IdentifiedObject}. It may be an 
implementation class.</li>
+     *   <li>An opaque key computed by {@link 
#toCacheKey(IdentifiedObject)}.</li>
+     * </ul>
+     *
+     * This method returns a generalization of the {@code object} argument: 
either a GeoAPI interface,
+     * or {@code object} if it was a cache key computed by {@link 
#toCacheKey(IdentifiedObject)}.
      *
-     * @param  factory   the factory which is writing a <abbr>SQL</abbr> 
statement.
-     * @param  userType  the type specified by the user.
-     * @param  buffer    where to append the {@code WHERE} clause.
-     * @return the subtype, or {@link #type} if no subtype was found.
+     * @param  factory  the factory which is writing a <abbr>SQL</abbr> 
statement.
+     * @param  object   the instance, class or cache key to search in the 
database.
+     * @param  buffer   where to append the {@code WHERE} clause.
+     * @return the {@code object} argument, potentially generalized.
      */
-    final Class<?> where(final EPSGDataAccess factory, final Class<?> 
userType, final StringBuilder buffer) {
+    final Object appendWhere(final EPSGDataAccess factory, final Object 
object, final StringBuilder buffer) {
+        final int dimension;            // 0 if not applicable. This is 
applicable only to `GeographicCRS`.
+        final Class<?> userType;
+        if (object instanceof Integer) {
+            dimension = (Integer) object;
+            userType  = GeographicCRS.class;
+        } else if (object instanceof Class<?>) {
+            userType  = (Class<?>) object;
+            dimension = 0;
+        } else if (object instanceof GeodeticCRS) {
+            final CoordinateSystem cs = ((GeodeticCRS) 
object).getCoordinateSystem();
+            if (cs instanceof EllipsoidalCS) {
+                userType  = GeographicCRS.class;
+                dimension = cs.getDimension();      // Intentionally 
restricted to this specific case.
+            } else {
+                if (cs instanceof CartesianCS || cs instanceof SphericalCS) {
+                    userType = DefaultGeocentricCRS.class;
+                } else {
+                    userType = object.getClass();
+                }
+                dimension = 0;
+            }
+        } else {
+            userType  = object.getClass();
+            dimension = 0;
+        }
+        /*
+         * Above code decomposed the given `object`.
+         * The rest of this method builds the SQL.
+         */
         buffer.append(" WHERE ");
         if (typeColumn != null) {
             for (int i=0; i<subTypes.length; i++) {
-                final Class<?> candidate = subTypes[i];
-                if (candidate.isAssignableFrom(userType)) {
-                    if (factory.translator.useEnumerations()) {
-                        buffer.append("CAST(").append(typeColumn).append(" AS 
")
-                                
.append(EPSGInstaller.ENUM_REPLACEMENT).append(')');
-                    } else {
-                        buffer.append(typeColumn);
+                final Class<?> subType = subTypes[i];
+                if (subType.isAssignableFrom(userType)) {
+                    /*
+                     * Found the type to request in the `COORD_REF_SYS_KIND` 
or `DATUM_TYPE` columns.
+                     * The mixin interfaces need to be handled in a special 
way.
+                     */
+                    String typeName = typeNames[i];
+                    if 
(DynamicReferenceFrame.class.isAssignableFrom(userType)) {
+                        typeName = DYNAMIC_TYPES.getOrDefault(typeName, 
typeName);
+                    }
+                    /*
+                     * We may need to look for more than one type if some 
information are missing
+                     * (for example, the dimension when EPSG distinguishes the 
2D and 3D cases).
+                     */
+                    String[] synonymous = SYNONYMOUS_TYPES.get(typeName);
+                    if (synonymous != null && dimension > 0 && dimension <= 9) 
{
+                        final String suffix = "2D".replace('2',  (char) ('0' + 
dimension));
+                        if (typeName.endsWith(suffix)) {
+                            synonymous = null;
+                        } else {
+                            for (String alternative : synonymous) {
+                                if (alternative.endsWith(suffix)) {
+                                    typeName = alternative;
+                                    synonymous = null;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    /*
+                     * Build the SQL `WHERE` clause.
+                     */
+                    buffer.append('(').append(typeColumn).append(" = 
'").append(typeName).append('\'');
+                    if (synonymous != null) {
+                        for (String alternative : synonymous) {
+                            buffer.append(" OR ").append(typeColumn).append(" 
= '").append(alternative).append('\'');
+                        }
                     }
-                    buffer.append(" LIKE '").append(typeNames[i]).append("%' 
AND ");
-                    return candidate;
+                    buffer.append(") AND ");
+                    return subType;
                 }
             }
         }
-        return type;
+        return (dimension != 0) ? dimension : type;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
index 68c7b489ff..3dcd46f972 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
@@ -699,14 +699,27 @@ public final class EPSGFactoryTest extends 
TestCaseWithLogs {
     @Test
     public void testDescriptionText() throws FactoryException {
         final EPSGFactory factory = dataEPSG.factory();
-        assertEquals("World Geodetic System 1984", 
factory.getDescriptionText(GeodeticDatum.class,      
"6326").get().toString(Locale.US));
-        assertEquals("Mean Sea Level",             
factory.getDescriptionText(VerticalDatum.class,      
"5100").get().toString(Locale.US));
-        assertEquals("NTF (Paris) / Nord France",  
factory.getDescriptionText(ProjectedCRS.class,      
"27591").get().toString(Locale.US));
-        assertEquals("NTF (Paris) / France II",    
factory.getDescriptionText(ProjectedCRS.class,      
"27582").get().toString(Locale.US));
-        assertEquals("Ellipsoidal height",         
factory.getDescriptionText(CoordinateSystemAxis.class, 
"84").get().toString(Locale.US));
+        assertDescriptionStarts("World Geodetic System 1984", factory, 
GeodeticDatum.class,      6326);
+        assertDescriptionStarts("Mean Sea Level",             factory, 
VerticalDatum.class,      5100);
+        assertDescriptionStarts("NTF (Paris) / Nord France",  factory, 
ProjectedCRS.class,      27591);
+        assertDescriptionStarts("NTF (Paris) / France II",    factory, 
ProjectedCRS.class,      27582);
+        assertDescriptionStarts("Ellipsoidal height",         factory, 
CoordinateSystemAxis.class, 84);
         loggings.assertNoUnexpectedLog();
     }
 
+    /**
+     * Asserts that the description text for the given code starts with the 
expected value.
+     * We do not require a full match because suffix such as "ensemble" may or 
may not be present
+     * depending on the version of the <abbr>EPSG</abbr> database.
+     */
+    private static void assertDescriptionStarts(final String expected, final 
EPSGFactory factory,
+            final Class<? extends IdentifiedObject> type, final int code) 
throws FactoryException
+    {
+        final var description = factory.getDescriptionText(type, 
Integer.toString(code));
+        assertTrue(description.isPresent(), expected);
+        assertTrue(description.get().toString(Locale.US).startsWith(expected), 
expected);
+    }
+
     /**
      * Tests the "UTM zone 10N" conversion (EPSG:16010).
      *
diff --git 
a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
 
b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
index 20346befa9..f4b76da0ad 100644
--- 
a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
+++ 
b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql
@@ -67,3 +67,13 @@ CREATE VIEW "Operation Method Type dimension" AS
  ORDER BY IS_CONVERSION;
 
 COMMENT ON VIEW "Operation Method Type dimension" IS 'Number of dimensions of 
Operation Method types.';
+
+
+--
+-- Summary of types of datum members in datum ensembles.
+--
+CREATE VIEW "Datum Member Type" AS
+ SELECT DISTINCT DATUM_TYPE
+ FROM "Datum" WHERE DATUM_CODE IN (SELECT DATUM_CODE FROM "Datum Ensemble 
Member");
+
+COMMENT ON VIEW "Datum Member Type" IS 'Types of datum members in datum 
ensembles.';


Reply via email to