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 33dff2bc95 The `SCOPE_CODE` and `EXTENT_CODE` columns in the "Datum" 
and "Coordinate Reference System" tables of version 10+ of the EPSG database 
are replaced by the "Usage" intersection table. Apache SIS continues to check 
the old columns for compatibility with pre-existing installations of the EPSG 
dataset.
33dff2bc95 is described below

commit 33dff2bc9544b1200dcff1ad95bd9ec88061730a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Jul 25 18:34:58 2025 +0200

    The `SCOPE_CODE` and `EXTENT_CODE` columns in the "Datum" and "Coordinate 
Reference System"
    tables of version 10+ of the EPSG database are replaced by the "Usage" 
intersection table.
    Apache SIS continues to check the old columns for compatibility with 
pre-existing installations of the EPSG dataset.
---
 .../apache/sis/referencing/NamedIdentifier.java    |   2 +-
 .../factory/GeodeticAuthorityFactory.java          |   2 +
 .../referencing/factory/GeodeticObjectFactory.java |   3 -
 .../referencing/factory/sql/EPSGDataAccess.java    | 348 ++++++++++++---------
 .../sis/referencing/factory/sql/EPSGFactory.java   |   4 +-
 .../sis/referencing/factory/sql/SQLTranslator.java | 109 +++++--
 .../referencing/factory/sql/EPSGFactoryTest.java   |  12 +-
 7 files changed, 301 insertions(+), 179 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
index a80def7661..f96a89a977 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/NamedIdentifier.java
@@ -158,7 +158,7 @@ public class NamedIdentifier extends ImmutableIdentifier 
implements GenericName
      *   </tr><tr>
      *     <td>{@code "name"}</td>
      *     <td>{@link GenericName}</td>
-     *     <td>(none)</td>
+     *     <td>(many)</td>
      *   </tr><tr>
      *     <th colspan="3" class="hsep">Defined in parent class (reminder)</th>
      *   </tr><tr>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
index 30ebddb624..09cd80a83e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
@@ -177,6 +177,8 @@ public abstract class GeodeticAuthorityFactory extends 
AbstractFactory implement
     /**
      * Returns an object of the specified type from a code. This 
implementation forwards
      * the method call to the most specialized methods determined by the given 
type.
+     * For example, a call to {@code createObject(GeodeticCRS.class, code)} 
delegates
+     * to <code>{@linkplain #createGeodeticCRS(String) 
createGeodeticCRS}(code)</code>.
      *
      * @param  <T>   the compile-time value of the {@code type} argument.
      * @param  type  the type of object to create.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index d1c78322d9..c6b18a1f62 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@ -64,9 +64,6 @@ import org.apache.sis.util.logging.Logging;
 import org.apache.sis.io.wkt.Parser;
 import org.apache.sis.xml.XML;
 
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.referencing.datum.DynamicReferenceFrame;
-
 
 /**
  * Creates Coordinate Reference System (CRS) implementations, with their 
Coordinate System (CS) and Datum components.
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 1a43f3d334..1decf179da 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
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.factory.sql;
 
-import java.util.Arrays;
 import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
@@ -67,6 +66,7 @@ import org.opengis.referencing.datum.*;
 import org.opengis.referencing.operation.*;
 import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.referencing.ImmutableIdentifier;
+import org.apache.sis.referencing.DefaultObjectDomain;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.datum.BursaWolfParameters;
@@ -110,6 +110,7 @@ import org.apache.sis.util.privy.Constants;
 import org.apache.sis.util.privy.CollectionsExt;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.privy.URLs;
+import org.apache.sis.util.iso.Types;
 import org.apache.sis.temporal.LenientDateFormat;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
@@ -172,12 +173,11 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
     /**
      * EPSG codes of parameters containing the EPSG code of another object.
      * Those parameters are integers (stored as {@code double} in the database)
-     * without unit (associated to {@link Units#UNITY} in the database).
+     * without unit (i.e., associated to {@link Units#UNITY} in the database).
      */
-    private static final int[] EPSG_CODE_PARAMETERS = {
-        1048,       // EPSG code for Interpolation CRS
-        1062        // EPSG code for "standard" CT
-    };
+    private static final Set<Integer> EPSG_CODE_PARAMETERS = Set.of(
+        1048,   // The EPSG code for the CRS that should be used to 
interpolate gridded data.
+        1062);  // The EPSG code for the coordinate transformation that may be 
used to avoid iteration in geocentric translation by grid interpolation methods.
 
     /**
      * The deprecated ellipsoidal coordinate systems and their replacements. 
Those coordinate systems are deprecated
@@ -189,27 +189,16 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * <p>We perform those replacements for avoiding a "Unit conversion from 
“DMS” to “°” is non-linear" exception
      * at projected CRS creation time.</p>
      *
+     * @param  code  <abbr>EPSG</abbr> code of a coordinate system to 
potentially replace.
+     * @return <abbr>EPSG</abbr> code of the replacement, or {@code code} if 
there is no replacement to apply.
+     *
      * @see #replaceDeprecatedCS
      */
     @Workaround(library = "EPSG:6401-6420", version = "8.9")        // 
Deprecated in 2002 but still present in 2016.
-    private static final Map<Integer,Integer> DEPRECATED_CS = deprecatedCS();
-    static Map<Integer,Integer> deprecatedCS() {
-        final var m = new HashMap<Integer,Integer>(24);
-
-        // Ellipsoidal 2D CS. Axes: latitude, longitude. Orientations: north, 
east. UoM: degree
-        Integer replacement = 6422;
-        m.put(6402, replacement);
-        for (int code = 6405; code <= 6412; code++) {
-            m.put(code, replacement);
-        }
-
-        // Ellipsoidal 3D CS. Axes: latitude, longitude, ellipsoidal height. 
Orientations: north, east, up. UoM: degree, degree, metre.
-        replacement = 6423;
-        m.put(6401, replacement);
-        for (int code = 6413; code <= 6420; code++) {
-            m.put(code, replacement);
-        }
-        return m;
+    static int replaceDeprecatedCS(final int code) {
+        if (code == 6402 || (code >= 6405 && code <= 6412)) return 6422;    // 
Ellipsoidal 2D CS in degrees.
+        if (code == 6401 || (code >= 6413 && code <= 6420)) return 6423;    // 
Ellipsoidal 3D CS in degrees and metres.
+        return code;
     }
 
     /**
@@ -251,39 +240,45 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
     private final Map<Class<?>, CloseableReference> authorityCodes = new 
HashMap<>();
 
     /**
-     * Cache for axis names. This service is not provided by {@code 
ConcurrentAuthorityFactory}
-     * because {@link AxisName} objects are specific to the <abbr>EPSG</abbr> 
authority factory.
+     * Cache for axis names, conventional reference systems, realization 
methods or naming systems.
+     * We do not use the shared cache provided by {@code 
ConcurrentAuthorityFactory} for the following reasons:
      *
-     * @see #getAxisName(Integer)
-     */
-    private final Map<Integer, AxisName> axisNames = new HashMap<>();
-
-    /**
-     * Cache for conventional reference systems, which are instances of the 
generic {@link IdentifiedObject} type.
-     * This is not cached by {@code ConcurrentAuthorityFactory} because there 
is not yet a specific type for that.
-     * Since we are not using the shared cache, there is a possibility that 
many objects are created for the same
-     * conventional <abbr>RS</abbr> code. However, this duplication should not 
happen if each instance appears in
-     * only one datum ensemble created by {@link #createDatumEnsemble(Integer, 
Map)}.
+     * <ol>
+     *   <li>{@link InternationalString} for scopes are read from a table 
specific to the <abbr>EPSG</abbr> database.</li>
+     *   <li>{@link RealizationMethod} codes are read from a table specific to 
the <abbr>EPSG</abbr> database.</li>
+     *   <li>{@link AxisName} objects are specific to the <abbr>EPSG</abbr> 
authority factory.</li>
+     *   <li>Conventional Reference Systems (<abbr>RS</abbr>) do not have a 
specific GeoAPI type
+     *       that we could use in the shared cache. Conventional 
<abbr>RS</abbr> are represented
+     *       in ISO 19111 by instances of the generic {@link IdentifiedObject} 
type.</li>
+     *   <li></li>
+     *   <li>{@link NameSpace} objects are read from a table specific to the 
<abbr>EPSG</abbr> database.</li>
+     * </ol>
      *
-     * @see #createConventionalRS(Integer)
-     */
-    private final Map<Integer, IdentifiedObject> conventionalRS = new 
HashMap<>();
-
-    /**
-     * Cache for realization methods. This service is not provided by {@code 
ConcurrentAuthorityFactory}
-     * because the realization method table is specific to the 
<abbr>EPSG</abbr> authority factory.
+     * Since we are not using the shared cache, there is a possibility that 
many objects are created for the same code.
+     * However, this duplication should not happen often. For example, each 
conventional <abbr>RS</abbr> should appears
+     * in only one datum ensemble created by {@link 
#createDatumEnsemble(Integer, Map)}.
      *
+     * <p>Keys are {@link Long} except the keys for naming systems which are 
{@link String}.</p>
+     *
+     * @see #getAxisName(Integer)
      * @see #getRealizationMethod(Integer)
+     * @see #createConventionalRS(Integer)
+     * @see #createProperties(String, Integer, String, CharSequence, String, 
String, CharSequence, boolean)
      */
-    private final Map<Integer, RealizationMethod> realizationMethods = new 
HashMap<>();
+    private final Map<Object, Object> localCache = new HashMap<>();
 
     /**
-     * Cache of naming systems other than EPSG. There is usually few of them 
(at most 15).
-     * This is used for aliases.
+     * Returns a key for use in {@link #localCache}. The {@code type} argument 
can have any value,
+     * provided that all types of object stored in {@link #localCache} use a 
distinct {@code type}.
+     * Usually, all invocations of this method should use its own unique 
{@code type} value.
      *
-     * @see #createProperties(String, Integer, String, CharSequence, String, 
String, CharSequence, boolean)
+     * @param  type  an arbitrary value that identify the type of object.
+     * @param  code  <abbr>EPSG</code> code.
+     * @return key to use in {@link #localCache}.
      */
-    private final Map<String, NameSpace> namingSystems = new HashMap<>();
+    private static Long cacheKey(final int type, final int code) {
+        return (((long) type) << Integer.SIZE) | Integer.toUnsignedLong(code);
+    }
 
     /**
      * The properties to be given the objects to construct.
@@ -318,7 +313,7 @@ public class EPSGDataAccess extends 
GeodeticAuthorityFactory implements CRSAutho
      * coordinate system at CRS creation time. This flag should be set to 
{@code true} only when creating
      * the base CRS of a projected or derived CRS.
      *
-     * @see #DEPRECATED_CS
+     * @see #replaceDeprecatedCS(int)
      */
     private transient boolean replaceDeprecatedCS;
 
@@ -782,8 +777,8 @@ codes:  for (int i=0; i<codes.length; i++) {
     {
         assert Thread.holdsLock(this);
         assert sql.contains('"' + table + '"') : table;
-        assert (codeColumn == null) || sql.contains(codeColumn) || 
table.equals("Area") : codeColumn;
-        assert (nameColumn == null) || sql.contains(nameColumn) || 
table.equals("Area") : nameColumn;
+        assert (codeColumn == null) || sql.contains(codeColumn) || 
table.equals("Extent") : codeColumn;
+        assert (nameColumn == null) || sql.contains(nameColumn) || 
table.equals("Extent") : nameColumn;
         return executeQuery(table, sql, toPrimaryKeys(table, codeColumn, 
nameColumn, codes));
     }
 
@@ -1088,54 +1083,101 @@ codes:  for (int i=0; i<codes.length; i++) {
         }
     }
 
+    /**
+     * Get all domains for the object identified by the given code.
+     * If found, the domains are added in the {@link #properties} map.
+     * The table used by this method is new in version 10 of EPSG database.
+     *
+     * @param  table   the table of the object for which to get usage.
+     * @param  code    the code for the object in the given table.
+     */
+    private void addDomainProperty(final String table, final Integer code) 
throws SQLException, FactoryException {
+        final var domains = new ArrayList<ObjectDomain>();
+        try (ResultSet result = executeMetadataQuery("Usage",
+                "SELECT EXTENT_CODE, SCOPE_CODE FROM \"Usage\"" +
+                " WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?",
+                translator.toActualTableName(table), code))
+        {
+            while (result.next()) {
+                final Extent extent = createExtent(getString(code, result, 1));
+                final InternationalString scope = getScope(getInteger(code, 
result, 2));
+                if (scope != null || extent != null) {
+                    domains.add(new DefaultObjectDomain(scope, extent));
+                }
+            }
+        }
+        if (!domains.isEmpty()) {
+            properties.put(IdentifiedObject.DOMAINS_KEY, 
domains.toArray(ObjectDomain[]::new));
+        }
+    }
+
+    /**
+     * Returns the scope from the given authority code.
+     *
+     * @param  code  the <abbr>EPSG</code> code.
+     * @return the scope, or {@code null} if none.
+     */
+    private InternationalString getScope(final Integer code) throws 
SQLException, FactoryDataException {
+        final Long cacheKey = cacheKey(1, code);
+        var scope = (InternationalString) localCache.get(cacheKey);
+        if (scope == null) {
+            try (ResultSet rs = executeQuery("Scope", "SELECT SCOPE FROM 
\"Scope\" WHERE SCOPE_CODE=?", code)) {
+                while (rs.next()) {
+                    scope = 
ensureSingleton(Types.toInternationalString(rs.getString(1)), scope, code);
+                }
+            }
+            localCache.put(cacheKey, scope);
+        }
+        return scope;
+    }
+
     /**
      * Logs a warning saying that the given code is deprecated and returns the 
code of the proposed replacement.
      *
      * @param  table   the table of the deprecated code.
      * @param  code    the deprecated code.
-     * @return the proposed replacement (may be the "(none)" text).
+     * @return the proposed replacement (may be the "(none)" text). Never 
empty.
      */
     private String getSupersession(final String table, final Integer code, 
final Locale locale) throws SQLException {
         String reason = null;
-        Object replacedBy = null;
-        try (ResultSet result = executeMetadataQuery("Deprecation",
+        String replacedBy;
+search: try (ResultSet result = executeMetadataQuery("Deprecation",
                 "SELECT DEPRECATION_REASON, REPLACED_BY FROM \"Deprecation\"" +
                 " WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?",
                 translator.toActualTableName(table), code))
         {
             while (result.next()) {
-                reason     = getOptionalString (result, 1);
-                replacedBy = getOptionalInteger(result, 2);
-                if (replacedBy != null) break;                  // Prefer the 
first record providing a replacement.
+                reason    = getOptionalString (result, 1);
+                Integer r = getOptionalInteger(result, 2);
+                if (r != null) {
+                    replacedBy = r.toString();
+                    break search;                   // Prefer the first record 
providing a replacement.
+                }
             }
-        }
-        if (replacedBy == null) {
             replacedBy = '(' + 
Vocabulary.forLocale(locale).getString(Vocabulary.Keys.None).toLowerCase(locale)
 + ')';
-        } else {
-            replacedBy = replacedBy.toString();
         }
         /*
          * Try to infer the method name from the table name. For example, if 
the deprecated code was found in
          * the "Coordinate Reference System" table, then we declare 
`createCoordinateReferenceSystem(String)`
          * as the source of the log message.
          */
-        String method = "create";
-        for (final TableInfo info : TableInfo.EPSG) {
-            if (info.tableMatches(table)) {
-                method += info.type.getSimpleName();
-                break;
-            }
-        }
         if (!quiet) {
-            LogRecord record = Resources.forLocale(locale).createLogRecord(
-                    Level.WARNING,
-                    Resources.Keys.DeprecatedCode_3,
-                    Constants.EPSG + Constants.DEFAULT_SEPARATOR + code,
-                    replacedBy,
-                    reason);
-            Logging.completeAndLog(LOGGER, EPSGDataAccess.class, method, 
record);
-        }
-        return (String) replacedBy;
+            String method = "create";
+            for (final TableInfo info : TableInfo.EPSG) {
+                if (info.tableMatches(table)) {
+                    method += info.type.getSimpleName();
+                    break;
+                }
+            }
+            Logging.completeAndLog(LOGGER, EPSGDataAccess.class, method,
+                    Resources.forLocale(locale).createLogRecord(
+                            Level.WARNING,
+                            Resources.Keys.DeprecatedCode_3,
+                            Constants.EPSG + Constants.DEFAULT_SEPARATOR + 
code,
+                            replacedBy,
+                            reason));
+        }
+        return replacedBy;
     }
 
     /**
@@ -1145,8 +1187,8 @@ codes:  for (int i=0; i<codes.length; i++) {
      * @param  code        the EPSG code of the object to construct.
      * @param  name        the name for the {@link IdentifiedObject} to 
construct.
      * @param  description a description associated with the name, or {@code 
null} if none.
-     * @param  domainCode  the code for the domain of validity, or {@code 
null} if none.
-     * @param  scope       the scope, or {@code null} if none.
+     * @param  extent      extent code, or {@code null} if none. This is a 
legacy of <abbr>EPSG</abbr> version 9.
+     * @param  scope       the scope, or {@code null} if none. This is a 
legacy of <abbr>EPSG</abbr> version 9.
      * @param  remarks     remarks as a {@link String} or {@link 
InternationalString}, or {@code null} if none.
      * @param  deprecated  {@code true} if the object to create is deprecated.
      * @return the name together with a set of properties.
@@ -1156,8 +1198,8 @@ codes:  for (int i=0; i<codes.length; i++) {
                                                 final Integer      code,
                                                       String       name,       
 // May be replaced by an alias.
                                                 final CharSequence description,
-                                                final String       domainCode,
-                                                      String       scope,      
 // May replace "?" by text.
+                                                final String       extent,     
 // Legacy from EPSG version 9.
+                                                final String       scope,      
 // Legacy from EPSG version 9.
                                                 final CharSequence remarks,
                                                 final boolean      deprecated)
             throws SQLException, FactoryException
@@ -1189,10 +1231,10 @@ codes:  for (int i=0; i<codes.length; i++) {
                 final String alias  = getString(code,   result, 2);
                 NameSpace ns = null;
                 if (naming != null) {
-                    ns = namingSystems.get(naming);
+                    ns = (NameSpace) localCache.get(naming);
                     if (ns == null) {
                         ns = 
owner.nameFactory.createNameSpace(owner.nameFactory.createLocalName(null, 
naming), null);
-                        namingSystems.put(naming, ns);
+                        localCache.put(naming, ns);
                     }
                 }
                 if (CharSequences.toASCII(alias).toString().equals(name)) {
@@ -1203,54 +1245,63 @@ codes:  for (int i=0; i<codes.length; i++) {
             }
         }
         /*
-         * At this point we can fill the properties map.
+         * Creates the primary name as an object which is both a name (like 
aliases) and an identifier.
+         * The identifier type is necessary because ISO 19111 defines the 
object names as identifiers.
+         * We put all information that we have, such as version and object 
description, in that name.
          */
+        final Locale      locale     = getLocale();
+        final Citation    authority  = owner.getAuthority();
+        final String      version    = Types.toString(authority.getEdition(), 
locale);
+        final GenericName scopedName = 
owner.nameFactory.createGenericName(namespace, Constants.EPSG, name);
         properties.clear();
-        GenericName gn = null;
-        final Locale locale = getLocale();
-        final Citation authority = owner.getAuthority();
-        final InternationalString edition = authority.getEdition();
-        final String version = (edition != null) ? edition.toString() : null;
-        if (name != null) {
-            gn = owner.nameFactory.createGenericName(namespace, 
Constants.EPSG, name);
-            properties.put("name", gn);
-            properties.put(NamedIdentifier.CODE_KEY,        name);
-            properties.put(NamedIdentifier.VERSION_KEY,     version);
-            properties.put(NamedIdentifier.AUTHORITY_KEY,   authority);
-            properties.put(NamedIdentifier.DESCRIPTION_KEY, description);
-            properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale);
-            final var id = new NamedIdentifier(properties);
-            properties.clear();
-            properties.put(IdentifiedObject.NAME_KEY, id);
-        }
+        properties.put("name",                          scopedName);
+        properties.put(NamedIdentifier.CODE_KEY,        name);
+        properties.put(NamedIdentifier.VERSION_KEY,     version);
+        properties.put(NamedIdentifier.AUTHORITY_KEY,   authority);
+        properties.put(NamedIdentifier.DESCRIPTION_KEY, description);
+        properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale);
+        final var nameAsIdentifier = new NamedIdentifier(properties);
+        /*
+         * At this point, we can fill the map of object properties.
+         * We store the deprecation flag in the object identifier.
+         */
+        properties.clear();
+        properties.put(IdentifiedObject.NAME_KEY, nameAsIdentifier);
         if (!aliases.isEmpty()) {
             properties.put(IdentifiedObject.ALIAS_KEY, 
aliases.toArray(GenericName[]::new));
         }
-        if (code != null) {
-            final String codeString = code.toString();
-            final ImmutableIdentifier identifier;
-            if (deprecated) {
-                final String replacedBy = getSupersession(table, code, locale);
-                identifier = new DeprecatedCode(authority, Constants.EPSG, 
codeString, version,
-                        Character.isDigit(replacedBy.charAt(0)) ? replacedBy : 
null,
-                        
Vocabulary.formatInternational(Vocabulary.Keys.SupersededBy_1, replacedBy));
-                properties.put(AbstractIdentifiedObject.DEPRECATED_KEY, 
Boolean.TRUE);
-            } else {
-                identifier = new ImmutableIdentifier(authority, 
Constants.EPSG, codeString, version,
-                                    (gn != null) ? gn.toInternationalString() 
: null);
-            }
-            properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifier);
-        }
+        final ImmutableIdentifier identifier;
+        if (deprecated) {
+            properties.put(AbstractIdentifiedObject.DEPRECATED_KEY, 
Boolean.TRUE);
+            final String replacedBy = getSupersession(table, code, locale);
+            identifier = new DeprecatedCode(
+                    authority,
+                    Constants.EPSG,
+                    code.toString(),
+                    version,
+                    Character.isDigit(replacedBy.charAt(0)) ? replacedBy : 
null,
+                    
Vocabulary.formatInternational(Vocabulary.Keys.SupersededBy_1, replacedBy));
+        } else {
+            identifier = new ImmutableIdentifier(
+                    authority,
+                    Constants.EPSG,
+                    code.toString(),
+                    version,
+                    scopedName.toInternationalString());
+        }
+        properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifier);
         properties.put(IdentifiedObject.REMARKS_KEY, remarks);
         properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale);
         properties.put(ReferencingFactoryContainer.MT_FACTORY, 
owner.mtFactory);
-        if ("?".equals(scope)) {                // EPSG sometimes uses this 
value for unspecified scope.
-            scope = null;
+        if (translator.isUsageTableFound()) {
+            addDomainProperty(table, code);                 // Intersection 
table new since EPSG version 10.
+        }
+        if (scope != null && !scope.equals("?")) {          // Should be 
always null since EPSG version 10.
+            properties.put(ObjectDomain.SCOPE_KEY, scope);
         }
-        if (domainCode != null) {
-            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, 
owner.createExtent(domainCode));
+        if (extent != null) {                               // Should be 
always null since EPSG version 10.
+            properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, 
owner.createExtent(extent));
         }
-        properties.put(ObjectDomain.SCOPE_KEY, scope);
         return properties;
     }
 
@@ -1279,8 +1330,8 @@ codes:  for (int i=0; i<codes.length; i++) {
     {
         ArgumentChecks.ensureNonNull("code", code);
         final boolean isPrimaryKey = isPrimaryKey(code);
-        final StringBuilder query  = new StringBuilder("SELECT ");
-        final int queryStart       = query.length();
+        final var query = new StringBuilder("SELECT ");
+        final int queryStart = query.length();
         int found = -1;
         try {
             final int pk = isPrimaryKey ? toPrimaryKeys(null, null, null, 
code)[0] : 0;
@@ -1383,7 +1434,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                 "SELECT COORD_REF_SYS_CODE,"          +     // [ 1]
                       " COORD_REF_SYS_NAME,"          +     // [ 2]
                       " AREA_OF_USE_CODE,"            +     // [ 3] Deprecated 
since EPSG version 10 (always null)
-                      " CRS_SCOPE,"                   +     // [ 4]
+                      " CRS_SCOPE,"                   +     // [ 4] Deprecated 
since EPSG version 10 (always null)
                       " REMARKS,"                     +     // [ 5]
                       " DEPRECATED,"                  +     // [ 6]
                       " COORD_REF_SYS_KIND,"          +     // [ 7]
@@ -1424,7 +1475,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                     case "geographic 3d": {
                         Integer csCode = getInteger(code, result, 8);
                         if (replaceDeprecatedCS) {
-                            csCode = DEPRECATED_CS.getOrDefault(csCode, 
csCode);
+                            csCode = replaceDeprecatedCS(csCode);
                         }
                         final EllipsoidalCS cs = 
owner.createEllipsoidalCS(csCode.toString());
                         final String datumCode = getOptionalString(result, 9);
@@ -1709,7 +1760,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                       " FRAME_REFERENCE_EPOCH,"   +  // [ 6] — NULL for static 
datum, non-null if dynamic.
                       " PUBLICATION_DATE,"        +  // [ 7] — Was 
REALIZATION_EPOCH in EPSG version 9.
                       " AREA_OF_USE_CODE,"        +  // [ 8] — Deprecated 
since EPSG version 10 (always null)
-                      " DATUM_SCOPE,"             +  // [ 9]
+                      " DATUM_SCOPE,"             +  // [ 9] — Deprecated 
since EPSG version 10 (always null)
                       " REMARKS,"                 +  // [10]
                       " DEPRECATED,"              +  // [11]
                       " ELLIPSOID_CODE,"          +  // [12] — Only for 
geodetic type
@@ -1861,7 +1912,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                 " ORDER BY DATUM_SEQUENCE", code))
         {
             while (result.next()) {
-                members.add(owner.createDatum(getInteger(code, result, 
1).toString()));
+                members.add(owner.createDatum(getString(code, result, 1)));
             }
         }
         return owner.datumFactory.createDatumEnsemble(properties, members, 
PositionalAccuracyConstant.ensemble(accuracy));
@@ -1876,8 +1927,12 @@ codes:  for (int i=0; i<codes.length; i++) {
      */
     private IdentifiedObject createConventionalRS(final Integer code) throws 
SQLException, FactoryException {
         assert Thread.holdsLock(this);
-        IdentifiedObject returnValue = conventionalRS.get(code);
-        if (returnValue == null && code != null) {
+        if (code == null) {
+            return null;
+        }
+        final Long cacheKey = cacheKey(4, code);
+        var returnValue = (IdentifiedObject) localCache.get(cacheKey);
+        if (returnValue == null) {
             try (ResultSet result = executeQuery("ConventionalRS",
                     "SELECT CONVENTIONAL_RS_CODE," +
                           " CONVENTIONAL_RS_NAME," +
@@ -1897,7 +1952,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                     returnValue = ensureSingleton(new 
AbstractIdentifiedObject(properties), returnValue, code);
                 }
             }
-            conventionalRS.put(code, returnValue);
+            localCache.put(cacheKey, returnValue);
         }
         return returnValue;
     }
@@ -2210,14 +2265,14 @@ codes:  for (int i=0; i<codes.length; i++) {
     {
         ArgumentChecks.ensureNonNull("code", code);
         Extent returnValue = null;
-        try (ResultSet result = executeQuery("Area", "AREA_CODE", "AREA_NAME",
-                "SELECT AREA_OF_USE," +
-                      " AREA_SOUTH_BOUND_LAT," +
-                      " AREA_NORTH_BOUND_LAT," +
-                      " AREA_WEST_BOUND_LON," +
-                      " AREA_EAST_BOUND_LON" +
-                " FROM \"Area\"" +
-                " WHERE AREA_CODE = ?", code))
+        try (ResultSet result = executeQuery("Extent", "EXTENT_CODE", 
"EXTENT_NAME",
+                "SELECT EXTENT_DESCRIPTION," +
+                      " BBOX_SOUTH_BOUND_LAT," +
+                      " BBOX_NORTH_BOUND_LAT," +
+                      " BBOX_WEST_BOUND_LON," +
+                      " BBOX_EAST_BOUND_LON" +
+                " FROM \"Extent\"" +
+                " WHERE EXTENT_CODE = ?", code))
         {
             while (result.next()) {
                 final String description = getOptionalString(result, 1);
@@ -2511,7 +2566,8 @@ codes:  for (int i=0; i<codes.length; i++) {
      */
     private AxisName getAxisName(final Integer code) throws FactoryException, 
SQLException {
         assert Thread.holdsLock(this);
-        AxisName returnValue = axisNames.get(code);
+        final Long cacheKey = cacheKey(3, code);
+        var returnValue = (AxisName) localCache.get(cacheKey);
         if (returnValue == null) {
             try (ResultSet result = executeQuery("Coordinate Axis Name",
                     "SELECT COORD_AXIS_NAME, DESCRIPTION, REMARKS" +
@@ -2529,7 +2585,7 @@ codes:  for (int i=0; i<codes.length; i++) {
             if (returnValue == null) {
                 throw noSuchAuthorityCode(AxisName.class, 
String.valueOf(code));
             }
-            axisNames.put(code, returnValue);
+            localCache.put(cacheKey, returnValue);
         }
         return returnValue;
     }
@@ -2542,7 +2598,11 @@ codes:  for (int i=0; i<codes.length; i++) {
      */
     private RealizationMethod getRealizationMethod(final Integer code) throws 
FactoryException, SQLException {
         assert Thread.holdsLock(this);
-        RealizationMethod returnValue = realizationMethods.get(code);
+        if (code == null) {
+            return null;
+        }
+        final Long cacheKey = cacheKey(2, code);
+        var returnValue = (RealizationMethod) localCache.get(cacheKey);
         if (returnValue == null && code != null) {
             try (ResultSet result = executeQuery("DatumRealizationMethod",
                     "SELECT REALIZATION_METHOD_NAME" +
@@ -2557,7 +2617,7 @@ codes:  for (int i=0; i<codes.length; i++) {
             if (returnValue == null) {
                 throw noSuchAuthorityCode(RealizationMethod.class, 
String.valueOf(code));
             }
-            realizationMethods.put(code, returnValue);
+            localCache.put(cacheKey, returnValue);
         }
         return returnValue;
     }
@@ -2687,7 +2747,7 @@ codes:  for (int i=0; i<codes.length; i++) {
                  */
                 Class<?> type;
                 final Set<Unit<?>> units;
-                if (epsg != null && Arrays.binarySearch(EPSG_CODE_PARAMETERS, 
epsg) >= 0) {
+                if (epsg != null && EPSG_CODE_PARAMETERS.contains(epsg)) {
                     type  = Integer.class;
                     units = Set.of();
                 } else {
@@ -2998,7 +3058,7 @@ next:                   while (r.next()) {
                           " COORD_TFM_VERSION," +
                           " COORD_OP_ACCURACY," +
                           " AREA_OF_USE_CODE," +    // Deprecated since EPSG 
version 10 (always null)
-                          " COORD_OP_SCOPE," +
+                          " COORD_OP_SCOPE," +      // Deprecated since EPSG 
version 10 (always null)
                           " REMARKS," +
                           " DEPRECATED" +
                     " FROM \"Coordinate_Operation\"" +
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
index fd88b10996..b01ecc5512 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGFactory.java
@@ -457,7 +457,7 @@ public class EPSGFactory extends 
ConcurrentAuthorityFactory<EPSGDataAccess> impl
                     if (tr == null) {
                         tr = new SQLTranslator(connection.getMetaData(), 
catalog, schema);
                         try {
-                            if (!tr.isTableFound()) {
+                            if (!tr.isSchemaFound()) {
                                 install(connection);
                                 tr.setup(connection.getMetaData());         // 
Set only on success.
                             }
@@ -467,7 +467,7 @@ public class EPSGFactory extends 
ConcurrentAuthorityFactory<EPSGDataAccess> impl
                     }
                 }
             }
-            if (tr.isTableFound()) {
+            if (tr.isSchemaFound()) {
                 return newDataAccess(connection, tr);
             } else {
                 String cause;
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 4cf1d9c27e..3c9383dff0 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
@@ -234,10 +234,25 @@ public class SQLTranslator implements 
UnaryOperator<String> {
     /**
      * Whether the sentinel table "Coordinate_Operation" or a variant has been 
found.
      * If {@code false}, then {@link EPSGInstaller} needs to be run.
+     * Note that the {@linkplain #schema} may be {@code null}.
      *
-     * @see #isTableFound()
+     * @see #isSchemaFound()
      */
-    private boolean isTableFound;
+    private boolean isSchemaFound;
+
+    /**
+     * Whether the "Usage" table has been found.
+     * That table has been added in version 10 of <abbr>EPSG</abbr> database.
+     *
+     * @see #isUsageTableFound()
+     */
+    private boolean isUsageTableFound;
+
+    /**
+     * Whether the table names in the hard-coded <abbr>SQL</abbr> queries and 
in the actual database are the same.
+     * If {@code true}, then the renaming of tables can be skipped.
+     */
+    private boolean sameTableNames;
 
     /**
      * Whether the <abbr>SQL</abbr> queries hard-coded in {@link 
EPSGDataAccess} can be used verbatim
@@ -316,7 +331,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
             }
             try (ResultSet result = md.getTables(catalog, schemaPattern, 
table, null)) {
                 while (result.next()) {
-                    isTableFound = true;
+                    isSchemaFound = true;
                     catalog = result.getString(Reflection.TABLE_CAT);
                     schema  = result.getString(Reflection.TABLE_SCHEM);
                     if (result.wasNull()) schema = "";
@@ -325,7 +340,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
                     }
                 }
             }
-        } while (!isTableFound);
+        } while (!isSchemaFound);
         /*
          * At this point, we found the EPSG sentinel table and we identified 
the
          * naming convention (unquoted or mixed-case, prefixed by "epsg_" or 
not).
@@ -333,7 +348,7 @@ public class SQLTranslator implements UnaryOperator<String> 
{
         UnaryOperator<String> toNativeCase = UnaryOperator.identity();
         schemaPattern  = SQLUtilities.escape(schema, escape);
         columnRenaming = new HashMap<>();
-        tableRewording = Map.of();
+        tableRewording = new HashMap<>();
         /*
          * Special cases not covered by the generic algorithm implemented in 
`toActualTableName(…)`.
          * The entries are actually not full table names, but words separated 
by space. For example,
@@ -341,8 +356,8 @@ public class SQLTranslator implements UnaryOperator<String> 
{
          * (note that this renaming combines the two entries of the map).
          */
         if (!useMixedCaseTableNames) {
-            tableRewording = Map.of("Coordinate_Operation", "coordoperation",
-                                    "Parameter",            "param");
+            tableRewording.put("Coordinate_Operation", "coordoperation");
+            tableRewording.put("Parameter",            "param");
             if (md.storesLowerCaseIdentifiers()) {
                 toNativeCase = (table) -> table.toLowerCase(Locale.US);
             } else if (md.storesUpperCaseIdentifiers()) {
@@ -361,9 +376,13 @@ public class SQLTranslator implements 
UnaryOperator<String> {
         final var missingColumns   = new HashMap<String, String>();
         final var mayRenameColumns = new HashMap<String, String>();
         final var brokenTargetCols = new HashSet<String>();
+        boolean isExtentTableFound = false;
         tableIndex = 0;
 check:  for (;;) {
             String table;
+            boolean isUsage  = false;   // "Usage"  is a new table in EPSG 
version 19.
+            boolean isArea   = false;   // "Area"   was a table name used in 
EPSG version 9.
+            boolean isExtent = false;   // "Extent" is the new name for "Area" 
in EPSG version 10.
             switch (tableIndex++) {
                 case 0: {
                     table = "Coordinate Axis";
@@ -384,16 +403,40 @@ check:  for (;;) {
                     mayRenameColumns.put("PUBLICATION_DATE", 
"REALIZATION_EPOCH");   // EPSG version 10 → version 9.
                     break;
                 }
+                case 4:
+                    if (isExtentTableFound) continue;
+                    isArea = true;
+                    // Fallthrough for testing "Area" if and only if "Extent" 
has not been found.
                 case 3: {
-                    table = "Alias";
+                    isExtent = !isArea;
+                    table = isExtent ? "Extent" : "Area";                      
      // "Area" in 9, "Extent" in 10.
+                    mayRenameColumns.put("EXTENT_CODE",          "AREA_CODE"); 
      // EPSG version 10 → version 9.
+                    mayRenameColumns.put("EXTENT_NAME",          "AREA_NAME");
+                    mayRenameColumns.put("EXTENT_DESCRIPTION",   
"AREA_OF_USE");
+                    mayRenameColumns.put("BBOX_SOUTH_BOUND_LAT", 
"AREA_SOUTH_BOUND_LAT");
+                    mayRenameColumns.put("BBOX_NORTH_BOUND_LAT", 
"AREA_NORTH_BOUND_LAT");
+                    mayRenameColumns.put("BBOX_WEST_BOUND_LON",  
"AREA_WEST_BOUND_LON");
+                    mayRenameColumns.put("BBOX_EAST_BOUND_LON",  
"AREA_EAST_BOUND_LON");
+                    break;
+                }
+                case 5: {
+                    isUsage = true;
+                    table = "Usage";
+                    break;
+                }
+                case 6: {
+                    if (isUsageTableFound) continue;    // The check of 
`ENUMERATION_COLUMN` is already done.
+                    table = "Alias";                    // For checking the 
type of the `ENUMERATION_COLUMN`.
                     break;
                 }
                 default: break check;
             }
+            boolean isTableFound = false;
             brokenTargetCols.addAll(mayRenameColumns.values());
             table = toNativeCase.apply(toActualTableName(table));
             try (ResultSet result = md.getColumns(catalog, schemaPattern, 
SQLUtilities.escape(table, escape), "%")) {
                 while (result.next()) {
+                    isTableFound = true;          // Assuming that all tables 
contain at least one column.
                     final String column = 
result.getString(Reflection.COLUMN_NAME).toUpperCase(Locale.US);
                     missingColumns.remove(column);
                     if (mayRenameColumns.remove(column) == null) {  // Do not 
rename if the new column exists.
@@ -419,21 +462,26 @@ check:  for (;;) {
                     }
                 }
             }
-            missingColumns.forEach((column, type) -> {
-                columnRenaming.put(column, "CAST(NULL AS " + type + ") AS " + 
column);
-            });
-            mayRenameColumns.values().removeAll(brokenTargetCols);  // For 
renaming only when the old name has been found.
-            columnRenaming.putAll(mayRenameColumns);
-            mayRenameColumns.clear();
-            brokenTargetCols.clear();
-            missingColumns.clear();
+            if (isTableFound) {
+                isUsageTableFound  |= isUsage;
+                isExtentTableFound |= isExtent;
+                missingColumns.forEach((column, type) -> {
+                    columnRenaming.put(column, "CAST(NULL AS " + type + ") AS 
" + column);
+                });
+                mayRenameColumns.values().removeAll(brokenTargetCols);  // For 
renaming only when the old name has been found.
+                columnRenaming.putAll(mayRenameColumns);
+                mayRenameColumns.clear();
+                brokenTargetCols.clear();
+                missingColumns.clear();
+                if (isArea) {
+                    tableRewording.put("Extent", "Area");
+                }
+            }
         }
+        tableRewording = Map.copyOf(tableRewording);
         columnRenaming = Map.copyOf(columnRenaming);
-        sameQueries = (tableNameEnum == null)
-                && tableRewording.isEmpty()
-                && columnRenaming.isEmpty()
-                && "\"".equals(identifierQuote)
-                && !useBoolean;
+        sameTableNames = useMixedCaseTableNames && 
"\"".equals(identifierQuote) && tableRewording.isEmpty();
+        sameQueries    = sameTableNames && (tableNameEnum == null) && 
columnRenaming.isEmpty() && !useBoolean;
     }
 
     /**
@@ -465,9 +513,18 @@ check:  for (;;) {
     /**
      * Returns whether the <abbr>EPSG</abbr> tables have been found.
      * If {@code false}, then {@link EPSGInstaller} needs to be run.
+     * Note that the {@linkplain #getSchema() schema} may be {@code null}.
+     */
+    final boolean isSchemaFound() {
+        return isSchemaFound;
+    }
+
+    /**
+     * Returns whether the "Usage" table has been found.
+     * That table has been added in version 10 of <abbr>EPSG</abbr> database.
      */
-    final boolean isTableFound() {
-        return isTableFound;
+    final boolean isUsageTableFound() {
+        return isUsageTableFound;
     }
 
     /**
@@ -520,7 +577,9 @@ check:  for (;;) {
      */
     private void toActualTableName(final String name, final StringBuilder 
buffer) {
         if (useMixedCaseTableNames) {
-            
buffer.append(identifierQuote).append(name).append(identifierQuote);
+            buffer.append(identifierQuote)
+                  .append(tableRewording.getOrDefault(name, name))
+                  .append(identifierQuote);
         } else {
             if (usePrefixedTableNames) {
                 buffer.append(TABLE_PREFIX);
@@ -548,7 +607,7 @@ check:  for (;;) {
         }
         final var buffer = new StringBuilder(sql.length() + 16);
         int end = 0;
-        if (!(useMixedCaseTableNames && "\"".equals(identifierQuote))) {
+        if (!sameTableNames) {
             int start;
             while ((start = sql.indexOf('"', end)) >= 0) {
                 /*
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 2eec4f4bc1..14ff6e8944 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
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.factory.sql;
 
-import java.util.Map;
 import java.util.Set;
 import java.util.List;
 import java.util.Locale;
@@ -426,10 +425,15 @@ public final class EPSGFactoryTest extends 
TestCaseWithLogs {
     @Test
     public void testDeprecatedCoordinateSystems() throws FactoryException {
         final EPSGFactory factory = dataEPSG.factory();
-        for (final Map.Entry<Integer,Integer> entry : 
EPSGDataAccess.deprecatedCS().entrySet()) {
-            final CoordinateSystem expected = 
factory.createEllipsoidalCS(entry.getValue().toString());
+        for (int deprecatedCode = 6401; deprecatedCode <= 6420; 
deprecatedCode++) {
+            final int replacementCode = 
EPSGDataAccess.replaceDeprecatedCS(deprecatedCode);
+            if (replacementCode == deprecatedCode) {
+                assertTrue(deprecatedCode >= 6403 && deprecatedCode <= 6404);
+                continue;   // Non-deprecated code.
+            }
+            final CoordinateSystem expected = 
factory.createEllipsoidalCS(Integer.toString(replacementCode));
             loggings.assertNoUnexpectedLog();
-            final String code = entry.getKey().toString();
+            final String code = Integer.toString(deprecatedCode);
             final CoordinateSystem deprecated;
             try {
                 deprecated = factory.createEllipsoidalCS(code);

Reply via email to