This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 4e2e756dc100c682eace3fdb75f0098655e6d439 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sun Aug 24 16:03:43 2025 +0200 In `IdentifiedObjectFinder`, retrofit the `createFromIdentifiers` and `createFromNames` methods into `getCodeCandidates`. This is needed because the previous algorithm assumed that CRS names were unique, which is not true in EPSG version 12. Also need to make the search for EPSG codes tolerant to the difference between datum and datum ensemble. --- .../sis/metadata/sql/privy/SQLUtilities.java | 61 ++-- .../org/apache/sis/metadata/sql/privy/Syntax.java | 2 +- .../sis/metadata/sql/privy/SQLUtilitiesTest.java | 4 +- .../sis/openoffice/ReferencingFunctions.java | 2 +- .../apache/sis/referencing/AuthorityFactories.java | 2 +- .../apache/sis/referencing/IdentifiedObjects.java | 6 +- .../factory/ConcurrentAuthorityFactory.java | 8 +- .../factory/IdentifiedObjectFinder.java | 131 +++----- .../referencing/factory/sql/AuthorityCodes.java | 154 ++++++--- .../factory/sql/CloseableReference.java | 6 + .../referencing/factory/sql/EPSGCodeFinder.java | 347 ++++++++++++++++---- .../referencing/factory/sql/EPSGDataAccess.java | 355 ++++++++++----------- .../referencing/factory/sql/ObjectPertinence.java | 11 +- .../sis/referencing/factory/sql/SQLTranslator.java | 19 +- .../sis/referencing/factory/sql/TableInfo.java | 28 +- .../operation/CoordinateOperationRegistry.java | 5 +- .../sis/referencing/AuthorityFactoriesTest.java | 2 +- .../referencing/factory/sql/EPSGFactoryTest.java | 19 +- .../sis/storage/sql/feature/InfoStatements.java | 2 +- .../main/org/apache/sis/util/logging/Logging.java | 2 +- .../sis/referencing/factory/sql/epsg/Prepare.sql | 4 +- 21 files changed, 702 insertions(+), 468 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java index c4b9881d55..38e6e1f423 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLUtilities.java @@ -137,58 +137,69 @@ public final class SQLUtilities extends Static { } /** - * Returns a string like the given string but with accented letters replaced by <abbr>ASCII</abbr> - * letters and all characters that are not letter or digit replaced by the wildcard % character. + * Returns a string like the given string but with accented letters replaced by any character ({@code '_'}) + * and all characters that are not letter or digit replaced by the wildcard ({@code '%'}). * - * @param text the text to get as a SQL LIKE pattern. - * @param toLower whether to convert characters to lower case. - * @return the "LIKE" pattern for the given text. + * @param text the text to get as a SQL LIKE pattern. + * @param toLowerCase whether to convert characters to lower case. + * @param escape value of {@link DatabaseMetaData#getSearchStringEscape()}. May be null or empty. + * @return the {@code LIKE} pattern for the given text. */ - public static String toLikePattern(final String text, final boolean toLower) { + public static String toLikePattern(final String text, final boolean toLowerCase, final String escape) { final var buffer = new StringBuilder(text.length()); - toLikePattern(text, 0, text.length(), false, toLower, buffer); + toLikePattern(text, 0, text.length(), false, toLowerCase, escape, buffer); return buffer.toString(); } /** * Returns a <abbr>SQL</abbr> LIKE pattern for the given text. The text is optionally returned in all lower cases * for allowing case-insensitive searches. Punctuations are replaced by any sequence of characters ({@code '%'}) - * and non-ASCII letters or digits are replaced by any single character ({@code '_'}). This method avoid to put - * a {@code '%'} symbol as the first character since it prevents some databases to use their index. + * and non-<abbr>ASCII</abbr> Latin letters are replaced by any single character ({@code '_'}). + * Ideograms (Japanese, Chinese, …) and hiragana (Japanese) are kept unchanged. + * This method avoids to put a {@code '%'} symbol as the first character + * because such character prevents some databases to use their index. * - * @param text the text to get as a SQL LIKE pattern. - * @param i index of the first character to use in the given {@code identifier}. - * @param end index after the last character to use in the given {@code identifier}. + * @param text the text to get as a <abbr>SQL</abbr> {@code LIKE} pattern. + * @param textStart index of the first character to use in the given {@code text}. + * @param textEnd index after the last character to use in the given {@code text}. * @param allowSuffix whether to append a final {@code '%'} wildcard at the end of the pattern. - * @param toLower whether to convert characters to lower case. - * @param buffer buffer where to append the SQL LIKE pattern. + * @param toLowerCase whether to convert characters to lower case. + * @param escape value of {@link DatabaseMetaData#getSearchStringEscape()}. May be null or empty. + * @param buffer buffer where to append the <abbr>SQL</abbr> {@code LIKE} pattern. */ - public static void toLikePattern(final String text, int i, final int end, - final boolean allowSuffix, final boolean toLower, final StringBuilder buffer) + public static void toLikePattern(final String text, int textStart, final int textEnd, final boolean allowSuffix, + final boolean toLowerCase, final String escape, final StringBuilder buffer) { - final int bs = buffer.length(); - while (i < end) { - final int c = text.codePointAt(i); + final int bufferStart = buffer.length(); + while (textStart < textEnd) { + final int c = text.codePointAt(textStart); if (Character.isLetterOrDigit(c)) { - if (c < 128) { // Use only ASCII characters in the search. - buffer.appendCodePoint(toLower ? Character.toLowerCase(c) : c); + // Ignore accented letters and Greek letters (before `U+0400`) in the search. + if (c < 0x80 || c >= 0x400) { + buffer.appendCodePoint(toLowerCase ? Character.toLowerCase(c) : c); } else { appendIfNotRedundant(buffer, '_'); } } else { final int length = buffer.length(); - if (length == bs) { - buffer.appendCodePoint(c != '%' ? c : '_'); + if (length == bufferStart) { + // Do not use wildcard in the first character. + if (escape != null && (c == '%' || c == '_' || text.startsWith(escape, textStart))) { + // Note: there will be bug if `escape` is a repetition of the same character. + // But we assume that this corner case is too rare for being worth a check. + buffer.append(escape); + } + buffer.appendCodePoint(c); } else if (buffer.charAt(length - 1) != '%') { buffer.append('%'); } } - i += Character.charCount(c); + textStart += Character.charCount(c); } if (allowSuffix) { appendIfNotRedundant(buffer, '%'); } - for (i=bs; (i = buffer.indexOf("_%", i)) >= 0;) { + for (int i=bufferStart; (i = buffer.indexOf("_%", i)) >= 0;) { buffer.deleteCharAt(i); } } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java index ae9281ee90..218e6b0d03 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java @@ -74,7 +74,7 @@ public class Syntax { * @see #escapeWildcards(String) * @see SQLBuilder#appendWildcardEscaped(String) */ - final String wildcardEscape; + public final String wildcardEscape; /** * The default catalog of the connection, or {@code null} if none. diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java index 538b5d2144..57ddfc8a7e 100644 --- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java @@ -55,7 +55,7 @@ public final class SQLUtilitiesTest extends TestCase { assertEquals("WGS%84", toLikePattern(buffer, "WGS 84")); assertEquals("A%text%with%random%symbols%", toLikePattern(buffer, "A text !* with_random:/symbols;+")); assertEquals("*%With%non%letter%start", toLikePattern(buffer, "*_+%=With non-letter start")); - assertEquals("_Special%case", toLikePattern(buffer, "%Special_case")); + assertEquals("\\%Special%case", toLikePattern(buffer, "%Special_case")); } /** @@ -63,7 +63,7 @@ public final class SQLUtilitiesTest extends TestCase { */ private static String toLikePattern(final StringBuilder buffer, final String identifier) { buffer.setLength(0); - SQLUtilities.toLikePattern(identifier, 0, identifier.length(), false, false, buffer); + SQLUtilities.toLikePattern(identifier, 0, identifier.length(), false, false, "\\", buffer); return buffer.toString(); } } diff --git a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java index 2fbfc7b4d3..6384701b86 100644 --- a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java +++ b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/ReferencingFunctions.java @@ -183,7 +183,7 @@ public class ReferencingFunctions extends CalcAddins implements XReferencing { return object.getName().getCode(); } // In Apache SIS implementation, `getDescriptionText(…)` returns the identified object name. - name = CRS.getAuthorityFactory(null).getDescriptionText(IdentifiedObject.class, codeOrPath).orElse(null); + name = CRS.getAuthorityFactory(null).getDescriptionText(CoordinateReferenceSystem.class, codeOrPath).orElse(null); } catch (Exception exception) { return getLocalizedMessage(exception); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java index 711f3dde50..c20937e0a6 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AuthorityFactories.java @@ -290,7 +290,7 @@ final class AuthorityFactories<T extends AuthorityFactory> extends LazySet<T> { } /** Returns a set of authority codes, using the fallback if necessary. */ - @Override protected Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { + @Override protected Iterable<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { for (;;) try { // Executed at most twice. return super.getCodeCandidates(object); } catch (UnavailableFactoryException e) { 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 da6e9567a4..6ea0d83d3d 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 @@ -686,8 +686,6 @@ public final class IdentifiedObjects extends Static { * <h4>Example 2: extend the search to deprecated definitions</h4> * By default, {@code lookup(…)} methods exclude deprecated objects from the search. * To search also among deprecated objects, one can use the following Java code: - * This example does not use the {@code findSingleton(…)} convenience method on the assumption - * that the search may find both deprecated and non-deprecated objects. * * {@snippet lang="java" : * IdentifiedObjectFinder finder = IdentifiedObjects.newFinder(null); @@ -706,9 +704,7 @@ public final class IdentifiedObjects extends Static { * @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#newIdentifiedObjectFinder() * @see IdentifiedObjectFinder#find(IdentifiedObject) */ - public static IdentifiedObjectFinder newFinder(final String authority) - throws NoSuchAuthorityFactoryException, FactoryException - { + public static IdentifiedObjectFinder newFinder(final String authority) throws NoSuchAuthorityFactoryException, FactoryException { final GeodeticAuthorityFactory factory; if (authority == null) { factory = AuthorityFactories.ALL; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java index 5f8fd800ea..af6b17c7c0 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java @@ -48,6 +48,7 @@ import org.opengis.parameter.ParameterDescriptor; import org.apache.sis.util.Classes; import org.apache.sis.util.Debug; import org.apache.sis.util.Printable; +import org.apache.sis.util.Exceptions; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.resources.Messages; @@ -626,11 +627,11 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa /* * We must close the factories from outside the synchronized block. */ - confirmClose(factories); try { + confirmClose(factories); close(factories); } catch (Exception exception) { - unexpectedException("closeExpired", exception); + unexpectedException("closeExpired", Exceptions.unwrap(exception)); } /* * If the queue of Data Access Objects (DAO) become empty, this means that this `ConcurrentAuthorityFactory` @@ -1901,7 +1902,7 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa * object than the specified one. This method delegates to the data access object. */ @Override - protected synchronized Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { + protected synchronized Iterable<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { try { acquire(); return finder.getCodeCandidates(object); @@ -2176,6 +2177,7 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa confirmClose(factories); close(factories); // Must be invoked outside the synchronized block. } catch (Exception e) { + e = Exceptions.unwrap(e); if (e instanceof FactoryException) { throw (FactoryException) e; } else { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java index 2af9d2d64d..a5dfbcd4a5 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java @@ -21,20 +21,24 @@ import java.util.LinkedHashSet; import java.util.Objects; import java.util.logging.Level; import java.util.logging.LogRecord; -import org.opengis.util.GenericName; import org.opengis.util.FactoryException; import org.opengis.metadata.Identifier; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.AuthorityFactory; import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.opengis.referencing.datum.Datum; import org.apache.sis.referencing.AbstractIdentifiedObject; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.referencing.datum.DatumOrEnsemble; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.Utilities; import org.apache.sis.util.privy.Constants; -import org.apache.sis.system.Semaphores; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.logging.Logging; +import org.apache.sis.system.Semaphores; + +// Specific to the geoapi-3.1 and geoapi-4.0 branches: +import org.opengis.referencing.datum.DatumEnsemble; /** @@ -57,7 +61,7 @@ import org.apache.sis.util.logging.Logging; * is thread-safe. If concurrent searches are desired, then a new instance should be created for each thread. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.4 + * @version 1.5 * * @see GeodeticAuthorityFactory#newIdentifiedObjectFinder() * @see IdentifiedObjects#newFinder(String) @@ -261,7 +265,19 @@ public class IdentifiedObjectFinder { * more reliable than user-specified objects. */ private boolean match(final IdentifiedObject candidate, final IdentifiedObject object) { - return Utilities.deepEquals(candidate, object, ignoreAxes ? ComparisonMode.ALLOW_VARIANT : COMPARISON_MODE); + final ComparisonMode mode = ignoreAxes ? ComparisonMode.ALLOW_VARIANT : COMPARISON_MODE; + if (Utilities.deepEquals(candidate, object, mode)) { + return true; + } + if (Datum.class.isAssignableFrom(proxy.type)) { + if (candidate instanceof Datum && object instanceof DatumEnsemble<?>) { + return DatumOrEnsemble.isLegacyDatum((DatumEnsemble<?>) object, (Datum) candidate, mode); + } + if (candidate instanceof DatumEnsemble<?> && object instanceof Datum) { + return DatumOrEnsemble.isLegacyDatum((DatumEnsemble<?>) candidate, (Datum) object, mode); + } + } + return false; } /** @@ -288,20 +304,8 @@ public class IdentifiedObjectFinder { /** * Lookups objects which are approximately equal to the specified object. - * The default implementation tries to instantiate some {@linkplain AbstractIdentifiedObject identified objects} - * from the authority factory specified at construction time, in the following order: - * - * <ul> - * <li>If the specified object contains {@linkplain AbstractIdentifiedObject#getIdentifiers() identifiers} - * associated to the same authority as the factory, then those identifiers are used for creating the - * objects to be compared by calls to a {@code create<Type>(String)} method.</li> - * <li>If the authority factory can create objects from their {@linkplain AbstractIdentifiedObject#getName() name} - * in addition of identifiers, then the name and {@linkplain AbstractIdentifiedObject#getAlias() aliases} are - * used for creating objects to be tested.</li> - * <li>If a full scan of the dataset is allowed, then full {@linkplain #getCodeCandidates set of candidate codes} - * is used for creating objects to be tested.</li> - * </ul> - * + * This method tries to instantiate objects identified by the {@linkplain #getCodeCandidates set of candidate codes} + * with the authority factory specified at construction time. * The created objects which are equal to the specified object in the * the sense of {@link ComparisonMode#APPROXIMATE} are returned. * @@ -322,14 +326,6 @@ public class IdentifiedObjectFinder { * trust the identifiers in the user object. */ IdentifiedObject candidate = createFromIdentifiers(object); - if (candidate == null) { - /* - * We are unable to find the object from its identifiers. Try a quick name lookup. - * Some implementations like the one backed by the EPSG database are capable to find - * an object from its name. - */ - candidate = createFromNames(object); - } if (candidate != null) { result = Set.of(candidate); } @@ -408,7 +404,6 @@ public class IdentifiedObjectFinder { * @throws FactoryException if an error occurred while creating an object. * * @see #createFromCodes(IdentifiedObject) - * @see #createFromNames(IdentifiedObject) */ private IdentifiedObject createFromIdentifiers(final IdentifiedObject object) throws FactoryException { for (final Identifier id : object.getIdentifiers()) { @@ -436,56 +431,6 @@ public class IdentifiedObjectFinder { return null; } - /** - * Creates an object equals (optionally ignoring metadata), to the specified object using only the - * {@linkplain AbstractIdentifiedObject#getName name} and {@linkplain AbstractIdentifiedObject#getAlias aliases}. - * If no such object is found, returns {@code null}. - * - * <p>This method may be used with some {@linkplain GeodeticAuthorityFactory authority factory} - * implementations like the one backed by the EPSG database, which are capable to find an object - * from its name when the identifier is unknown.</p> - * - * @param object the object looked up. - * @return the identified object, or {@code null} if not found. - * @throws FactoryException if an error occurred while creating an object. - * - * @see #createFromCodes(IdentifiedObject) - * @see #createFromIdentifiers(IdentifiedObject) - */ - private IdentifiedObject createFromNames(final IdentifiedObject object) throws FactoryException { - String code = object.getName().getCode(); - IdentifiedObject candidate; - try { - candidate = create(code); - } catch (FactoryException e) { - /* - * The identifier was not recognized. We will continue later with aliases. - * Note: we catch a more generic exception than NoSuchAuthorityCodeException because - * this attempt may fail for various reasons (character string not supported - * by the underlying database for primary key, duplicated name found, etc.). - */ - exceptionOccurred(e); - candidate = null; - } - if (match(candidate, object)) { - return candidate; - } - for (final GenericName id : object.getAlias()) { - code = id.toString(); - try { - candidate = create(code); - } catch (FactoryException e) { - // The name was not recognized. No problem, let's go on. - exceptionOccurred(e); - continue; - } - if (match(candidate, object)) { - return candidate; - } - } - return null; - } - /** * Creates an object equals (optionally ignoring metadata), to the specified object. * This method scans the {@linkplain #getCodeCandidates(IdentifiedObject) authority codes}, @@ -495,20 +440,14 @@ public class IdentifiedObjectFinder { * <p>This method may be used in order to get a fully {@linkplain AbstractIdentifiedObject identified object} * from an object without {@linkplain AbstractIdentifiedObject#getIdentifiers() identifiers}.</p> * - * <p>Scanning the whole set of authority codes may be slow. Users should try - * <code>{@linkplain #createFromIdentifiers createFromIdentifiers}(object)</code> and/or - * <code>{@linkplain #createFromNames createFromNames}(object)</code> before to fallback - * on this method.</p> - * * @param object the object looked up. * @return the identified object, or {@code null} if not found. * @throws FactoryException if an error occurred while scanning through authority codes. * * @see #createFromIdentifiers(IdentifiedObject) - * @see #createFromNames(IdentifiedObject) */ Set<IdentifiedObject> createFromCodes(final IdentifiedObject object) throws FactoryException { - final Set<IdentifiedObject> result = new LinkedHashSet<>(); // We need to preserve order. + final var result = new LinkedHashSet<IdentifiedObject>(); // We need to preserve order. final boolean finer = Semaphores.queryAndSet(Semaphores.FINER_OBJECT_CREATION_LOGS); try { for (final String code : getCodeCandidates(object)) { @@ -557,15 +496,21 @@ public class IdentifiedObjectFinder { /** * Returns a set of authority codes that <em>may</em> identify the same object as the specified one. - * The returned set must contains <em>at least</em> the code of every objects that are - * {@link ComparisonMode#APPROXIMATE approximately equal} to the specified one. - * However, the set may conservatively contains the code for more objects if an exact search is too expensive. + * The elements may be determined from object identifiers, from object names, or from a more extensive search in the database. + * The effort in populating the returned set is specified by the {@linkplain #getSearchDomain() search domain}. + * The returned set should contain at least the codes of every objects in the search domain + * that are {@linkplain ComparisonMode#APPROXIMATE approximately equal} to the specified object. + * However, the set may conservatively contain the codes for more objects if an exact search is too expensive. * * <p>This method is invoked by the default {@link #find(IdentifiedObject)} method implementation. - * The caller iterates through the returned codes, instantiate the objects and compare them with - * the specified one in order to determine which codes are really applicable. - * The iteration stops as soon as a match is found (in other words, if more than one object is equal - * to the specified one, then the {@code find(…)} method selects the first one in iteration order).</p> + * The caller iterates through the returned codes, instantiates the objects and compares them with + * the specified object in order to determine which codes are really matching. + * The iteration order should be the preference order.</p> + * + * <h4>Exceptions during iteration</h4> + * An unchecked {@link BackingStoreException} may be thrown during the iteration if the implementation + * fetches the codes lazily (when first needed) from the authority factory, and that action failed. + * The exception cause is often the checked {@link FactoryException}. * * <h4>Default implementation</h4> * The default implementation returns the same set as @@ -577,7 +522,7 @@ public class IdentifiedObjectFinder { * @return a set of code candidates. * @throws FactoryException if an error occurred while fetching the set of code candidates. */ - protected Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { + protected Iterable<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { return factory.getAuthorityCodes(proxy.type.asSubclass(IdentifiedObject.class)); } @@ -691,7 +636,7 @@ public class IdentifiedObjectFinder { * The default method implementation delegates the work to the finder specified by {@link #delegate()}. */ @Override - protected Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { + protected Iterable<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { @SuppressWarnings("LocalVariableHidesMemberVariable") final IdentifiedObjectFinder delegate = delegate(); return (delegate != this) ? delegate.getCodeCandidates(object) : super.getCodeCandidates(object); 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 ab1f20c895..2b26f07d70 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 @@ -16,14 +16,15 @@ */ package org.apache.sis.referencing.factory.sql; +import java.util.Collection; import java.util.LinkedHashMap; import java.io.Serializable; import java.io.ObjectStreamException; import java.sql.ResultSet; -import java.sql.Connection; import java.sql.SQLException; import java.sql.PreparedStatement; import java.sql.Statement; +import org.apache.sis.metadata.sql.privy.SQLUtilities; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.collection.IntegerList; import org.apache.sis.util.privy.AbstractMap; @@ -32,7 +33,7 @@ import org.apache.sis.util.privy.Strings; /** * A map of <abbr>EPSG</abbr> authority codes as keys and object names as values. - * This map requires a living connection to the EPSG database. + * This map requires a valid connection to the <abbr>EPSG</abbr> database. * * <h2>Serialization</h2> * Serialization of this class stores a copy of all authority codes. @@ -63,7 +64,7 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali /** * Index in the {@link #sql} and {@link #statements} arrays. */ - private static final int ALL = 0, ONE = 1; + private static final int ALL_CODES = 0, NAME_FOR_CODE = 1, CODES_FOR_NAME = 2; /** * The factory which is the owner of this map. One purpose of this field is to prevent @@ -83,12 +84,14 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali * In this array: * * <ul> - * <li>{@code sql[ALL]} is a statement for querying all codes.</li> - * <li>{@code sql[ONE]} is a statement for querying a single code. - * This statement is similar to {@code sql[ALL]} with the addition of a {@code WHERE} clause.</li> + * <li>{@code sql[ALL_CODES]} is a statement for querying all codes.</li> + * <li>{@code sql[NAME_FOR_CODE]} is a statement for querying the name associated to a single code.</li> + * <li>{@code sql[CODES_FOR_NAME]} is a statement for querying the code(s) for an object of a given name.</li> * </ul> + * + * The array length may be only 1 instead of 3 if there is no <abbr>SQL</abbr> statement for fetching the name. */ - private final transient String[] sql = new String[2]; + private final transient String[] sql; /** * The JDBC statements for the SQL commands in the {@link #sql} array, created when first needed. @@ -96,15 +99,15 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali * This array will also be stored in {@link CloseableReference} for closing the statements * when the garbage collector detected that {@code AuthorityCodes} is no longer in use. */ - private final transient Statement[] statements = new Statement[2]; + private final transient Statement[] statements; /** - * The result of {@code statements[ALL]}, created only if requested. + * The result of {@code statements[ALL_CODES]}, created only if requested. * The codes will be queried at most once and cached in the {@link #codes} list. * * <p>Note that if this result set is not closed explicitly, it will be closed implicitly when - * {@code statements[ALL]} will be closed. This is because JDBC specification said that closing - * a statement also close its result set.</p> + * {@code statements[ALL_CODES]} will be closed. This is because <abbr>JDBC</abbr> specification + * said that closing a statement also close its result set.</p> */ private transient ResultSet results; @@ -116,49 +119,60 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali /** * Creates a new map of authority codes for the specified type. * - * @param connection the connection to the EPSG database. - * @param table the table to query. - * @param type the type to query. - * @param factory the factory originator. + * @param table the table to query. + * @param type the type to query. + * @param factory the factory originator. */ - AuthorityCodes(final Connection connection, final TableInfo table, final Class<?> type, final EPSGDataAccess factory) - throws SQLException - { + AuthorityCodes(final TableInfo table, final Class<?> type, final EPSGDataAccess factory) throws SQLException { this.factory = factory; + final int count = (table.nameColumn != null) ? 3 : 1; + sql = new String[count]; + statements = new Statement[count]; /* * Build the SQL query for fetching the codes of all object. It is of the form: * - * SELECT code FROM table ORDER BY code; + * SELECT code FROM table WHERE DEPRECATED=FALSE ORDER BY code; */ final var buffer = new StringBuilder(100); final int columnNameStart = buffer.append("SELECT ").length(); final int columnNameEnd = buffer.append(table.codeColumn).length(); - buffer.append(" FROM ").append(table.table); - final Class<?> tableType = table.where(factory, type, buffer); + buffer.append(" FROM ").append(table.fromClause); + this.type = table.where(factory, type, buffer); final int conditionStart = buffer.length(); if (table.showColumn != null) { buffer.append(table.showColumn).append("=TRUE AND "); - // Do not put spaces around "<>" - SQLTranslator searches for this exact match. } // Do not put spaces around "=" - SQLTranslator searches for this exact match. - buffer.append("DEPRECATED=FALSE ORDER BY ").append(table.codeColumn); - sql[ALL] = factory.translator.apply(buffer.toString()); + sql[ALL_CODES] = buffer.append("DEPRECATED=FALSE ORDER BY ").append(table.codeColumn).toString(); + /* + * Build the SQL query for fetching the codes of object having a name matching a pattern. + * It is of the form: + * + * SELECT code FROM table WHERE name LIKE ? AND DEPRECATED=FALSE ORDER BY code; + */ + if (count > CODES_FOR_NAME) { + sql[CODES_FOR_NAME] = buffer.insert(conditionStart, table.nameColumn + " LIKE ? AND ").toString(); + /* + * Workaround for Derby bug. See `SQLUtilities.filterFalsePositive(…)`. + */ + String t = sql[CODES_FOR_NAME]; + t = t.substring(0, columnNameEnd) + ", " + table.nameColumn + t.substring(columnNameEnd); + sql[CODES_FOR_NAME] = t; + } /* * Build the SQL query for fetching the name of a single object for a given code. * This query will also be used for testing object existence. It is of the form: * * SELECT name FROM table WHERE code = ? */ - buffer.setLength(conditionStart); - if (table.nameColumn != null) { + if (count > NAME_FOR_CODE) { + buffer.setLength(conditionStart); buffer.replace(columnNameStart, columnNameEnd, table.nameColumn); + sql[NAME_FOR_CODE] = buffer.append(table.codeColumn).append(" = ?").toString(); + } + for (int i=0; i<count; i++) { + sql[i] = factory.translator.apply(sql[i]); } - buffer.append(table.codeColumn).append(" = ?"); - sql[ONE] = factory.translator.apply(buffer.toString()); - /* - * Other information opportunistically computed from above search. - */ - this.type = tableType; } /** @@ -170,6 +184,64 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali return new CloseableReference(this, factory, statements); } + /** + * Returns the prepared statement at the given index, creating it when first needed. + * This method must be invoked in a block synchronized on {@link #factory}. + */ + private PreparedStatement prepareStatement(final int index) throws SQLException { + var statement = (PreparedStatement) statements[index]; + if (statement == null) { + statements[index] = statement = factory.connection.prepareStatement(sql[index]); + sql[index] = null; // Not needed anymore. + } + return statement; + } + + /** + * Puts codes associated to the given name in the given collection. + * + * @param pattern the {@code LIKE} pattern of the name to search. + * @param name the original name (workaround for Derby bug). + * @param addTo the collection where to add the codes. + * @throws SQLException if an error occurred while querying the database. + */ + final void findCodesFromName(final String pattern, final String name, final Collection<Integer> addTo) throws SQLException { + if (statements.length > CODES_FOR_NAME) { + synchronized (factory) { + final PreparedStatement statement = prepareStatement(CODES_FOR_NAME); + statement.setString(1, pattern); + try (ResultSet result = statement.executeQuery()) { + while (result.next()) { + final int code = result.getInt(1); + if (!result.wasNull() && SQLUtilities.filterFalsePositive(name, result.getString(2))) { + addTo.add(code); + } + } + } + } + } + } + + /** + * Puts all codes in the given collection. This method is used only as a fallback when {@link EPSGCodeFinder} + * cannot get a list of authority codes in a more selective way, with some conditions on property values. + * This method should not be invoked for the most common objects such as <abbr>CRS</abbr> and datum. + * + * @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 getAllCodes(final Collection<Integer> addTo) throws SQLException { + boolean changed = false; + synchronized (factory) { + int code; + for (int index=0; (code = getCodeAt(index)) >= 0; index++) { + changed |= addTo.add(code); + } + } + return changed; + } + /** * Returns the code at the given index, or -1 if the index is out of bounds. * @@ -182,8 +254,8 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali synchronized (factory) { if (codes == null) { codes = new IntegerList(100, MAX_CODE); - results = (statements[ALL] = factory.connection.createStatement()).executeQuery(sql[ALL]); - sql[ALL] = null; // Not needed anymore. + results = (statements[ALL_CODES] = factory.connection.createStatement()).executeQuery(sql[ALL_CODES]); + sql[ALL_CODES] = null; // Not needed anymore. } int more = index - codes.size(); // Positive as long as we need more data. if (more < 0) { @@ -196,8 +268,8 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali if (!r.next()) { results = null; r.close(); - statements[ALL].close(); - statements[ALL] = null; + statements[ALL_CODES].close(); + statements[ALL_CODES] = null; return -1; } code = r.getInt(1); @@ -245,7 +317,7 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali */ @Override public String get(final Object code) { - if (code != null) { + if (code != null && statements.length > NAME_FOR_CODE) { final int n; if (code instanceof Number) { n = ((Number) code).intValue(); @@ -256,11 +328,7 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali } try { synchronized (factory) { - var statement = (PreparedStatement) statements[ONE]; - if (statement == null) { - statements[ONE] = statement = factory.connection.prepareStatement(sql[ONE]); - sql[ONE] = null; // Not needed anymore. - } + final PreparedStatement statement = prepareStatement(NAME_FOR_CODE); statement.setInt(1, n); try (ResultSet r = statement.executeQuery()) { while (r.next()) { @@ -335,7 +403,7 @@ final class AuthorityCodes extends AbstractMap<String,String> implements Seriali /** * Invoked when a SQL statement cannot be executed, or the result retrieved. */ - private BackingStoreException factoryFailure(final SQLException exception) { + private static BackingStoreException factoryFailure(final SQLException exception) { return new BackingStoreException(exception.getLocalizedMessage(), exception); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java index f317371de3..09ddda6381 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CloseableReference.java @@ -46,6 +46,12 @@ final class CloseableReference extends WeakReference<AuthorityCodes> implements */ private final Statement[] statements; + /** + * Whether the referenced {@link AuthorityCodes} has been given to the user. + * If {@code false}, we can invoke {@link #close()} without waiting for the garbage collection. + */ + boolean published; + /** * Creates a new phantom reference which will close the given statements * when the given referenced object will be garbage collected. 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 690cb585ef..0c0df29970 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 @@ -18,7 +18,10 @@ package org.apache.sis.referencing.factory.sql; import java.util.Set; import java.util.List; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.Collection; +import java.util.Iterator; import java.sql.Statement; import java.sql.ResultSet; import java.sql.SQLException; @@ -32,16 +35,21 @@ import org.opengis.referencing.crs.CompoundCRS; import org.opengis.referencing.crs.GeodeticCRS; import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.crs.VerticalCRS; +import org.opengis.referencing.crs.EngineeringCRS; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.datum.Datum; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.datum.TemporalDatum; import org.opengis.referencing.datum.VerticalDatum; +import org.opengis.referencing.datum.EngineeringDatum; import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Exceptions; import org.apache.sis.util.CharSequences; import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.privy.Constants; import org.apache.sis.util.privy.CollectionsExt; +import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.pending.jdk.JDK16; import org.apache.sis.pending.jdk.JDK19; import org.apache.sis.metadata.privy.ReferencingServices; @@ -49,10 +57,15 @@ import org.apache.sis.metadata.sql.privy.SQLUtilities; import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.referencing.privy.Formulas; +import org.apache.sis.referencing.datum.DatumOrEnsemble; import org.apache.sis.referencing.factory.IdentifiedObjectFinder; import org.apache.sis.referencing.factory.ConcurrentAuthorityFactory; import static org.apache.sis.metadata.privy.NameToIdentifier.Simplifier.ESRI_DATUM_PREFIX; +// Specific to the geoapi-3.1 and geoapi-4.0 branches: +import org.opengis.referencing.crs.ParametricCRS; +import org.opengis.referencing.datum.ParametricDatum; + // Specific to the geoapi-4.0 branch: import org.opengis.referencing.crs.DerivedCRS; @@ -74,7 +87,7 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder { /** * The type of object to search, or {@code null} for using {@code object.getClass()}. * This is set to a non-null value when searching for dependencies, in order to avoid - * confusion in an implementation class implements more than one GeoAPI interfaces. + * confusion if an implementation class implements more than one GeoAPI interfaces. * * @see #isInstance(Class, IdentifiedObject) */ @@ -103,6 +116,20 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder { } } + /** + * Returns the name of the given object, with a preference for <abbr>EPSG</abbr> name. + * + * @param object the object for which to get a name. + * @return the object name, or {@code null} if none. + */ + private static String getName(final IdentifiedObject object) { + String name = IdentifiedObjects.getName(object, Citations.EPSG); + if (name == null) { + name = IdentifiedObjects.getName(object, null); + } + return name; + } + /** * Returns a description of the condition to put in a {@code WHERE} clause for an object having * the given dependency. @@ -139,7 +166,7 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder { final Set<Number> filters = JDK19.newLinkedHashSet(find.size()); for (final IdentifiedObject dep : find) { Identifier id = IdentifiedObjects.getIdentifier(dep, Citations.EPSG); - if (id != null) try { // Should never be null, but let be safe. + if (id != null) try { filters.add(Integer.valueOf(id.getCode())); } catch (NumberFormatException e) { Logging.recoverableException(EPSGDataAccess.LOGGER, EPSGCodeFinder.class, "getCodeCandidates", e); @@ -271,14 +298,19 @@ final class EPSGCodeFinder extends IdentifiedObjectFinder { } /** - * Returns a set of authority codes that <strong>may</strong> identify the same object as the specified one. - * This implementation tries to get a smaller set than what {@link EPSGDataAccess#getAuthorityCodes(Class)} - * would produce. Deprecated objects must be last in iteration order. + * Adds in the given collection the authority codes that <strong>may</strong> identify the same object as the specified one. + * This implementation tries to get a smaller set than what {@link EPSGDataAccess#getAuthorityCodes(Class)} would produce. + * Deprecated objects must be last in iteration order. + * + * @param object the object to search in the database. + * @param all whether to include the codes of all objects, even deprecated. + * @param addTo where to add the codes of objects that have been found. + * @return whether at least one code has been added. + * @throws FactoryException if an error occurred while fetching the set of code candidates. */ - @Override - protected Set<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { - final TableInfo table; // Contains `codeColumn` and `table` names. - final Condition[] filters; // Conditions to put in the WHERE clause. + private boolean searchCodesFromProperties(final IdentifiedObject object, final boolean all, final Collection<Integer> addTo) throws FactoryException { + final TableInfo source; // Contains `codeColumn` and `table` names. + final Condition[] filters; // Conditions to put in the WHERE clause. crs: if (isInstance(CoordinateReferenceSystem.class, object)) { /* * For compound CRS, the SQL statement may be something like below @@ -288,24 +320,26 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * AND CMPD_HORIZCRS_CODE IN (?,…) * AND CMPD_VERTCRS_CODE IN (?,…) */ - table = TableInfo.CRS; + source = TableInfo.CRS; if (isInstance(CompoundCRS.class, object)) { final List<CoordinateReferenceSystem> components = ((CompoundCRS) object).getComponents(); if (components != null) { // Paranoiac check. - final int n = components.size(); - if (n == 2) { - filters = new Condition[2]; - for (int i=0; i<=1; i++) { - if ((filters[i] = dependencies((i == 0) ? "CMPD_HORIZCRS_CODE" : "CMPD_VERTCRS_CODE", - CoordinateReferenceSystem.class, components.get(i), false)) == null) - { - return Set.of(); + switch (components.size()) { + case 1: { + // Defined for safety, but should not happen. + return searchCodesFromProperties(components.get(0), all, addTo); + } + case 2: { + filters = new Condition[2]; + for (int i=0; i<=1; i++) { + final CoordinateReferenceSystem component = components.get(i); + final String column = (i == 0) ? "CMPD_HORIZCRS_CODE" : "CMPD_VERTCRS_CODE"; + if ((filters[i] = dependencies(column, CoordinateReferenceSystem.class, component, false)) == null) { + return false; + } } + break crs; } - break crs; - } - if (n == 1) { // Should not happen. - return getCodeCandidates(components.get(0)); } } } @@ -322,18 +356,22 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { if (object instanceof DerivedCRS) { // No need to use isInstance(Class, Object) from here. filter = dependencies("BASE_CRS_CODE", SingleCRS.class, ((DerivedCRS) object).getBaseCRS(), true); } else if (object instanceof GeodeticCRS) { - filter = dependencies("DATUM_CODE", GeodeticDatum.class, ((GeodeticCRS) object).getDatum(), true); + filter = dependencies("DATUM_CODE", GeodeticDatum.class, DatumOrEnsemble.asDatum((GeodeticCRS) object), true); } else if (object instanceof VerticalCRS) { - filter = dependencies("DATUM_CODE", VerticalDatum.class, ((VerticalCRS) object).getDatum(), true); + filter = dependencies("DATUM_CODE", VerticalDatum.class, DatumOrEnsemble.asDatum((VerticalCRS) object), true); } else if (object instanceof TemporalCRS) { - filter = dependencies("DATUM_CODE", TemporalDatum.class, ((TemporalCRS) object).getDatum(), true); + filter = dependencies("DATUM_CODE", TemporalDatum.class, DatumOrEnsemble.asDatum((TemporalCRS) object), true); + } else if (object instanceof ParametricCRS) { + filter = dependencies("DATUM_CODE", ParametricDatum.class, DatumOrEnsemble.asDatum((ParametricCRS) object), true); + } else if (object instanceof EngineeringCRS) { + filter = dependencies("DATUM_CODE", EngineeringDatum.class, DatumOrEnsemble.asDatum((EngineeringCRS) object), true); } else if (object instanceof SingleCRS) { filter = dependencies("DATUM_CODE", Datum.class, ((SingleCRS) object).getDatum(), true); } else { - return Set.of(); + return false; } if (filter == null) { - return Set.of(); + return false; } filters = new Condition[] {filter}; } else if (isInstance(Datum.class, object)) { @@ -346,19 +384,15 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * WHERE ELLIPSOID_CODE IN (?,…) * AND (LOWER(DATUM_NAME) LIKE '?%') */ - table = TableInfo.DATUM; + source = TableInfo.DATUM; if (isInstance(GeodeticDatum.class, object)) { - filters = new Condition[] { - dependencies("ELLIPSOID_CODE", Ellipsoid.class, ((GeodeticDatum) object).getEllipsoid(), true), - Condition.NAME - }; - if (filters[0] == null) { - return Set.of(); + Condition filter = dependencies("ELLIPSOID_CODE", Ellipsoid.class, ((GeodeticDatum) object).getEllipsoid(), true); + if (filter == null) { + return false; } + filters = new Condition[] {filter, Condition.NAME}; } else { - filters = new Condition[] { - Condition.NAME - }; + filters = new Condition[] {Condition.NAME}; } } else if (isInstance(Ellipsoid.class, object)) { /* @@ -368,13 +402,15 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * WHERE SEMI_MAJOR_AXIS >= ?-ε AND SEMI_MAJOR_AXIS <= ?+ε * ORDER BY ABS(SEMI_MAJOR_AXIS-?) */ - table = TableInfo.ELLIPSOID; + source = TableInfo.ELLIPSOID; filters = new Condition[] { new FloatCondition("SEMI_MAJOR_AXIS", ((Ellipsoid) object).getSemiMajorAxis()) }; - } else { - // Not a supported type. Returns all codes. - return super.getCodeCandidates(object); + } else try { + // Not a supported type. Returns all codes if not too expensive. + return dao.getAuthorityCodes(declaredType, addTo); + } catch (SQLException exception) { + throw databaseFailure(exception); } /* * At this point we collected the information needed for creating the main SQL query. @@ -388,13 +424,13 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { final String aliasSQL; if (ArraysExt.containsIdentity(filters, Condition.NAME)) { namePatterns = new LinkedHashSet<>(); - namePatterns.add(toDatumPattern(object.getName().getCode(), buffer)); + namePatterns.add(toDatumPattern(getName(object), buffer)); for (final GenericName id : object.getAlias()) { namePatterns.add(toDatumPattern(id.tip().toString(), buffer)); } buffer.setLength(0); buffer.append("SELECT OBJECT_CODE FROM \"Alias\" WHERE OBJECT_TABLE_NAME='") - .append(dao.translator.toActualTableName(table.unquoted())) + .append(dao.translator.toActualTableName(source.table)) .append("' AND "); // PostgreSQL does not require explicit cast when the value is a literal instead of "?". appendFilterByName(namePatterns, "ALIAS", buffer); @@ -417,8 +453,8 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * of dependencies or parameter values as floating points. The last condition is on the object name. * It may be absent (typically, only datums or reference frames have that condition). */ - buffer.append("SELECT ").append(table.codeColumn).append(" FROM ").append(table.table); - table.where(dao, object, buffer); // Unconditionally append a "WHERE" clause. + buffer.append("SELECT ").append(source.codeColumn).append(" FROM ").append(source.fromClause); + source.where(dao, object, buffer); // Unconditionally append a "WHERE" clause. boolean isNext = false; for (final Condition filter : filters) { isNext |= filter.appendToWhere(buffer, isNext); @@ -432,14 +468,14 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { if (namePatterns != null) { if (isNext) buffer.append(" AND "); isNext = false; - appendFilterByName(namePatterns, table.nameColumn, buffer); + appendFilterByName(namePatterns, source.nameColumn, buffer); try (ResultSet result = stmt.executeQuery(aliasSQL)) { while (result.next()) { final int code = result.getInt(1); if (!result.wasNull()) { // Should never be null but we are paranoiac. if (!isNext) { isNext = true; - buffer.append(" OR ").append(table.codeColumn).append(" IN ("); + buffer.append(" OR ").append(source.codeColumn).append(" IN ("); } else { buffer.append(','); } @@ -449,7 +485,6 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { } if (isNext) buffer.append(')'); } - final boolean all = (getSearchDomain() == Domain.ALL_DATASET); if (!all) { buffer.append(" AND DEPRECATED=FALSE"); // Do not put spaces around "=" because SQLTranslator searches for this exact match. @@ -461,28 +496,36 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { for (final Condition filter : filters) { filter.appendToOrderBy(buffer); } - buffer.append(table.codeColumn); // Only for making order determinist. + buffer.append(source.codeColumn); // Only for making order determinist. /* * At this point the SQL query is complete. Run it, preserving order. * Then sort the result by taking in account the supersession table. */ - final var result = new LinkedHashSet<String>(); // We need to preserve order in this set. - try (ResultSet r = stmt.executeQuery(dao.translator.apply(buffer.toString()))) { - while (r.next()) { - result.add(r.getString(1)); + try (ResultSet result = stmt.executeQuery(dao.translator.apply(buffer.toString()))) { + while (result.next()) { + final int code = result.getInt(1); + if (!result.wasNull()) { // Should never be null in a valid EPSG schema. + addTo.add(code); + } } } - result.remove(null); // Should not have null element, but let be safe. - dao.sort(table.unquoted(), result).ifPresent((sorted) -> { - result.clear(); - result.addAll(JDK16.toList(sorted)); + dao.sort(source.table, addTo, Integer::intValue).ifPresent((sorted) -> { + addTo.clear(); + addTo.addAll(JDK16.toList(sorted.mapToObj(Integer::valueOf))); }); - return result; + return true; } catch (SQLException exception) { throw dao.databaseFailure(Identifier.class, String.valueOf(CollectionsExt.first(filters[0].values)), exception); } } + /** + * Returns the exception to throw when a database error occurred for no particular <abbr>EPSG</abbr> code. + */ + private static FactoryException databaseFailure(final SQLException exception) { + return new FactoryException(exception.getLocalizedMessage(), Exceptions.unwrap(exception)); + } + /** * Returns a SQL pattern for the given datum name. The name is returned in all lower cases for allowing * case-insensitive searches. Punctuations are replaced by any sequence of characters ({@code '%'}) and @@ -496,7 +539,7 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { * * @see org.apache.sis.referencing.datum.DefaultGeodeticDatum#isHeuristicMatchForName(String) */ - private static String toDatumPattern(final String name, final StringBuilder buffer) { + private String toDatumPattern(final String name, final StringBuilder buffer) { int start = 0; if (name.startsWith(ESRI_DATUM_PREFIX)) { start = ESRI_DATUM_PREFIX.length(); @@ -505,7 +548,7 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { if (end < 0) end = name.length(); end = CharSequences.skipTrailingWhitespaces(name, start, end); buffer.setLength(0); - SQLUtilities.toLikePattern(name, start, end, true, true, buffer); + SQLUtilities.toLikePattern(name, start, end, true, true, dao.translator.wildcardEscape, buffer); return buffer.toString(); } @@ -532,4 +575,188 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { } buffer.append(')'); } + + /** + * Returns a set of authority codes that <strong>may</strong> identify the same object as the specified one. + * This implementation tries to get a smaller set than what {@link EPSGDataAccess#getAuthorityCodes(Class)} + * would produce. Deprecated objects must be last in iteration order. + * + * <h4>Exceptions during iteration</h4> + * An unchecked {@link BackingStoreException} may be thrown during the iteration + * if the action of fetching codes from database was delayed and that action failed. + * The exception cause may be {@link FactoryException} or {@link SQLException}. + * + * @param object the object to search in the database. + * @return codes of objects that may be the requested ones. + * @throws FactoryException if an error occurred while fetching the set of code candidates. + */ + @Override + protected Iterable<String> getCodeCandidates(final IdentifiedObject object) throws FactoryException { + for (final TableInfo table : TableInfo.EPSG) { + if (table.type.isInstance(object)) try { + return new CodeCandidates(object, table); + } catch (SQLException exception) { + throw databaseFailure(exception); + } + } + return Set.of(); + } + + /** + * Set of authority codes that <strong>may</strong> identify the same object as the specified one. + * This collection returns the codes that can be obtained easily before the more expensive searches. + */ + private final class CodeCandidates implements Iterable<String> { + /** The object to search. */ + private final IdentifiedObject object; + + /** Information about the tables of the object to search. */ + private final TableInfo source; + + /** Cache of codes found so far. */ + private final Set<Integer> codes; + + /** Whether to include the codes of all objects, even the deprecated ones. */ + private boolean includeAll; + + /** Whether to use only identifiers, name and aliases in the search. */ + private boolean easySearch; + + /** + * Algorithm used for filling the {@link #codes} collection so far. + * 0 = identifiers, 1 = name, 2 = aliases, 3 = search based on properties. + */ + private byte searchMethod; + + /** + * Creates a lazy collection of code candidates. + * This constructor loads immediately some codes in order to have an exception early in case of problem. + * + * @param object the object to search in the database. + * @param source information about the table where to search for the object. + * @throws SQLException if an error occurred while searching for codes associated to names. + * @throws FactoryException if an error occurred while fetching the set of code candidates. + */ + CodeCandidates(final IdentifiedObject object, final TableInfo source) throws SQLException, FactoryException { + this.object = object; + this.source = source; + this.codes = new LinkedHashSet<>(); + switch (getSearchDomain()) { + case DECLARATION: easySearch = true; break; + case ALL_DATASET: includeAll = true; break; + case EXHAUSTIVE_VALID_DATASET: { + // Skip the search methods based on identifiers, name or aliases. + searchCodesFromProperties(object, false, codes); + searchMethod = 3; + return; + } + } + for (final Identifier id : object.getIdentifiers()) { + if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) try { + codes.add(Integer.valueOf(id.getCode())); + } catch (NumberFormatException exception) { + Logging.ignorableException(EPSGDataAccess.LOGGER, IdentifiedObjectFinder.class, "find", exception); + } + } + if (codes.isEmpty()) { + fetchMoreCodes(codes); + } + } + + /** + * Populates the given collection with code candidates. + * This method tries less expansive search methods before to tries more expensive search methods. + * + * @param addTo an initially empty collection where to add the codes. + * @return whether at least one code has been added to the given collection. + * @throws SQLException if an error occurred while searching for codes associated to names. + * @throws FactoryException if an error occurred while fetching the set of code candidates. + */ + private boolean fetchMoreCodes(final Collection<Integer> addTo) throws SQLException, FactoryException { + do { + switch (searchMethod) { + case 0: findCodesFromName(false, addTo); break; // Fetch codes for the name. + case 1: findCodesFromName(true, addTo); break; // Fetch codes for the aliases. + case 2: if (easySearch) break; // Search codes based on object properties. + searchCodesFromProperties(object, includeAll, addTo); + break; + default: return false; + } + searchMethod++; + } while (addTo.isEmpty()); + return true; + } + + /** + * Adds the authority codes of all objects of the given name. + * Callers should update the {@link #searchMethod} flag accordingly. + * + * @param alias whether to search in the alias table rather than the main name. + * @param codes the collection where to add the codes that have been found. + * @throws SQLException if an error occurred while querying the database. + */ + private void findCodesFromName(final boolean alias, final Collection<Integer> addTo) throws SQLException { + final String name = getName(object); + if (name != null) { // Should never be null, but we are paranoiac. + dao.findCodesFromName(source.table, object.getClass(), name, alias, addTo); + } + } + + /** + * Returns additional code candidates which were not yet returned by the iteration. + * This method uses the next search method which hasn't be tried. + * + * @return the additional codes. + * @throws BackingStoreException if an error occurred while fetching the set of code candidates. + */ + private Iterator<Integer> fetchMoreCodes() { + final var addTo = new ArrayList<Integer>(); + do { + try { + if (!fetchMoreCodes(addTo)) break; + } catch (SQLException | FactoryException exception) { + throw new BackingStoreException(exception); + } + for (Iterator<Integer> it = addTo.iterator(); it.hasNext();) { + if (!codes.add(it.next())) { + it.remove(); // Code has already be returned. + } + } + } while (addTo.isEmpty()); + return addTo.iterator(); + } + + /** + * Returns an iterator over the code candidates. The codes are cached: + * the should not be fetched again if a second iteration is executed. + * + * <h4>Limitation</h4> + * The current implementation does not support concurrent iterations, even in the same thread. + * This is okay for the usage that Apache <abbr>SIS</abbr> is making of this iterator. + */ + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + /** Iterator over a subset of the codes. */ + private Iterator<Integer> sources = codes.iterator(); + + /** Tests whether there is more codes to return. */ + @Override public boolean hasNext() { + if (sources.hasNext()) { + return true; + } + sources = fetchMoreCodes(); + return sources.hasNext(); + } + + /** Returns the next code. */ + @Override public String next() { + if (!sources.hasNext()) { + sources = fetchMoreCodes(); + } + return sources.next().toString(); + } + }; + } + } } 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 c06174587d..5158226b41 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 @@ -21,7 +21,6 @@ import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.ArrayList; import java.util.Collection; @@ -30,7 +29,8 @@ import java.util.Date; import java.util.Locale; import java.util.Objects; import java.util.Optional; -import java.util.stream.Stream; +import java.util.stream.IntStream; +import java.util.function.ToIntFunction; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; @@ -219,12 +219,6 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho */ private final NameSpace namespace; - /** - * The last table in which object name were looked for. - * This is for internal use by {@link #toPrimaryKeys} only. - */ - private String lastTableForName; - /** * A pool of prepared statements. Keys are {@link String} objects related to their originating method * (for example "Ellipsoid" for {@link #createEllipsoid(String)}). @@ -540,7 +534,7 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho * for a deprecated object, even if that identifier does not show up in iterations. * In other words, the returned collection behaves as if deprecated codes were included in the set but invisible. * - * @param type the spatial reference objects type (may be {@code Object.class}). + * @param type the type of spatial reference objects for which to get the authority codes. * @return the set of authority codes for spatial reference objects of the given type (may be an empty set). * @throws FactoryException if access to the underlying database failed. */ @@ -550,85 +544,98 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho if (connection.isClosed()) { throw new FactoryException(error().getString(Errors.Keys.ConnectionClosed)); } - return getCodeMap(type).keySet(); + final AuthorityCodes codes = getCodeMap(Objects.requireNonNull(type), null, true); + if (codes != null) { + return codes.keySet(); + } } catch (SQLException exception) { throw new FactoryException(exception.getLocalizedMessage(), Exceptions.unwrap(exception)); } + return Set.of(); + } + + /** + * Puts all codes in the given collection. This method is used only as a fallback when {@link EPSGCodeFinder} + * cannot get a smaller list of authority codes by using {@code WHERE} conditions on property values. + * This method should not be invoked for the most common objects such as <abbr>CRS</abbr> and datum. + * 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. + * @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; } /** * Returns a map of <abbr>EPSG</abbr> authority codes as keys and object names as values. * 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 (may be {@code Object.class}). - * @return the map of authority codes associated to their names. May be an empty map. + * @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. + * @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 Map<String,String> getCodeMap(final Class<?> type) throws SQLException { + private synchronized AuthorityCodes getCodeMap(final Class<?> type, TableInfo source, boolean publish) throws SQLException { CloseableReference reference = authorityCodes.get(type); if (reference != null) { AuthorityCodes existing = reference.get(); if (existing != null) { + reference.published |= publish; return existing; } } - Map<String, String> result = Map.of(); - for (final TableInfo table : TableInfo.EPSG) { - /* - * We test `isAssignableFrom` in the two ways for catching the following use cases: - * - * - `table.type.isAssignableFrom(type)` - * is for the case where a table is for CoordinateReferenceSystem while the user type is some subtype - * like GeographicCRS. The GeographicCRS need to be queried into the CoordinateReferenceSystem table. - * An additional filter will be applied inside the AuthorityCodes class implementation. - * - * - `type.isAssignableFrom(table.type)` - * is for the case where the user type is IdentifiedObject or Object, in which case we basically want - * to iterate through every tables. - */ - if (table.type.isAssignableFrom(type) || type.isAssignableFrom(table.type)) { - /* - * 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. - */ - var codes = new AuthorityCodes(connection, table, type, this); - reference = authorityCodes.get(codes.type); - if (reference != null) { - AuthorityCodes existing = reference.get(); - if (existing != null) { - codes = existing; - } else { - reference = null; // The weak reference is no longer valid. - } - } - if (reference == null) { - reference = codes.createReference(); - authorityCodes.put(codes.type, reference); - } - if (type != codes.type) { - authorityCodes.put(type, reference); - } - /* - * We now have the codes for a single type. Append with the codes of previous types, if any. - * This usually happen only if the user asked for the IdentifiedObject type. Of course this - * break all our effort to query the data only when first needed, but the user should ask - * for more specific types. - */ - if (result.isEmpty()) { - result = codes; - } else { - if (result instanceof AuthorityCodes) { - result = new LinkedHashMap<>(result); + if (source == null) { + for (TableInfo c : TableInfo.EPSG) { + if (c.type.isAssignableFrom(type)) { + if (source != null) { + return null; // The specified type is too generic. } - result.putAll(codes); + source = c; } } + if (source == null) { + return null; // The specified type is unsupported. + } } - return result; + AuthorityCodes codes = new AuthorityCodes(source, type, 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); + if (reference != null) { + AuthorityCodes existing = reference.get(); + if (existing != null) { + codes = existing; + } else { + reference = null; // The weak reference is no longer valid. + } + } + if (reference == null) { + reference = codes.createReference(); + authorityCodes.put(codes.type, reference); + } + if (type != codes.type) { + authorityCodes.put(type, reference); + } + reference.published |= publish; + return codes; } /** @@ -658,12 +665,11 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho throws FactoryException { try { - for (final TableInfo table : TableInfo.EPSG) { - if (table.nameColumn != null && type.isAssignableFrom(table.type)) { - final String text = getCodeMap(table.type).get(code); - if (text != null) { - return Optional.of(new SimpleInternationalString(text)); - } + final AuthorityCodes codes = getCodeMap(Objects.requireNonNull(type), null, false); + if (codes != null) { + final String text = codes.get(code); + if (text != null) { + return Optional.of(new SimpleInternationalString(text)); } } } catch (SQLException | BackingStoreException exception) { @@ -712,81 +718,37 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho * Converts <abbr>EPSG</abbr> codes or <abbr>EPSG</abbr> names to the numerical identifiers (the primary keys). * This method can be seen as the converse of above {@link #getDescriptionText(Class, String)} method. * - * @param table the table where the code should appears, or {@code null} if {@code codeColumn} is null. - * @param codeColumn the column name for the codes, or {@code null} if none. - * @param nameColumn the column name for the names, or {@code null} if none. - * @param codes the codes or names to convert to primary keys, as an array of length 1 or 2. + * @param table the table where the code should appear, or {@code null} for no search by name. + * @param codes the codes or names to convert to primary keys, as an array of length 1 or 2. * @return the numerical identifiers (i.e. the table primary key values). * @throws SQLException if an error occurred while querying the database. * @throws FactoryDataException if code is a name and two distinct numerical codes match the name. * @throws NoSuchAuthorityCodeException if code is a name and no numerical code match the name. */ - private int[] toPrimaryKeys(final String table, final String codeColumn, final String nameColumn, final String... codes) - throws SQLException, FactoryException - { + private int[] toPrimaryKeys(final String table, final String... codes) throws SQLException, FactoryException { final int[] primaryKeys = new int[codes.length]; -next: for (int i=0; i<codes.length; i++) { + for (int i=0; i<codes.length; i++) { String code = codes[i]; - if (codeColumn != null && nameColumn != null && !isPrimaryKey(code)) { + if (table != null && !isPrimaryKey(code)) { /* * The given string is not a numerical code. Search the value in the database. * We search first in the table of the query. If the name is not found there, * then we will search in the aliases table as a fallback. */ - final String pattern = SQLUtilities.toLikePattern(code, false); - boolean searchInTableOfQuery = true; + final var result = new ArrayList<Integer>(); + findCodesFromName(table, null, code, false, result); + if (result.isEmpty()) { + // Search in aliases only if no match was found in primary names. + findCodesFromName(table, null, code, true, result); + } Integer resolved = null; - do { // Executed exactly 1 or 2 times. - PreparedStatement stmt; - if (searchInTableOfQuery) { - /* - * The SQL query for searching in the queried table is a little bit more complicated - * than the query for searching in the alias table. The existing prepared statement - * can be reused only if it was created for the current table. - */ - final String KEY = "PrimaryKey"; - if (table.equals(lastTableForName)) { - stmt = statements.get(KEY); - } else { - stmt = statements.remove(KEY); - if (stmt != null) { - stmt.close(); - stmt = null; - } - } - if (stmt == null) { - stmt = connection.prepareStatement(translator.apply( - "SELECT " + codeColumn + ", " + nameColumn - + " FROM \"" + table + '"' - + " WHERE " + nameColumn + " LIKE ?")); - statements.put(KEY, stmt); - lastTableForName = table; - } - stmt.setString(1, pattern); - } else { - /* - * If the object name is not found in the queries table, - * search in the table of aliases. - */ - stmt = prepareStatement("AliasKey", - "SELECT OBJECT_CODE, ALIAS" - + " FROM \"Alias\"" - + " WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?"); - stmt.setString(1, translator.toActualTableName(table)); - stmt.setString(2, pattern); - } - try (ResultSet result = stmt.executeQuery()) { - while (result.next()) { - if (SQLUtilities.filterFalsePositive(code, result.getString(2))) { - resolved = ensureSingleton(getOptionalInteger(result, 1), resolved, code); - } - } - } - if (resolved != null) { - primaryKeys[i] = resolved; - continue next; - } - } while ((searchInTableOfQuery = !searchInTableOfQuery) == false); + for (Integer value : result) { + resolved = ensureSingleton(value, resolved, code); + } + if (resolved != null) { + primaryKeys[i] = resolved; + continue; + } } /* * At this point, `code` should be the primary key. It may still be a non-numerical string @@ -805,6 +767,48 @@ next: for (int i=0; i<codes.length; i++) { return primaryKeys; } + /** + * Finds the authority codes for the given name. + * + * @param table the table where the code should appear. + * @parma type the type of object to searh, or {@code null} for inferring from the table. + * @param name the name to search. + * @param alias whether to search in the alias table rather than the main name. + * @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 String table, final Class<?> type, final String name, final boolean alias, final Collection<Integer> addTo) + throws SQLException + { + final String pattern = SQLUtilities.toLikePattern(name, false, translator.wildcardEscape); + if (alias) { + final PreparedStatement stmt = prepareStatement( + "AliasKey", + "SELECT OBJECT_CODE, ALIAS" + + " FROM \"Alias\"" + + " WHERE OBJECT_TABLE_NAME=? AND ALIAS LIKE ?"); + stmt.setString(1, translator.toActualTableName(table)); + stmt.setString(2, pattern); + try (ResultSet result = stmt.executeQuery()) { + while (result.next()) { + if (SQLUtilities.filterFalsePositive(name, result.getString(2))) { + addTo.add(getOptionalInteger(result, 1)); + } + } + } + } else { + for (final TableInfo source : TableInfo.EPSG) { + if (table.equals(source.table)) { + AuthorityCodes codes = getCodeMap(type == null ? source.type : type, source, false); + if (codes != null) { + codes.findCodesFromName(pattern, name, addTo); + break; + } + } + } + } + } + /** * Creates and executes a statement for the given codes with a protection against infinite loops. * The first code value is assigned to parameter #1, the second code value (if any) is assigned to parameter #2, @@ -825,27 +829,21 @@ next: for (int i=0; i<codes.length; i++) { * in their {@code finally} block.</li> * </ul> * - * @param table the table where the code should appears. - * @param codeColumn the column name for the codes, or {@code null} if none. - * @param nameColumn the column name for the names, or {@code null} if none. - * @param sql the SQL statement to use for creating the {@link PreparedStatement} object. - * Will be used only if no prepared statement was already created for the given code. - * @param codes the codes of the object to create, as an array of length 1 or 2. + * @param table the table where the code should appears. + * @param sql the SQL statement to use for creating the {@link PreparedStatement} object. + * Will be used only if no prepared statement was already created for the given code. + * @param codes the codes of the object to create, as an array of length 1 or 2. * @return the result of the query. * @throws SQLException if an error occurred while querying the database. */ - private ResultSet executeSingletonQuery(final String table, - final String codeColumn, - final String nameColumn, - final String sql, - final String... codes) + private ResultSet executeSingletonQuery(final String table, final String sql, final String... codes) throws SQLException, FactoryException { assert Thread.holdsLock(this); assert sql.contains('"' + table + '"') : table; - assert (codeColumn == null) || sql.contains(codeColumn) || table.equals("Extent") : codeColumn; - assert (nameColumn == null) || sql.contains(nameColumn) || table.equals("Extent") : nameColumn; - final int[] keys = toPrimaryKeys(table, codeColumn, nameColumn, codes); + // assert (codeColumn == null) || sql.contains(codeColumn) || table.equals("Extent") : codeColumn; + // assert (nameColumn == null) || sql.contains(nameColumn) || table.equals("Extent") : nameColumn; + final int[] keys = toPrimaryKeys(table, codes); currentSingletonQuery = new QueryID(table, keys, currentSingletonQuery); if (currentSingletonQuery.isAlreadyInProgress()) { throw new FactoryDataException(resources().getString( @@ -1423,7 +1421,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final int queryStart = query.length(); int found = -1; try { - final int key = isPrimaryKey ? toPrimaryKeys(null, null, null, code)[0] : 0; + final int key = isPrimaryKey ? toPrimaryKeys(null, code)[0] : 0; for (int i = 0; i < TableInfo.EPSG.length; i++) { final TableInfo table = TableInfo.EPSG[i]; final String column = isPrimaryKey ? table.codeColumn : table.nameColumn; @@ -1435,7 +1433,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", if (!isPrimaryKey) { query.append(", ").append(column); // Only for filterFalsePositive(…). } - query.append(" FROM ").append(table.table) + query.append(" FROM ").append(table.fromClause) .append(" WHERE ").append(column).append(isPrimaryKey ? " = ?" : " LIKE ?"); try (PreparedStatement stmt = connection.prepareStatement(translator.apply(query.toString()))) { /* @@ -1445,7 +1443,7 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", if (isPrimaryKey) { stmt.setInt(1, key); } else { - stmt.setString(1, SQLUtilities.toLikePattern(code, false)); + stmt.setString(1, SQLUtilities.toLikePattern(code, false, translator.wildcardEscape)); } Integer present = null; try (ResultSet result = stmt.executeQuery()) { @@ -1565,8 +1563,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate Reference System", - "COORD_REF_SYS_CODE", - "COORD_REF_SYS_NAME", "SELECT"+ /* column 1 */ " COORD_REF_SYS_CODE," + /* column 2 */ " COORD_REF_SYS_NAME," + /* column 3 */ " AREA_OF_USE_CODE," // Deprecated since EPSG version 10 (always NULL) @@ -1880,8 +1876,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Datum", - "DATUM_CODE", - "DATUM_NAME", "SELECT"+ /* column 1 */ " DATUM_CODE," + /* column 2 */ " DATUM_NAME," + /* column 3 */ " DATUM_TYPE," @@ -2170,8 +2164,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Ellipsoid", - "ELLIPSOID_CODE", - "ELLIPSOID_NAME", "SELECT"+ /* column 1 */ " ELLIPSOID_CODE," + /* column 2 */ " ELLIPSOID_NAME," + /* column 3 */ " SEMI_MAJOR_AXIS," @@ -2274,8 +2266,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Prime Meridian", - "PRIME_MERIDIAN_CODE", - "PRIME_MERIDIAN_NAME", "SELECT"+ /* column 1 */ " PRIME_MERIDIAN_CODE," + /* column 2 */ " PRIME_MERIDIAN_NAME," + /* column 3 */ " GREENWICH_LONGITUDE," @@ -2355,8 +2345,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Extent", - "EXTENT_CODE", - "EXTENT_NAME", "SELECT"+ /* column 1 */ " EXTENT_DESCRIPTION," + /* column 2 */ " BBOX_SOUTH_BOUND_LAT," + /* column 3 */ " BBOX_NORTH_BOUND_LAT," @@ -2474,8 +2462,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate System", - "COORD_SYS_CODE", - "COORD_SYS_NAME", "SELECT"+ /* column 1 */ " COORD_SYS_CODE," + /* column 2 */ " COORD_SYS_NAME," + /* column 3 */ " COORD_SYS_TYPE," @@ -2637,8 +2623,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate Axis", - "COORD_AXIS_CODE", - null, "SELECT"+ /* column 1 */ " COORD_AXIS_CODE," + /* column 2 */ " COORD_AXIS_NAME_CODE," + /* column 3 */ " COORD_AXIS_ORIENTATION," @@ -2779,8 +2763,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Unit of Measure", - "UOM_CODE", - "UNIT_OF_MEAS_NAME", "SELECT"+ /* column 1 */ " UOM_CODE," + /* column 2 */ " FACTOR_B," + /* column 3 */ " FACTOR_C," @@ -2863,8 +2845,6 @@ search: try (ResultSet result = executeMetadataQuery("Deprecation", final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate_Operation Parameter", - "PARAMETER_CODE", - "PARAMETER_NAME", "SELECT"+ /* column 1 */ " PARAMETER_CODE," + /* column 2 */ " PARAMETER_NAME," + /* column 3 */ " DESCRIPTION," @@ -3115,8 +3095,6 @@ next: while (r.next()) { final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate_Operation Method", - "COORD_OP_METHOD_CODE", - "COORD_OP_METHOD_NAME", "SELECT"+ /* column 1 */ " COORD_OP_METHOD_CODE," + /* column 2 */ " COORD_OP_METHOD_NAME," + /* column 3 */ " REMARKS," @@ -3193,8 +3171,6 @@ next: while (r.next()) { final QueryID previousSingletonQuery = currentSingletonQuery; try (ResultSet result = executeSingletonQuery( "Coordinate_Operation", - "COORD_OP_CODE", - "COORD_OP_NAME", "SELECT"+ /* column 1 */ " COORD_OP_CODE," + /* column 2 */ " COORD_OP_NAME," + /* column 3 */ " COORD_OP_TYPE," @@ -3385,7 +3361,7 @@ next: while (r.next()) { final String label = sourceCRS + " ⇨ " + targetCRS; final var set = new CoordinateOperationSet(owner); try { - final int[] pair = toPrimaryKeys(null, null, null, sourceCRS, targetCRS); + final int[] pair = toPrimaryKeys(null, sourceCRS, targetCRS); boolean searchTransformations = false; do { /* @@ -3422,9 +3398,9 @@ next: while (r.next()) { * Alter the ordering using the information supplied in the extents * and supersession tables. */ - List<String> codes = Arrays.asList(set.getAuthorityCodes()); - sort("Coordinate_Operation", codes).ifPresent((sorted) -> { - set.setAuthorityCodes(sorted.toArray(String[]::new)); + final List<String> codes = Arrays.asList(set.getAuthorityCodes()); + sort("Coordinate_Operation", codes, Integer::parseInt).ifPresent((sorted) -> { + set.setAuthorityCodes(sorted.mapToObj(Integer::toString).toArray(String[]::new)); }); } catch (SQLException exception) { throw databaseFailure(CoordinateOperation.class, label, exception); @@ -3468,9 +3444,10 @@ next: while (r.next()) { * * @param table the table of the objects for which to check for supersession. * @param codes the codes to sort. This collection will not be modified by this method. - * @return codes of sorted elements, or empty if this method did not changed the codes order. + * @param parser the method to invoke for converting a {@code codes} element to an integer. + * @return codes of sorted elements, or empty if this method did not change the codes order. */ - final synchronized Optional<Stream<String>> sort(final String table, final Collection<String> codes) + final synchronized <C extends Comparable<?>> Optional<IntStream> sort(final String table, final Collection<C> codes, final ToIntFunction<C> parser) throws SQLException, FactoryException { final int size = codes.size(); @@ -3479,10 +3456,10 @@ next: while (r.next()) { final var extents = new ArrayList<String>(); final String actualTable = translator.toActualTableName(table); int count = 0; - for (final String code : codes) { + for (final C code : codes) { final int key; try { - key = Integer.parseInt(code); + key = parser.applyAsInt(code); } catch (NumberFormatException e) { unexpectedException("sort", e); continue; @@ -3497,7 +3474,7 @@ next: while (r.next()) { * the finally block and the `TableInfo.areaOfUse` flag. */ for (final TableInfo info : TableInfo.EPSG) { - if (table.equals(info.unquoted())) { + if (table.equals(info.table)) { if (info.areaOfUse) { try (ResultSet result = executeQueryForCodes( "Area", // Table from EPSG version 9. Does not exist anymore in version 10. @@ -3532,7 +3509,7 @@ next: while (r.next()) { elements[count++] = element; } if (ObjectPertinence.sort(elements)) { - return Optional.of(Arrays.stream(elements).map(ObjectPertinence::code)); + return Optional.of(Arrays.stream(elements).mapToInt((p) -> p.code)); } } finally { /* @@ -3603,11 +3580,22 @@ next: while (r.next()) { */ final synchronized boolean canClose() { boolean can = true; + SQLException error = null; if (!authorityCodes.isEmpty()) { System.gc(); // For cleaning as much weak references as we can before we check them. final Iterator<CloseableReference> it = authorityCodes.values().iterator(); while (it.hasNext()) { - if (JDK16.refersTo(it.next(), null)) { + final CloseableReference reference = it.next(); + if (!reference.published) { + it.remove(); + try { + reference.close(); + } catch (SQLException e) { + if (error == null) error = e; + else error.addSuppressed(e); + } + reference.clear(); + } else if (JDK16.refersTo(reference, null)) { it.remove(); } else { /* @@ -3618,11 +3606,14 @@ next: while (r.next()) { } } } + if (error != null) { + unexpectedException("canClose", error); + } return can; } /** - * Closes the JDBC connection used by this factory. + * Closes the <abbr>JDBC</abbr> connection used by this factory. * If this {@code EPSGDataAccess} is used by an {@link EPSGFactory}, then this method * will be automatically invoked after some {@linkplain EPSGFactory#getTimeout timeout}. * diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java index 82da6646a7..f9c2c22860 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java @@ -47,7 +47,7 @@ final class ObjectPertinence implements Comparable<ObjectPertinence> { /** * Code of the object for which the pertinence is evaluated. */ - private final int code; + final int code; /** * An estimation of the surface of the domain of validity as a negative number, or NaN if none. @@ -85,13 +85,6 @@ final class ObjectPertinence implements Comparable<ObjectPertinence> { replacedBy = new ArrayList<>(); } - /** - * Returns the code of the object for which the pertinence is evaluated. - */ - final String code() { - return Integer.toString(code); - } - /** * Determines the ordering based on the extent. * This method does not take supersession in account. @@ -123,7 +116,7 @@ final class ObjectPertinence implements Comparable<ObjectPertinence> { do { redo = false; for (int i=0; i<elements.length; i++) { - for (final Integer replacement : elements[i].replacedBy) { + for (final int replacement : elements[i].replacedBy) { for (int j=i+1; j<elements.length; j++) { final ObjectPertinence candidate = elements[j]; if (candidate.code == replacement) { 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 1ec4515c09..b275964bab 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 @@ -214,11 +214,18 @@ public class SQLTranslator implements UnaryOperator<String> { private Map<String,String> replacements; /** - * The characters used for quoting identifiers, or a whitespace if none. + * The characters used for quoting identifiers, or an empty string if none. * This information is provided by {@link DatabaseMetaData#getIdentifierQuoteString()}. */ private final String identifierQuote; + /** + * The string that can be used to escape wildcard characters in {@code LIKE}. + * This is the value returned by {@link DatabaseMetaData#getSearchStringEscape()}. + * It may be null or empty if the database has no escape character. + */ + final String wildcardEscape; + /** * Non-null if the {@value #ENUMERATION_COLUMN} column in {@code "Alias"} table uses enumeration instead * than character varying. In such case, this field contains the enumeration type. If {@code null}, then @@ -283,6 +290,7 @@ public class SQLTranslator implements UnaryOperator<String> { */ public SQLTranslator(final DatabaseMetaData md, final String catalog, final String schema) throws SQLException { identifierQuote = md.getIdentifierQuoteString().trim(); + wildcardEscape = md.getSearchStringEscape(); this.catalog = catalog; this.schema = schema; setup(md); @@ -308,8 +316,7 @@ public class SQLTranslator implements UnaryOperator<String> { */ @SuppressWarnings("fallthrough") final void setup(final DatabaseMetaData md) throws SQLException { - final String escape = md.getSearchStringEscape(); - String schemaPattern = SQLUtilities.escape(schema, escape); + String schemaPattern = SQLUtilities.escape(schema, wildcardEscape); int tableIndex = 0; do { usePrefixedTableNames = false; @@ -318,7 +325,7 @@ public class SQLTranslator implements UnaryOperator<String> { switch (tableIndex++) { case 0: { // Test EPSG standard table name first. usePrefixedTableNames = true; - table = SQLUtilities.escape(TABLE_PREFIX, escape); + table = SQLUtilities.escape(TABLE_PREFIX, wildcardEscape); // Fallthrough for testing "epsg_coordoperation". } case 2: { @@ -352,7 +359,7 @@ public class SQLTranslator implements UnaryOperator<String> { * naming convention (unquoted or mixed-case, prefixed by "epsg_" or not). */ UnaryOperator<String> toNativeCase = UnaryOperator.identity(); - schemaPattern = SQLUtilities.escape(schema, escape); + schemaPattern = SQLUtilities.escape(schema, wildcardEscape); tableRewording = new HashMap<>(); replacements = new HashMap<>(); /* @@ -456,7 +463,7 @@ check: for (;;) { boolean isTableFound = false; brokenTargetCols.addAll(mayRenameColumns.values()); table = toNativeCase.apply(toActualTableName(table)); - try (ResultSet result = md.getColumns(catalog, schemaPattern, SQLUtilities.escape(table, escape), "%")) { + try (ResultSet result = md.getColumns(catalog, schemaPattern, SQLUtilities.escape(table, wildcardEscape), "%")) { 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); 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 bd68bcbf52..5acf36cebb 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 @@ -169,13 +169,17 @@ final class TableInfo { final Class<?> type; /** - * The table name for SQL queries, including schema name and quotes for mixed-case names. - * May contain a {@code JOIN} clause. - * - * @see #unquoted() + * The table name in mixed-case and without quotes. */ final String table; + /** + * The <abbr>SQL</abbr> fragment to use in the {@code FROM} clause. + * This is usually the table name, including the quotes for mixed-case names. + * In sometime, this is a more complex clause including {@code JOIN} statements. + */ + final String fromClause; + /** * Column name for the code (usually with the {@code "_CODE"} suffix). */ @@ -216,7 +220,7 @@ final class TableInfo { * Stores information about a specific table. * * @param type the class of object to be created (usually a GeoAPI interface). - * @param table the table name for SQL queries, including quotes for mixed-case names. + * @param fromClause The <abbr>SQL</abbr> fragment to use in the {@code FROM} clause, including quotes. * @param codeColumn column name for the code (usually with the {@code "_CODE"} suffix). * @param nameColumn column name for the name (usually with the {@code "_NAME"} suffix), or {@code null}. * @param typeColumn column type for the type (usually with the {@code "_TYPE"} suffix), or {@code null}. @@ -226,12 +230,12 @@ final class TableInfo { * @param areaOfUse whether the table had an {@code "AREA_OF_USE_CODE"} column in the legacy versions. */ private TableInfo(final Class<?> type, - final String table, final String codeColumn, final String nameColumn, + final String fromClause, final String codeColumn, final String nameColumn, final String typeColumn, final Class<?>[] subTypes, final String[] typeNames, final String showColumn, final boolean areaOfUse) { this.type = type; - this.table = table; + this.fromClause = fromClause; this.codeColumn = codeColumn; this.nameColumn = nameColumn; this.typeColumn = typeColumn; @@ -239,13 +243,7 @@ final class TableInfo { this.typeNames = typeNames; this.showColumn = showColumn; this.areaOfUse = areaOfUse; - } - - /** - * Returns the table name without schema name and without quotes. - */ - final String unquoted() { - return table.substring(table.indexOf('"') + 1, table.lastIndexOf('"')); + table = fromClause.substring(fromClause.indexOf('"') + 1, fromClause.lastIndexOf('"')).intern(); } /** @@ -263,7 +261,7 @@ final class TableInfo { table = table.substring(SQLTranslator.TABLE_PREFIX.length()); } for (final TableInfo info : EPSG) { - if (CharSequences.isAcronymForWords(table, info.unquoted())) { + if (CharSequences.isAcronymForWords(table, info.table)) { return Optional.of(info.type.getSimpleName()); } } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java index 2e9b18a75a..786face47f 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java @@ -324,9 +324,8 @@ class CoordinateOperationRegistry { } codes = new ArrayList<>(); codeFinder.setIgnoringAxes(true); - codeFinder.setSearchDomain(isEasySearch(crs) - ? IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET - : IdentifiedObjectFinder.Domain.VALID_DATASET); + codeFinder.setSearchDomain(isEasySearch(crs) ? IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET + : IdentifiedObjectFinder.Domain.VALID_DATASET); int matchCount = 0; try { final Citation authority = registry.getAuthority(); diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java index 6f7773c557..6f723ce1e3 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AuthorityFactoriesTest.java @@ -107,7 +107,7 @@ public final class AuthorityFactoriesTest extends TestCaseWithLogs { * @param code the code of the object for which to fetch the description. */ private void assertDescriptionEquals(String expected, String code) throws FactoryException { - assertEquals(expected, AuthorityFactories.ALL.getDescriptionText(IdentifiedObject.class, code).orElseThrow().toString()); + assertEquals(expected, AuthorityFactories.ALL.getDescriptionText(CoordinateReferenceSystem.class, code).orElseThrow().toString()); } /** 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 6543f29648..bf8525cc65 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 @@ -684,15 +684,6 @@ public final class EPSGFactoryTest extends TestCaseWithLogs { assertFalse(units.isEmpty()); assertTrue (units.size() >= 2); - // Tests the fusion of all types - if (RUN_EXTENSIVE_TESTS) { - final Set<String> all = factory.getAuthorityCodes(IdentifiedObject.class); - assertTrue(all.containsAll(crs)); - assertTrue(all.containsAll(datum)); - assertTrue(all.containsAll(operations)); - assertTrue(all.containsAll(units)); - } - // Try a dummy type. @SuppressWarnings({"unchecked","rawtypes"}) final Class<? extends IdentifiedObject> wrong = (Class) String.class; @@ -708,11 +699,11 @@ public final class EPSGFactoryTest extends TestCaseWithLogs { @Test public void testDescriptionText() throws FactoryException { final EPSGFactory factory = dataEPSG.factory(); - assertEquals("World Geodetic System 1984", factory.getDescriptionText(IdentifiedObject.class, "6326").get().toString(Locale.US)); - assertEquals("Mean Sea Level", factory.getDescriptionText(IdentifiedObject.class, "5100").get().toString(Locale.US)); - assertEquals("NTF (Paris) / Nord France", factory.getDescriptionText(IdentifiedObject.class, "27591").get().toString(Locale.US)); - assertEquals("NTF (Paris) / France II", factory.getDescriptionText(IdentifiedObject.class, "27582").get().toString(Locale.US)); - assertEquals("Ellipsoidal height", factory.getDescriptionText(IdentifiedObject.class, "84").get().toString(Locale.US)); + 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)); loggings.assertNoUnexpectedLog(); } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java index ad0bb48804..af7647a142 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java @@ -816,7 +816,7 @@ public class InfoStatements implements Localized, AutoCloseable { sridFromCRS = prepareSearchCRS(true); } sridFromCRS.setInt(1, code); - sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, true)); + sridFromCRS.setString(2, SQLUtilities.toLikePattern(authority, true, database.wildcardEscape)); try (ResultSet result = sridFromCRS.executeQuery()) { while (result.next()) { if (SQLUtilities.filterFalsePositive(authority, result.getString(1))) { diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java index a69a986a26..d964ed27e9 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/logging/Logging.java @@ -356,7 +356,7 @@ public final class Logging extends Static { * @since 1.0 */ public static boolean ignorableException(final Logger logger, final Class<?> classe, - final String method, final Throwable error) + final String method, final Throwable error) { final String classname = (classe != null) ? classe.getName() : null; return unexpectedException(logger, classname, method, error, Level.FINER); diff --git a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql index 39e59a71d5..9083d21524 100644 --- a/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql +++ b/optional/src/org.apache.sis.referencing.epsg/main/org/apache/sis/referencing/factory/sql/epsg/Prepare.sql @@ -10,8 +10,8 @@ -- If enumerated values are not supported by the database, Apache SIS will automatically replace their usage -- by the VARCHAR type. -- -CREATE TYPE "Datum Kind" AS ENUM ('geodetic', 'vertical', 'temporal', 'engineering', 'dynamic geodetic', 'ensemble'); -CREATE TYPE "CRS Kind" AS ENUM ('geocentric', 'geographic 2D', 'geographic 3D', 'projected', 'vertical', 'temporal', 'compound', 'engineering', 'derived'); +CREATE TYPE "Datum Kind" AS ENUM ('geodetic', 'vertical', 'temporal', 'parametric', 'engineering', 'dynamic geodetic', 'ensemble'); +CREATE TYPE "CRS Kind" AS ENUM ('geocentric', 'geographic 2D', 'geographic 3D', 'projected', 'vertical', 'temporal', 'parametric', 'engineering', 'derived', 'compound'); CREATE TYPE "CS Kind" AS ENUM ('ellipsoidal', 'spherical', 'Cartesian', 'vertical', 'gravity-related', 'time', 'linear', 'polar', 'cylindrical', 'affine', 'ordinal'); CREATE TYPE "Supersession Type" AS ENUM ('Supersession'); CREATE TYPE "Table Name" AS ENUM
