This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 6cffa03c4c Add the SRID argument when executing a spatial function `ST_*` in a spatial database. The reprojection of geometry literal, if needed, was already handled by `Optimization`. Add special case where the geometry column declares the CRS but the literal does not. Redirect to data store listeners the logs that may occur during geometry reprojection. 6cffa03c4c is described below commit 6cffa03c4cf09898aa66ef2dc573b9a5af20e8de Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Mar 19 17:33:44 2025 +0100 Add the SRID argument when executing a spatial function `ST_*` in a spatial database. The reprojection of geometry literal, if needed, was already handled by `Optimization`. Add special case where the geometry column declares the CRS but the literal does not. Redirect to data store listeners the logs that may occur during geometry reprojection. --- .../org/apache/sis/feature/internal/Resources.java | 5 + .../sis/feature/internal/Resources.properties | 1 + .../sis/feature/internal/Resources_fr.properties | 1 + .../apache/sis/filter/BinaryGeometryFilter.java | 2 +- .../main/org/apache/sis/filter/LeafExpression.java | 2 +- .../main/org/apache/sis/filter/internal/Node.java | 24 +- .../org/apache/sis/filter/privy/WarningEvent.java | 128 +++++++++++ .../geometry/wrapper/jts/GeometryTransform.java | 10 +- .../org/apache/sis/geometry/wrapper/jts/JTS.java | 20 +- .../sis/geometry/wrapper/esri/FactoryTest.java | 8 +- .../apache/sis/geometry/wrapper/jts/JTSTest.java | 2 +- .../apache/sis/metadata/sql/privy/SQLBuilder.java | 57 +++-- .../org/apache/sis/metadata/sql/privy/Syntax.java | 6 +- .../main/org/apache/sis/openoffice/CalcAddins.java | 2 +- .../sis/storage/netcdf/base/GridMapping.java | 2 +- .../org/apache/sis/storage/sql/DataAccess.java | 2 + .../sis/storage/sql/feature/FeatureIterator.java | 26 ++- .../sis/storage/sql/feature/FeatureStream.java | 41 ++-- .../sis/storage/sql/feature/InfoStatements.java | 4 +- .../apache/sis/storage/sql/feature/Resources.java | 5 + .../sis/storage/sql/feature/Resources.properties | 1 + .../storage/sql/feature/Resources_fr.properties | 1 + .../sis/storage/sql/feature/SelectionClause.java | 250 +++++++++++++++++++-- .../storage/sql/feature/SelectionClauseWriter.java | 30 ++- .../org/apache/sis/storage/sql/feature/Table.java | 2 +- .../storage/sql/postgis/ExtendedClauseWriter.java | 5 + .../sis/storage/sql/postgis/ExtentEstimator.java | 2 +- .../storage/sql/feature/GeometryGetterTest.java | 1 + .../sis/storage/sql/postgis/PostgresTest.java | 98 ++++++-- .../sis/storage/sql/postgis/SpatialFeatures.sql | 8 + .../org/apache/sis/storage/wkt/StoreFormat.java | 2 +- .../main/org/apache/sis/setup/GeometryLibrary.java | 37 ++- .../org/apache/sis/util/collection/WeakEntry.java | 2 +- .../apache/sis/storage/geoheif/GeoHeifStore.java | 2 +- 34 files changed, 664 insertions(+), 125 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java index 3bdf50aeb5..c3ad59f00a 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java @@ -336,6 +336,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short MismatchedValueClass_3 = 48; + /** + * Mixed geometry implementations from two libraries: {0} and {1}. + */ + public static final short MixedGeometryImplementation_2 = 91; + /** * No category for value {0}. */ diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties index df2a468bba..1aeaa4f3bf 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties @@ -74,6 +74,7 @@ MismatchedPropertyType_1 = Mismatched type for \u201c{0}\u201d property MismatchedSampleModel = The two images use different sample models. MismatchedTileGrid = The two images have different tile grid. MismatchedValueClass_3 = An attribute for \u2018{1}\u2019 values where expected, but the \u201c{0}\u201d attribute specifies values of type \u2018{2}\u2019. +MixedGeometryImplementation_2 = Mixed geometry implementations from two libraries: {0} and {1}. NoCategoryForValue_1 = No category for value {0}. NoNDimensionalSlice_3 = Cannot infer a {0}-dimensional slice from the grid envelope. Dimension {1} has {2,number} cells. NonLinearInDimensions_1 = non-linear in {0} dimension{0,choice,1#|2#s}: diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties index 4dfb9f41af..b2b4f45237 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties @@ -79,6 +79,7 @@ MismatchedPropertyType_1 = Le type de la propri\u00e9t\u00e9 \u00ab\u20 MismatchedSampleModel = Les deux images disposent les pixels diff\u00e9remment. MismatchedTileGrid = Les deux images utilisent des grilles de tuiles diff\u00e9rentes. MismatchedValueClass_3 = Un attribut pour des valeurs de type \u2018{1}\u2019 \u00e9tait attendu, mais l\u2019attribut \u00ab\u202f{0}\u202f\u00bb sp\u00e9cifie des valeurs de type \u2018{2}\u2019. +MixedGeometryImplementation_2 = Les g\u00e9om\u00e9tries m\u00e9langent des impl\u00e9mentations de deux biblioth\u00e8ques: {0} et {1}. NoCategoryForValue_1 = Aucune cat\u00e9gorie n\u2019est d\u00e9finie pour la valeur {0}. NoNDimensionalSlice_3 = Ne peut pas inf\u00e9rer une tranche \u00e0 {0} dimensions \u00e0 partir de l\u2019enveloppe de la grille. La dimension {1} a {2,number} cellules. NonLinearInDimensions_1 = non-lin\u00e9aire dans {0} dimension{0,choice,1#|2#s}\u00a0: diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java index f35237d4d7..685c3ea900 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java @@ -120,7 +120,7 @@ abstract class BinaryGeometryFilter<R> extends Node implements SpatialOperator<R if (value != null) { final GeometryWrapper gt = context.transform(value); if (gt != value) { - final Expression<R, GeometryWrapper> tr = new LeafExpression.Transformed<>(gt, literal); + final var tr = new LeafExpression.Transformed<R, GeometryWrapper>(gt, literal); switch (index) { case 0: expression1 = tr; break; case 1: expression2 = tr; break; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java index 6350cc0eb6..308c9adc7c 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java @@ -243,7 +243,7 @@ abstract class LeafExpression<R,V> extends Node implements FeatureExpression<R,V try { return original.toValueType(target); } catch (RuntimeException bis) { - final ClassCastException c = new ClassCastException(Errors.format( + final var c = new ClassCastException(Errors.format( Errors.Keys.CanNotConvertValue_2, getFunctionName(), target)); c.initCause(e); c.addSuppressed(bis); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java index 9602325c2f..8afbdfd5d0 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.function.Predicate; import java.util.logging.Logger; import java.io.Serializable; +import java.util.function.Consumer; import org.opengis.util.CodeList; import org.opengis.util.LocalName; import org.opengis.util.ScopedName; @@ -31,6 +32,7 @@ import org.apache.sis.math.FunctionProperty; import org.apache.sis.feature.DefaultAttributeType; import org.apache.sis.feature.internal.Resources; import org.apache.sis.feature.privy.FeatureExpression; +import org.apache.sis.filter.privy.WarningEvent; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryWrapper; import org.apache.sis.util.iso.Names; @@ -144,7 +146,7 @@ public abstract class Node implements Serializable { * * @return the name of this function. */ - private Object getDisplayName() { + public final Object getDisplayName() { if (this instanceof Expression<?,?>) { return ((Expression<?,?>) this).getFunctionName(); } else if (this instanceof Filter<?>) { @@ -181,11 +183,12 @@ public abstract class Node implements Serializable { final Geometries<G> library, final Expression<R,?> expression) { if (expression instanceof GeometryConverter<?,?>) { - if (library.equals(((GeometryConverter<?,?>) expression).library)) { + final Geometries<?> other = ((GeometryConverter<?,?>) expression).library; + if (library.equals(other)) { return (GeometryConverter<R,G>) expression; - } else { - throw new InvalidFilterValueException(); // TODO: provide a message. } + throw new InvalidFilterValueException(Resources.format( + Resources.Keys.MixedGeometryImplementation_2, library.library, other.library)); } return new GeometryConverter<>(library, expression); } @@ -389,11 +392,16 @@ public abstract class Node implements Serializable { * @see <a href="https://issues.apache.org/jira/browse/SIS-460">SIS-460</a> */ protected final void warning(final Exception e, final boolean recoverable) { - final String method = (this instanceof Predicate) ? "test" : "apply"; - if (recoverable) { - Logging.recoverableException(LOGGER, getClass(), method, e); + final Consumer<WarningEvent> listener = WarningEvent.LISTENER.get(); + if (listener != null) { + listener.accept(new WarningEvent(this, e)); } else { - Logging.unexpectedException(LOGGER, getClass(), method, e); + final String method = (this instanceof Predicate) ? "test" : "apply"; + if (recoverable) { + Logging.recoverableException(LOGGER, getClass(), method, e); + } else { + Logging.unexpectedException(LOGGER, getClass(), method, e); + } } } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java new file mode 100644 index 0000000000..09a6f891d5 --- /dev/null +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.filter.privy; + +import java.util.Optional; +import java.util.function.Consumer; +import org.opengis.util.CodeList; +import org.opengis.util.ScopedName; +import org.apache.sis.filter.internal.Node; + +// Specific to the geoapi-3.1 and geoapi-4.0 branches: +import org.opengis.filter.Filter; +import org.opengis.filter.Expression; + + +/** + * A warning emitted during operations on filters or expressions. + * This class is a first draft that may move to public API in a future version. + * + * @author Martin Desruisseaux (Geomatys) + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-460">SIS-460</a> + */ +public final class WarningEvent { + /** + * Where to send the warning. If the value is {@code null}, + * then the warning will be logged to a default logger. + */ + public static final ThreadLocal<Consumer<WarningEvent>> LISTENER = new ThreadLocal<>(); + + /** + * The filter or expression that produced this warning. + */ + private final Node source; + + /** + * The exception that occurred. + */ + public final Exception exception; + + /** + * Creates a new warning. + * + * @param source the filter or expression that produced this warning. + * @param exception the exception that occurred. + */ + public WarningEvent(final Node source, final Exception exception) { + this.source = source; + this.exception = exception; + } + + /** + * If the source is a filter, returns the operator type. + * Otherwise, returns an empty value. + * + * @return the operator type if the source is a filter. + */ + public Optional<CodeList<?>> getOperatorType() { + if (source instanceof Filter<?>) { + return Optional.of(((Filter<?>) source).getOperatorType()); + } + return Optional.empty(); + } + + /** + * If the source is an expression, returns the function name. + * Otherwise, returns an empty value. + * + * @return the function name if the source is an expression. + */ + public Optional<ScopedName> getFunctionName() { + if (source instanceof Expression<?,?>) { + return Optional.of(((Expression<?,?>) source).getFunctionName()); + } + return Optional.empty(); + } + + /** + * If the source is an expression with at least one parameter of the given type, returns that parameter. + * If there is many parameter assignable to the given type, then the first occurrence is returned. + * The {@code type} argument is typically {@code Literal.class} or {@code ValueReference.class}. + * + * @param type the desired type of the parameter to return. + * @return the first parameter of the given type, or empty if none. + */ + @SuppressWarnings("unchecked") + public <P extends Expression<?,?>> Optional<P> getParameter(final Class<P> type) { + if (source instanceof Filter<?>) { + for (Expression<?,?> parameter : ((Filter<?>) source).getExpressions()) { + if (type.isInstance(parameter)) { + return Optional.of((P) parameter); + } + } + } + if (source instanceof Expression<?,?>) { + for (Expression<?,?> parameter : ((Expression<?,?>) source).getParameters()) { + if (type.isInstance(parameter)) { + return Optional.of((P) parameter); + } + } + } + return Optional.empty(); + } + + /** + * Returns a string representation of the warning for debugging purposes. + * + * @return a string representation of the warning. + */ + @Override + public String toString() { + return source.getDisplayName() + ": " + exception.toString(); + } +} diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java index bf638535c1..2cb88090ec 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryTransform.java @@ -104,7 +104,7 @@ public abstract class GeometryTransform { * @throws TransformException if an error occurred while transforming a geometry. */ public MultiPoint transform(final MultiPoint geom) throws TransformException { - final Point[] subs = new Point[geom.getNumGeometries()]; + final var subs = new Point[geom.getNumGeometries()]; for (int i = 0; i < subs.length; i++) { subs[i] = transform((Point) geom.getGeometryN(i)); } @@ -133,7 +133,7 @@ public abstract class GeometryTransform { * @throws TransformException if an error occurred while transforming a geometry. */ public MultiLineString transform(final MultiLineString geom) throws TransformException { - final LineString[] subs = new LineString[geom.getNumGeometries()]; + final var subs = new LineString[geom.getNumGeometries()]; for (int i = 0; i < subs.length; i++) { subs[i] = transform((LineString) geom.getGeometryN(i)); } @@ -163,7 +163,7 @@ public abstract class GeometryTransform { */ public Polygon transform(final Polygon geom) throws TransformException { final LinearRing exterior = transform(geom.getExteriorRing()); - final LinearRing[] holes = new LinearRing[geom.getNumInteriorRing()]; + final var holes = new LinearRing[geom.getNumInteriorRing()]; for (int i = 0; i < holes.length; i++) { holes[i] = transform(geom.getInteriorRingN(i)); } @@ -179,7 +179,7 @@ public abstract class GeometryTransform { * @throws TransformException if an error occurred while transforming a geometry. */ public MultiPolygon transform(final MultiPolygon geom) throws TransformException { - final Polygon[] subs = new Polygon[geom.getNumGeometries()]; + final var subs = new Polygon[geom.getNumGeometries()]; for (int i = 0; i < subs.length; i++) { subs[i] = transform((Polygon) geom.getGeometryN(i)); } @@ -195,7 +195,7 @@ public abstract class GeometryTransform { * @throws TransformException if an error occurred while transforming a geometry. */ public GeometryCollection transform(final GeometryCollection geom) throws TransformException { - final Geometry[] subs = new Geometry[geom.getNumGeometries()]; + final var subs = new Geometry[geom.getNumGeometries()]; for (int i = 0; i < subs.length; i++) { subs[i] = transform(geom.getGeometryN(i)); } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java index 36cbe002c1..7201ad9ea3 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/JTS.java @@ -30,6 +30,8 @@ import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.IdentifiedObjects; +import org.apache.sis.referencing.crs.AbstractCRS; +import org.apache.sis.referencing.cs.AxesConvention; import org.apache.sis.util.Static; import org.apache.sis.util.Utilities; import org.apache.sis.util.logging.Logging; @@ -124,28 +126,32 @@ public final class JTS extends Static { if (userData instanceof CoordinateReferenceSystem) { return (CoordinateReferenceSystem) userData; } else if (userData instanceof Map<?,?>) { - final Map<?,?> map = (Map<?,?>) userData; + final var map = (Map<?,?>) userData; final Object value = map.get(CRS_KEY); if (value instanceof CoordinateReferenceSystem) { return (CoordinateReferenceSystem) value; } } /* - * Fallback on SRID with the assumption that they are EPSG codes. + * Fallback on SRID with the assumption that they are EPSG codes except for axis order which + * is (longitude, latitude). This is the order used in popular spatial databases and is also + * the order frequently used by other libraries that use JTS. * * TODO: This is not necessarily EPSG code. We need a plugin mechanism for specifying the authority. * It may be for example the "spatial_ref_sys" table of a spatial database. */ final int srid = source.getSRID(); if (srid > 0) { - return CRS.forCode(Constants.EPSG + ':' + srid); + CoordinateReferenceSystem crs = CRS.forCode(Constants.EPSG + ':' + srid); + crs = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); + return crs; } } return null; } /** - * Sets the Coordinate Reference System (CRS) in the specified geometry. This method overwrite any previous + * Sets the Coordinate Reference System (CRS) in the specified geometry. This method overwrites any previous * user data; it should be invoked only when the geometry is known to not store any other information. * In current Apache SIS usage, this method is invoked only for newly created geometries. * @@ -155,7 +161,7 @@ public final class JTS extends Static { * @param target the geometry where to store coordinate reference system information. * @param crs the CRS to store, or {@code null}. */ - static void setCoordinateReferenceSystem(final Geometry target, final CoordinateReferenceSystem crs) { + public static void setCoordinateReferenceSystem(final Geometry target, final CoordinateReferenceSystem crs) { target.setUserData(crs); int epsg = 0; final Identifier id = IdentifiedObjects.getIdentifier(crs, Citations.EPSG); @@ -207,7 +213,7 @@ public final class JTS extends Static { bbox = new DefaultGeographicBoundingBox(); try { final Envelope e = areaOfInterest.getEnvelopeInternal(); - final GeneralEnvelope env = new GeneralEnvelope(sourceCRS); // May be 3- or 4-dimensional. + final var env = new GeneralEnvelope(sourceCRS); // May be 3- or 4-dimensional. env.setRange(0, e.getMinX(), e.getMaxX()); env.setRange(1, e.getMinY(), e.getMaxY()); bbox.setBounds(env); @@ -296,7 +302,7 @@ public final class JTS extends Static { */ public static Geometry transform(Geometry geometry, final MathTransform transform) throws TransformException { if (geometry != null && transform != null && !transform.isIdentity()) { - final GeometryCoordinateTransform gct = new GeometryCoordinateTransform(transform, geometry.getFactory()); + final var gct = new GeometryCoordinateTransform(transform, geometry.getFactory()); geometry = gct.transform(geometry); } return geometry; diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java index eaf53fa485..dfd1c6096b 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/esri/FactoryTest.java @@ -82,7 +82,7 @@ public final class FactoryTest extends GeometriesTestCase { @Override public void testCreatePolyline() { super.testCreatePolyline(); - final Polyline poly = (Polyline) geometry; + final var poly = (Polyline) geometry; assertEquals(2, poly.getPathCount()); } @@ -93,7 +93,7 @@ public final class FactoryTest extends GeometriesTestCase { @Override public void testMergePolylines() { super.testMergePolylines(); - final Polyline poly = (Polyline) geometry; + final var poly = (Polyline) geometry; assertEquals(3, poly.getPathCount()); } @@ -105,7 +105,7 @@ public final class FactoryTest extends GeometriesTestCase { @Override protected void assertWktEquals(String expected, final String actual) { assertTrue(actual.startsWith("MULTI")); - final StringBuilder b = new StringBuilder(expected.length() + 7).append("MULTI").append(expected); + final var b = new StringBuilder(expected.length() + 7).append("MULTI").append(expected); StringBuilders.replace(b, "(", "(("); StringBuilders.replace(b, ")", "))"); expected = b.toString(); @@ -121,6 +121,6 @@ public final class FactoryTest extends GeometriesTestCase { final GeometryWrapper ogw = other.castOrWrap(other.createPoint(5, 6)); assertNotNull(other.getGeometry(ogw)); var e = assertThrows(ClassCastException.class, () -> factory.getGeometry(ogw)); - assertMessageContains(e, "ESRI", "JAVA2D"); + assertMessageContains(e, "ESRI", "Java2D"); } } diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java index 6c488a1238..8aec3d6a9e 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/JTSTest.java @@ -79,7 +79,7 @@ public final class JTSTest extends TestCase { */ geometry.setUserData(null); geometry.setSRID(4326); - assertEquals(CommonCRS.WGS84.geographic(), JTS.getCoordinateReferenceSystem(geometry)); + assertEquals(CommonCRS.WGS84.normalizedGeographic(), JTS.getCoordinateReferenceSystem(geometry)); } /** diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java index e914f49685..dba6153af5 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java @@ -159,55 +159,66 @@ public class SQLBuilder extends Syntax { /** * Appends an identifier between quote characters. * - * @param identifier the identifier to append. + * @param name the identifier to append. * @return this builder, for method call chaining. */ - public final SQLBuilder appendIdentifier(final String identifier) { - buffer.append(quote).append(identifier).append(quote); + public final SQLBuilder appendIdentifier(final String name) { + buffer.append(quote).append(name).append(quote); return this; } /** * Appends an identifier for an element in the given schema. + * The following rules apply: * <ul> * <li>The given schema will be written only if non-null.</li> - * <li>The given schema will be quoted only if {@code quoteSchema} is {@code true}.</li> - * <li>The given identifier is always quoted.</li> + * <li>The given schema will be quoted only if {@link #quoteSchema} is {@code true}.</li> + * <li>The given name is always quoted.</li> * </ul> * - * @param schema the schema, or {@code null} or empty if none. - * @param identifier the identifier to append. + * @param schema the schema, or {@code null} or empty if none. + * @param name the name part of the identifier to append. * @return this builder, for method call chaining. */ - public final SQLBuilder appendIdentifier(final String schema, final String identifier) { - if (schema != null && !schema.isEmpty()) { - if (quoteSchema) { - appendIdentifier(schema); - } else { - buffer.append(schema); - } - buffer.append('.'); - } - return appendIdentifier(identifier); + public final SQLBuilder appendIdentifier(final String schema, final String name) { + return appendIdentifier(null, schema, name, true); } /** * Appends an identifier for an element in the given schema and catalog. + * The schema is quoted only if {@link #quoteSchema} is {@code true}. + * The name part is quoted only if {@code quoteName} is {@code true}. + * Unquoted names are useful when the name is for built-in functions, + * which often use the lower/upper case convention of the database. * - * @param catalog the catalog, or {@code null} or empty if none. - * @param schema the schema, or {@code null} or empty if none. - * @param identifier the identifier to append. + * @param catalog the catalog, or {@code null} or empty if none. + * @param schema the schema, or {@code null} or empty if none. + * @param name the name part of the identifier to append. + * @param quoteName whether to quote the name part. * @return this builder, for method call chaining. */ - public final SQLBuilder appendIdentifier(final String catalog, final String schema, final String identifier) { + public final SQLBuilder appendIdentifier(final String catalog, final String schema, final String name, final boolean quoteName) { if (catalog != null && !catalog.isEmpty()) { appendIdentifier(catalog); buffer.append('.'); - if (schema == null) { + if (schema == null || schema.isEmpty()) { buffer.append(quote).append(quote).append('.'); } } - return appendIdentifier(schema, identifier); + if (schema != null && !schema.isEmpty()) { + if (quoteSchema) { + appendIdentifier(schema); + } else { + buffer.append(schema); + } + buffer.append('.'); + } + if (quoteName) { + return appendIdentifier(name); + } else { + buffer.append(name); + return this; + } } /** 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 3db59a8239..335aa2f276 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 @@ -41,7 +41,11 @@ public class Syntax { /** * Whether the schema name should be written between quotes. If {@code false}, - * we will let the database engine uses its default lower case / upper case policy. + * Apache SIS lets the database engine uses its default lower case / upper case policy. + * This flag is usually {@code true} when the schema was specified by the user or has + * been discovered from database metadata. This flag is {@code false} when the schema + * has been created by an Apache SIS script, which intentionally uses unquoted schema + * for integration with database conventions. * * @see SQLBuilder#appendIdentifier(String, String) */ diff --git a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java index f98133f25d..d597cf0335 100644 --- a/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java +++ b/endorsed/src/org.apache.sis.openoffice/main/org/apache/sis/openoffice/CalcAddins.java @@ -190,7 +190,7 @@ public abstract class CalcAddins extends WeakBase implements XServiceName, XServ final Logger logger = getLogger(); final LogRecord record = new LogRecord(Level.WARNING, getLocalizedMessage(exception)); record.setLoggerName(logger.getName()); - record.setSourceClassName(getClass().getName()); + record.setSourceClassName(getClass().getCanonicalName()); record.setSourceMethodName(method); record.setThrown(exception); logger.log(record); diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java index 70412a7534..45015c40de 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java @@ -574,7 +574,7 @@ final class GridMapping { if (warnings != null) { final var record = new LogRecord(Level.WARNING, warnings.toString()); record.setLoggerName(Modules.NETCDF); - record.setSourceClassName(Variable.class.getCanonicalName()); + record.setSourceClassName(Variable.class.getName()); record.setSourceMethodName("getGridGeometry"); mapping.decoder.listeners.warning(record); } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java index 1b7aeee962..bed8c9d194 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/DataAccess.java @@ -90,6 +90,8 @@ public class DataAccess implements AutoCloseable { /** * Helper methods for fetching information such as coordinate reference systems. * Created when first needed. + * + * @see #spatialInformation() */ private InfoStatements spatialInformation; diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java index 13be593b51..450f625a5b 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java @@ -25,7 +25,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.sis.metadata.sql.privy.SQLBuilder; -import org.apache.sis.storage.InternalDataStoreException; import org.apache.sis.util.collection.WeakValueHashMap; // Specific to the geoapi-3.1 and geoapi-4.0 branches: @@ -109,20 +108,29 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable { * @param table the source table. * @param connection connection to the database, used for creating the statement. * @param distinct whether the set should contain distinct feature instances. - * @param filter condition to append, not including the {@code WHERE} keyword. + * @param selection condition to append, not including the {@code WHERE} keyword. * @param sort the {@code ORDER BY} clauses, or {@code null} if none. * @param offset number of rows to skip in underlying SQL query, or ≤ 0 for none. * @param count maximum number of rows to return, or ≤ 0 for no limit. */ - FeatureIterator(final Table table, final Connection connection, - final boolean distinct, final String filter, final SortBy<? super Feature> sort, - final long offset, final long count) - throws SQLException, InternalDataStoreException + FeatureIterator(final Table table, + final Connection connection, + final boolean distinct, + final SelectionClause selection, + final SortBy<? super Feature> sort, + final long offset, + final long count) + throws Exception { adapter = table.adapter(connection); - spatialInformation = table.database.getSpatialSchema().isPresent() - ? table.database.createInfoStatements(connection) : null; - String sql = adapter.sql; + String sql = adapter.sql; // Will be completed below with `WHERE` clause if needed. + + if (table.database.getSpatialSchema().isPresent()) { + spatialInformation = table.database.createInfoStatements(connection); + } else { + spatialInformation = null; + } + final String filter = (selection != null) ? selection.query(connection, spatialInformation) : null; if (distinct || filter != null || sort != null || offset > 0 || count > 0) { final var builder = new SQLBuilder(table.database).append(sql); if (distinct) { diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java index c24daa5e88..1cbd99ae93 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java @@ -35,6 +35,7 @@ import org.apache.sis.util.privy.Strings; import org.apache.sis.util.stream.DeferredStream; import org.apache.sis.util.stream.PaginedStream; import org.apache.sis.filter.privy.SortByComparator; +import org.apache.sis.filter.privy.WarningEvent; import org.apache.sis.storage.DataStoreException; // Specific to the geoapi-3.1 and geoapi-4.0 branches: @@ -177,17 +178,22 @@ final class FeatureStream extends DeferredStream<Feature> { * if we have a "F₀ AND F₁ AND F₂" chain, it is possible to have some Fₙ as SQL statements and * other Fₙ executed in Java code. */ - final Optimization optimization = new Optimization(); - optimization.setFeatureType(table.featureType); Stream<Feature> stream = this; - for (final Filter<? super Feature> filter : optimization.applyAndDecompose((Filter<? super Feature>) predicate)) { - if (filter == Filter.include()) continue; - if (filter == Filter.exclude()) return empty(); - if (!selection.tryAppend(filterToSQL, filter)) { - // Delegate to Java code all filters that we cannot translate to SQL statement. - stream = super.filter(filter); - hasPredicates = true; + try { + WarningEvent.LISTENER.set(selection); + final var optimization = new Optimization(); + optimization.setFeatureType(table.featureType); + for (final Filter<? super Feature> filter : optimization.applyAndDecompose((Filter<? super Feature>) predicate)) { + if (filter == Filter.include()) continue; + if (filter == Filter.exclude()) return empty(); + if (!selection.tryAppend(filterToSQL, filter)) { + // Delegate to Java code all filters that we cannot translate to SQL statement. + stream = super.filter(filter); + hasPredicates = true; + } } + } finally { + WarningEvent.LISTENER.remove(); } return stream; } @@ -323,12 +329,15 @@ final class FeatureStream extends DeferredStream<Feature> { sql.appendIdentifier(table.attributes[0].label); } table.appendFromClause(sql.append(')')); - if (selection != null && !selection.isEmpty()) { - sql.append(" WHERE ").append(selection.toString()); - } lock(table.database.transactionLocks); try (Connection connection = getConnection()) { makeReadOnly(connection); + if (selection != null) { + final String filter = selection.query(connection, null); + if (filter != null) { + sql.append(" WHERE ").append(filter); + } + } try (Statement st = connection.createStatement(); ResultSet rs = st.executeQuery(sql.toString())) { @@ -337,7 +346,7 @@ final class FeatureStream extends DeferredStream<Feature> { if (!rs.wasNull()) return n; } } - } catch (SQLException e) { + } catch (Exception e) { throw cannotExecute(e); } finally { unlock(); @@ -394,15 +403,13 @@ final class FeatureStream extends DeferredStream<Feature> { */ @Override protected Spliterator<Feature> createSourceIterator() throws Exception { - final String filter = (selection != null && !selection.isEmpty()) ? selection.toString() : null; - selection = null; // Let the garbage collector do its work. - lock(table.database.transactionLocks); final Connection connection = getConnection(); setCloseHandler(connection); // Executed only if `FeatureIterator` creation fails, discarded later otherwise. makeReadOnly(connection); - final var features = new FeatureIterator(table, connection, distinct, filter, sort, offset, count); + final var features = new FeatureIterator(table, connection, distinct, selection, sort, offset, count); setCloseHandler(features); + selection = null; // Let the garbage collector do its work. return features; } 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 f8590e8d43..34d4a25d5c 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 @@ -489,7 +489,7 @@ public class InfoStatements implements Localized, AutoCloseable { private void log(final String method, final LogRecord warning) { if (warning != null) { warning.setLoggerName(Modules.SQL); - warning.setSourceClassName(getClass().getName()); + warning.setSourceClassName(getClass().getCanonicalName()); warning.setSourceMethodName(method); database.listeners.warning(warning); } @@ -522,7 +522,7 @@ public class InfoStatements implements Localized, AutoCloseable { * responsible for holding a lock. It may be a read lock or write lock depending * on the {@link Connection#isReadOnly()} value. * - * @param crs the CRS for which to find a SRID, or {@code null}. + * @param crs the CRS for which to find a SRID, or {@code null}. * @return SRID for the given CRS, or 0 if the given CRS was null. * @throws Exception if an SQL error, parsing error or other error occurred. */ diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java index 534146c158..7ea58675af 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java @@ -93,6 +93,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short IllegalQualifiedName_1 = 3; + /** + * The literal of function “{0}” is not compatible with the reference system of property “{1}”. + */ + public static final short IncompatibleLiteralCRS_2 = 18; + /** * Unexpected error while analyzing the database schema. */ diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties index 77620d600b..1060cb1e1c 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties @@ -27,6 +27,7 @@ DataSource = Provider of connections to the database. DuplicatedColumn_1 = Unexpected duplication of column named \u201c{0}\u201d. DuplicatedSRID_2 = Spatial Reference Identifier (SRID) {1} has more than one entry in \u201c{0}\u201d table. IllegalQualifiedName_1 = \u201c{0}\u201d is not a valid qualified name for a table. +IncompatibleLiteralCRS_2 = The literal of function \u201c{0}\u201d is not compatible with the reference system of property \u201c{1}\u201d. InternalError = Unexpected error while analyzing the database schema. MalformedForeignerKey_2 = Unexpected column \u201c{1}\u201d in the \u201c{0}\u201d foreigner key. MappedSQLQueries = Resource names mapped to SQL queries. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties index 437f789ad5..3d6a4a87f0 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties @@ -32,6 +32,7 @@ DataSource = Fournisseur de connexions \u00e0 la base de DuplicatedColumn_1 = Doublon inattendu d\u2019une colonne nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb. DuplicatedSRID_2 = L\u2019identifiant de r\u00e9f\u00e9rence spatiale (SRID) {1} a plusieurs entr\u00e9s dans la table \u00ab\u202f{0}\u202f\u00bb. IllegalQualifiedName_1 = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un nom qualifi\u00e9 de table valide. +IncompatibleLiteralCRS_2 = Le litt\u00e9ral de la fonction \u00ab\u202f{0}\u202f\u00bb n'est pas compatible avec le syst\u00e8me de r\u00e9f\u00e9rence de la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb. InternalError = Erreur inattendue pendant l\u2019analyse du sch\u00e9ma de la base de donn\u00e9es. MalformedForeignerKey_2 = Colonne \u00ab\u202f{1}\u202f\u00bb inattendue dans la cl\u00e9 \u00e9trang\u00e8re \u00ab\u202f{0}\u202f\u00bb. MappedSQLQueries = Noms de ressources associ\u00e9s \u00e0 des requ\u00eates SQL. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java index 819d204b07..aa0d7a92ab 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java @@ -16,14 +16,29 @@ */ package org.apache.sis.storage.sql.feature; +import java.util.Map; +import java.util.AbstractMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.function.Consumer; +import java.sql.Connection; +import org.opengis.util.CodeList; import org.opengis.geometry.Envelope; -import org.opengis.geometry.Geometry; import org.opengis.metadata.extent.GeographicBoundingBox; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.WraparoundMethod; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryWrapper; import org.apache.sis.metadata.sql.privy.SQLBuilder; +import org.apache.sis.filter.privy.WarningEvent; +import org.apache.sis.storage.FeatureSet; +import org.apache.sis.system.Modules; +import org.apache.sis.util.Utilities; +import org.apache.sis.util.Workaround; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.feature.Feature; @@ -37,20 +52,68 @@ import org.opengis.filter.ValueReference; * @author Alexis Manin (Geomatys) * @author Martin Desruisseaux (Geomatys) */ -public final class SelectionClause extends SQLBuilder { +public final class SelectionClause extends SQLBuilder implements Consumer<WarningEvent> { + /** + * Whether the database rejects spatial functions that mix geometries with and without <abbr>CRS</abbr>. + * We observed that PostGIS 3.4 produces an error not only when the geometry operands have different CRS, + * but also when one operand has a CRS and the other operand has no explicit CRS. Example: + * + * <blockquote>Operation on mixed SRID geometries (Polygon, 4326) != (Polygon, 0)</blockquote> + * + * As a workaround, if the literal has no CRS, we assume that the CRS of the geometry column was implied. + * In such cases, the SRID 0 is replaced by the SRID of the geometry column. Current version applies this + * workaround for all databases, but we could make this field non-static if a future version of Apache SIS + * decides to apply this policy on a case-by-case basis. + * + * <p>Note that above error is not always visible. For example, when using {@code ST_Intersects}, + * PostGIS first performs a quick filtering based on bounding boxes using the {@code &&} operator. + * But that operator does not check the <abbr>SRID</abbr>. Therefore, no error message is raised + * when the bounding boxes of the geometries do not intersect.</p> + */ + @Workaround(library = "PostGIS", version = "3.4") + static final boolean REPLACE_UNSPECIFIED_CRS = true; + /** * The table or view for which to create a SQL statement. */ private final Table table; + /** + * The parameters to set after the connection has been established. + * For each entry, the key is the index in {@link #buffer} of the parameter to set + * and the value is the object to convert to a value that can be set in the query. + * The following values need to be converted: + * + * <ul> + * <li>Instances of {@link CoordinateReferenceSystem} shall be replaced by <abbr>SRID</abbr>.</li> + * </ul> + * + * Elements must be sorted in increasing order of keys. + * + * @see #query(InfoStatements) + */ + private final List<Map.Entry<Integer, CoordinateReferenceSystem>> parameters; + + /** + * The coordinate reference system of the geometry columns referenced by the spatial functions to write. + * This is {@code null} when not writing a spatial function, or if the function does not reference any + * geometry column, or if it references more than one geometry column with different <abbr>CRS</abbr>. + * If non-null, the optional may be empty if the geometry column does not declare a reference system. + * In the latter case (empty), each row of the table can be a geometry in a different <abbr>CRS</abbr>. + * + * @see #REPLACE_UNSPECIFIED_CRS + */ + private Optional<CoordinateReferenceSystem> columnCRS; + /** * Flag sets to {@code true} if a filter or expression cannot be converted to SQL. * When a SQL string become flagged as invalid, it is truncated to the length that * it had the last time that it was valid. * * @see #invalidate() + * @see #isInvalid() */ - boolean isInvalid; + private boolean isInvalid; /** * Creates a new builder for the given table. @@ -60,6 +123,39 @@ public final class SelectionClause extends SQLBuilder { SelectionClause(final Table table) { super(table.database); this.table = table; + parameters = new ArrayList<>(); + } + + /** + * Clears any <abbr>CRS</abbr> that was configured by {@code acceptColumnCRS(…)}. + * This method should be invoked after the caller finished to write a spatial function. + */ + final void clearColumnCRS() { + columnCRS = null; + } + + /** + * If the referenced column is a geometry or raster column, remembers its default coordinate reference system. + * This method can be invoked before {@link #appendLiteral(Object)} in order to set a default <abbr>CRS</abbr> + * for literals that do not declare themselves their <abbr>CRS</abbr>. + * + * @param ref reference to a property to insert in SQL statement. + * @return whether the caller needs to stop the check of operands. + * + * @see #REPLACE_UNSPECIFIED_CRS + */ + final boolean acceptColumnCRS(final ValueReference<Feature,?> ref) { + final Column c = table.getColumn(ref.getXPath()); + if (c != null && c.getGeometryType().isPresent()) { + final Optional<CoordinateReferenceSystem> crs = c.getDefaultCRS(); + if (columnCRS == null) { + columnCRS = crs; // May be empty, which is not the same as null for this class. + } else if (!Utilities.equalsIgnoreMetadata(columnCRS.orElse(null), crs.orElse(null))) { + clearColumnCRS(); + return true; + } + } + return false; } /** @@ -84,10 +180,10 @@ public final class SelectionClause extends SQLBuilder { appendGeometry(null, new GeneralEnvelope((GeographicBoundingBox) value)); } else if (value instanceof Envelope) { appendGeometry(null, (Envelope) value); - } else if (value instanceof Geometry) { - appendGeometry(Geometries.wrap((Geometry) value).orElse(null), null); } else { - appendValue(value); + Geometries.wrap(value).ifPresentOrElse( + (wrapper) -> appendGeometry(wrapper, null), + () -> appendValue(value)); } } @@ -111,9 +207,14 @@ public final class SelectionClause extends SQLBuilder { invalidate(); return; } + /* + * Get the average span of the geometry. It will be used for computing a flatness factor. + * It does not matter if the span is inaccurate, as the flatness factor is only a hint + * ignored by most geometry libraries. + */ final double span = (bounds.getSpan(0) + bounds.getSpan(1)) / Geometries.BIDIMENSIONAL; if (Double.isNaN(span)) { - final GeneralEnvelope e = new GeneralEnvelope(bounds); + final var e = new GeneralEnvelope(bounds); for (int i=0; i<Geometries.BIDIMENSIONAL; i++) { final double lower = clampInfinity(e.getLower(i)); final double upper = clampInfinity(e.getUpper(i)); @@ -128,8 +229,40 @@ public final class SelectionClause extends SQLBuilder { if (wrapper == null) { wrapper = table.database.geomLibrary.toGeometry2D(bounds, WraparoundMethod.SPLIT); } - final String wkt = wrapper.formatWKT(0.05 * span); - append("ST_GeomFromText(").appendValue(wkt).append(')'); + final String wkt = wrapper.formatWKT(0.05 * span); // Arbitrary flateness factor. + /* + * Format a spatial function for building the geometry from the Well-Known Text. + * The CRS, if available, while be specified as a SRID if the spatial support has + * been recognized (otherwise we cannot map the CRS to the database-dependent SRID). + */ + appendSpatialFunction("ST_GeomFromText"); + append('(').appendValue(wkt); + if (table.database.getSpatialSchema().isPresent()) { + CoordinateReferenceSystem crs = wrapper.getCoordinateReferenceSystem(); + if (REPLACE_UNSPECIFIED_CRS && columnCRS != null) { + if (crs == null) { + crs = columnCRS.orElse(null); + } else { + /* + * If `columnCRS` is empty, then we have a geometry column without CRS (SRID = 0). + * For making the literal consistent with the column, we could set `crs` to null. + * However, while the column has no CRS, the geometry instances on each row may have a CRS + * and clearing the literal CRS will result in "Operation on mixed SRID geometries" error. + * + * The opposite problem also exists: we could really have no CRS at all, neither in the column + * and in the geometry instances. In such case, not clearing the CRS may also cause above error. + * We have no easy way to determine if we should clear the CRS or not. The conservative approach + * applied for now is to leave the literal as the user specified it. + */ + } + } + if (crs != null) { + buffer.append(", "); + parameters.add(new AbstractMap.SimpleEntry<>(buffer.length(), crs)); + buffer.append('?'); + } + } + append(')'); } /** @@ -142,11 +275,16 @@ public final class SelectionClause extends SQLBuilder { } /** - * Declares the SQL as invalid. It does not means that the whole SQL needs to be discarded. - * The SQL may be truncated to the last point where it was considered valid. + * Appends the name of a spatial function. The catalog and schema names are + * included for making sure that it works even if the search path is not set. + * The function name is written without quotes, because the functions kept by + * {@link SelectionClauseWriter#removeUnsupportedFunctions(Database)} use the + * case convention of the database. + * + * @param name name of the spatial function to append. */ - final void invalidate() { - isInvalid = true; + final void appendSpatialFunction(final String name) { + appendIdentifier(table.database.catalogOfSpatialTables, table.database.schemaOfSpatialTables, name, false); } /** @@ -170,4 +308,90 @@ public final class SelectionClause extends SQLBuilder { } return true; } + + /** + * Returns whether an error occurred while writing the <abbr>SQL</abbr> statement. + * If this method returns {@code true}, then the caller should truncate the SQL to + * the last length which was known to be valid and fallback on Java code for the rest. + */ + final boolean isInvalid() { + return isInvalid; + } + + /** + * Declares the SQL as invalid. It does not means that the whole SQL needs to be discarded. + * The SQL may be truncated to the last point where it was considered valid. + */ + final void invalidate() { + isInvalid = true; + } + + /** + * Returns the localized resources for warnings and error messages. + */ + private Resources resources() { + return Resources.forLocale(table.database.listeners.getLocale()); + } + + /** + * Sets the logger, class and method names of the given record, then logs it. + * This method declares {@link FeatureSet#features(boolean)} as the public source of the log. + * + * @param record the record to configure and log. + */ + private void log(final LogRecord record) { + record.setSourceClassName(FeatureSet.class.getName()); + record.setSourceMethodName("features"); + record.setLoggerName(Modules.SQL); + table.database.listeners.warning(record); + } + + /** + * Invoked when a warning occurred during operations on filters or expressions. + * + * @param event the warning. + */ + @Override + public void accept(final WarningEvent event) { + final LogRecord record = resources().getLogRecord( + Level.WARNING, + Resources.Keys.IncompatibleLiteralCRS_2, + event.getOperatorType().flatMap(CodeList::identifier).orElse("?"), + event.getParameter(ValueReference.class).map(ValueReference<?,?>::getXPath).orElse("?")); + record.setThrown(event.exception); + log(record); + } + + /** + * Returns the <abbr>SQL</abbr> fragment built by this {@code SelectionClause}. + * This method completes the information that we deferred until a connection is established. + * + * @param spatialInformation a cache of statements for fetching spatial information, or {@code null}. + * @return the <abbr>SQL</abbr> fragment, or {@code null} if there is no {@code WHERE} clause to add. + * @throws Exception if an SQL error, parsing error or other error occurred. + */ + final String query(final Connection connection, InfoStatements spatialInformation) throws Exception { + if (isEmpty()) { + return null; + } + boolean close = false; + for (int i = parameters.size(); --i >= 0;) { + if (spatialInformation == null) { + spatialInformation = table.database.createInfoStatements(connection); + close = true; + } + final var entry = parameters.get(i); + final int index = entry.getKey(); + final int srid = spatialInformation.findSRID(entry.getValue()); + buffer.replace(index, index + 1, Integer.toString(srid)); + } + if (close) { + /* + * We could put this in a `finally` block, but this method is already invoked + * in a context where the caller will close the connection in case of failure. + */ + spatialInformation.close(); + } + return buffer.toString(); + } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java index 208fb33c89..a9e96dd22c 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java @@ -231,7 +231,7 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { @SuppressWarnings("unchecked") final boolean write(final SelectionClause sql, final Filter<? super Feature> filter) { visit((Filter<Feature>) filter, sql); - return sql.isInvalid; + return sql.isInvalid(); } /** @@ -243,7 +243,7 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { */ private boolean write(final SelectionClause sql, final Expression<Feature, ?> expression) { visit(expression, sql); - return sql.isInvalid; + return sql.isInvalid(); } /** @@ -387,7 +387,8 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { /** * Appends a function name with an arbitrary number of parameters (potentially zero). * This method stops immediately if a parameter cannot be expressed in SQL, leaving - * the trailing part of the SQL in an invalid state. + * the trailing part of the SQL in an invalid state. Callers should check if this is + * the case by invoking {@link SelectionClause#isInvalid()} after this method call. */ private final class Function implements BiConsumer<Filter<Feature>, SelectionClause> { /** Name the function. */ @@ -398,10 +399,27 @@ public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> { this.name = name; } - /** Writes the function as an SQL statement. */ + /** + * Writes the function as an SQL statement. The function is usually spatial (with geometry operands), + * but not necessarily. If the given {@code filter} contains geometry operands specified as literal, + * {@link org.apache.sis.filter.Optimization} should have already transformed the literals to the CRS + * of the geometry column when those CRS are known. Therefore, it should not be needed to perform any + * geometry transformation in this method. + */ @Override public void accept(final Filter<Feature> filter, final SelectionClause sql) { - sql.append(name); - writeParameters(sql, filter.getExpressions(), ", ", false); + sql.appendSpatialFunction(name); + final List<Expression<Feature, ?>> expressions = filter.getExpressions(); + if (SelectionClause.REPLACE_UNSPECIFIED_CRS) { + for (Expression<Feature,?> exp : expressions) { + if (exp instanceof ValueReference<?,?>) { + if (sql.acceptColumnCRS((ValueReference<Feature,?>) exp)) { + break; + } + } + } + } + writeParameters(sql, expressions, ", ", false); + sql.clearColumnCRS(); } } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java index 932e3fde96..13856b4c89 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java @@ -421,7 +421,7 @@ final class Table extends AbstractFeatureSet { if (query != null) { sql.append('(').append(query).append(") AS USER_QUERY"); } else { - sql.appendIdentifier(name.catalog, name.schema, name.table); + sql.appendIdentifier(name.catalog, name.schema, name.table, true); } } diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java index f2e40cd0b2..0e0ee0504b 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtendedClauseWriter.java @@ -26,6 +26,11 @@ import org.opengis.filter.SpatialOperatorName; * Converter from filters/expressions to the {@code WHERE} part of SQL statement * with PostGIS-specific syntax where useful. * + * This class adds the search by bounding box using the {@code &&} operator. + * Note that contrarily to standard operators such as {@code ST_Intersects}, + * the {@code &&} operator does not verify the <abbr>CRS</abbr>. + * No error message is raised is case of mismatched CRS. + * * @author Alexis Manin (Geomatys) */ final class ExtendedClauseWriter extends SelectionClauseWriter { diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java index 5865e933c1..d11dd644fb 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/postgis/ExtentEstimator.java @@ -98,7 +98,7 @@ final class ExtentEstimator { GeneralEnvelope estimate(final Statement statement, final boolean recall) throws SQLException { query(statement); if (envelope == null && !recall) { - builder.append("ANALYZE ").appendIdentifier(table.catalog, table.schema, table.table); + builder.append("ANALYZE ").appendIdentifier(table.catalog, table.schema, table.table, true); final String sql = builder.toString(); builder.clear(); statement.execute(sql); diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java index 3158ada9a8..9e13d21269 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/GeometryGetterTest.java @@ -162,6 +162,7 @@ public final class GeometryGetterTest extends TestCase { public static CoordinateReferenceSystem getExpectedCRS(final int srid) throws FactoryException { final String code; switch (srid) { + case 0: return null; case 3395: code = "EPSG:3395"; break; case 4326: return CommonCRS.WGS84.normalizedGeographic(); default: throw new AssertionError(srid); diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java index 2babdb9146..f9ed98cc6b 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java @@ -30,6 +30,8 @@ import java.nio.channels.ReadableByteChannel; import java.lang.reflect.Method; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Coordinate; import org.opengis.geometry.Envelope; import org.opengis.metadata.Metadata; import org.opengis.metadata.identification.Identification; @@ -46,7 +48,9 @@ import org.apache.sis.storage.sql.SQLStoreProvider; import org.apache.sis.storage.sql.SimpleFeatureStore; import org.apache.sis.storage.sql.ResourceDefinition; import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.io.stream.ChannelDataInput; +import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.sql.feature.BinaryEncoding; import org.apache.sis.geometry.wrapper.jts.JTS; import org.apache.sis.referencing.CommonCRS; @@ -129,8 +133,8 @@ public final class PostgresTest extends TestCase { database.executeSQL(List.of(resource("SpatialFeatures.sql"))); final var connector = new StorageConnector(database.source); connector.setOption(OptionKey.GEOMETRY_LIBRARY, GeometryLibrary.JTS); - final ResourceDefinition table = ResourceDefinition.table(null, SQLStoreTest.SCHEMA, "SpatialData"); - try (SimpleFeatureStore store = new SimpleFeatureStore(new SQLStoreProvider(), connector, table)) { + final var table = ResourceDefinition.table(null, SQLStoreTest.SCHEMA, "SpatialData"); + try (var store = new SimpleFeatureStore(new SQLStoreProvider(), connector, table)) { validate(store.getMetadata()); /* * Invoke the private `model()` method. We have to use reflection because the class @@ -151,12 +155,9 @@ public final class PostgresTest extends TestCase { * Tests through public API. */ final FeatureSet resource = store.findResource("SpatialData"); - try (Stream<Feature> features = resource.features(false)) { - features.forEach(PostgresTest::validate); - } - final Envelope envelope = resource.getEnvelope().get(); - assertEquals(envelope.getMinimum(0), -72, 1); - assertEquals(envelope.getMaximum(1), 43, 1); + testAllFeatures(resource); + testFilteredFeatures(resource, false); + testFilteredFeatures(resource, true); } } } @@ -183,7 +184,7 @@ public final class PostgresTest extends TestCase { * @throws Exception if an error occurred while testing the database. */ private static void testGeometryGetter(final ExtendedInfo info, final Connection connection) throws Exception { - final GeometryGetterTest test = new GeometryGetterTest(); + final var test = new GeometryGetterTest(); test.testFromDatabase(connection, info, BinaryEncoding.HEXADECIMAL); } @@ -193,8 +194,8 @@ public final class PostgresTest extends TestCase { private static void testRasterReader(final TestRaster test, final ExtendedInfo info, final Connection connection) throws Exception { - final BinaryEncoding encoding = BinaryEncoding.HEXADECIMAL; - final RasterReader reader = new RasterReader(info); + final var encoding = BinaryEncoding.HEXADECIMAL; + final var reader = new RasterReader(info); try (PreparedStatement stmt = connection.prepareStatement("SELECT image FROM features.\"SpatialData\" WHERE filename=?")) { stmt.setString(1, test.filename); final ResultSet r = stmt.executeQuery(); @@ -206,6 +207,62 @@ public final class PostgresTest extends TestCase { } } + /** + * Tests iterating over all features without filters. + * Opportunistically verifies also the bounding boxes of all features. + * + * @param resource the set of all features. + * @throws DataStoreException if an error occurred while fetching the envelope of feature instances. + */ + private static void testAllFeatures(final FeatureSet resource) throws DataStoreException { + final Envelope envelope = resource.getEnvelope().get(); + assertEquals(-72, envelope.getMinimum(0), 1); + assertEquals( 43, envelope.getMaximum(1), 1); + try (Stream<Feature> features = resource.features(false)) { + features.forEach(PostgresTest::validate); + } + try (Stream<Feature> features = resource.features(false)) { + assertEquals(8, features.count()); + } + } + + /** + * Tests iterating over features with a filter applied, with our without coordinate operation. + * The filter is {@code ST_Intersects(geometry, literal)} where the literal is a polygon. + * The coordinate operation is simply an axis swapping. + * + * @param resource the set of all features. + * @param transform whether to apply a coordinate operation. + * @throws Exception if an error occurred while fetching the envelope of feature instances. + */ + private static void testFilteredFeatures(final FeatureSet resource, final boolean transform) throws Exception { + final Coordinate[] coordinates = { + new Coordinate(-72, 42), + new Coordinate(-71, 42), + new Coordinate(-71, 43), + new Coordinate(-72, 43), + new Coordinate(-72, 42) + }; + if (transform) { + for (Coordinate c : coordinates) { + double swp = c.x; + c.x = c.y; + c.y = swp; + } + } + final Geometry geom = new GeometryFactory().createPolygon(coordinates); + if (transform) { + JTS.setCoordinateReferenceSystem(geom, CommonCRS.WGS84.geographic()); + } else { + geom.setSRID(4326); + } + final var factory = DefaultFilterFactory.forFeatures(); + final var filter = factory.intersects(factory.property("geom4326"), factory.literal(geom)); + try (Stream<Feature> features = resource.features(false).filter(filter)) { + assertEquals(4, features.count()); + } + } + /** * Invoked for each feature instances for performing some checks on the feature. * This method performs only a superficial verification of geometries. @@ -222,8 +279,15 @@ public final class PostgresTest extends TestCase { assertSame(CommonCRS.WGS84.normalizedGeographic(), raster.getCoordinateReferenceSystem()); return; } + case "point-nocrs": { + var p = (Point) geometry; + assertEquals(3, p.getX()); + assertEquals(4, p.getY()); + geomSRID = 0; + break; + } case "point-prj": { - final Point p = (Point) geometry; + var p = (Point) geometry; assertEquals(2, p.getX()); assertEquals(3, p.getY()); geomSRID = 3395; @@ -239,9 +303,13 @@ public final class PostgresTest extends TestCase { try { final CoordinateReferenceSystem expected = GeometryGetterTest.getExpectedCRS(geomSRID); final CoordinateReferenceSystem actual = JTS.getCoordinateReferenceSystem(geometry); - assertNotNull(actual); - if (expected != null) { - assertEquals(expected, actual); + if (geomSRID == 0) { + assertNull(actual); + } else { + assertNotNull(actual); + if (expected != null) { + assertEquals(expected, actual); + } } } catch (FactoryException e) { throw new AssertionError(e); diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql index 2dec522994..f6853ae912 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/SpatialFeatures.sql @@ -11,6 +11,7 @@ SET search_path TO public; CREATE TABLE features."SpatialData" ( "filename" VARCHAR(20) NOT NULL, "geometry" GEOMETRY, + "geom4326" GEOMETRY(Geometry, 4326), "image" RASTER, CONSTRAINT "PK_SpatialData" PRIMARY KEY ("filename") @@ -32,6 +33,7 @@ INSERT INTO features."SpatialData" ("filename", "image") VALUES -- Geometries with arbitrary coordinate values. -- INSERT INTO features."SpatialData" ("filename", "geometry") VALUES + ('point-nocrs', ST_GeomFromText('POINT(3 4)')), ('point-prj', ST_GeomFromText('POINT(2 3)', 3395)), ('linestring', ST_GeomFromText('LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)', 4326)), ('polygon-prj', ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))', 3395)), @@ -60,6 +62,12 @@ INSERT INTO features."SpatialData" ("filename", "geometry") VALUES || '-71.1043850704575 42.3150793250568,-71.1043632495873 42.315113108546)))', 4326)); +-- +-- A column where all geometries are in the same CRS. +-- +UPDATE features."SpatialData" SET geom4326 = geometry WHERE filename NOT LIKE '%-prj'; + + -- -- Geometries with WKT representation in one column and WKB in another column. -- Used for parsing the same geometry in two ways and comparing the results. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java index 4262b5461d..fc3a29b6d7 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreFormat.java @@ -171,7 +171,7 @@ public final class StoreFormat extends WKTFormat { private void log(final Exception e) { final LogRecord record = Resources.forLocale(listeners.getLocale()) .getLogRecord(Level.WARNING, Resources.Keys.CanNotReadCRS_WKT_1, listeners.getSourceName()); - record.setSourceClassName(listeners.getSource().getClass().getName()); + record.setSourceClassName(listeners.getSource().getClass().getCanonicalName()); record.setSourceMethodName("getMetadata"); record.setLoggerName(Loggers.WKT); listeners.warning(record); diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java index d11db13b12..acc07cbf18 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/setup/GeometryLibrary.java @@ -28,7 +28,7 @@ import org.opengis.metadata.acquisition.GeometryType; * All those libraries are optional. * * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * * @see OptionKey#GEOMETRY_LIBRARY * @see org.apache.sis.feature.builder.FeatureTypeBuilder#addAttribute(GeometryType) @@ -52,7 +52,7 @@ public enum GeometryLibrary { * Note that contrarily to JTS and ESRI libraries, * a point does not extend any root geometry class in Java2D. */ - JAVA2D, + JAVA2D("Java2D"), /** * The ESRI geometry API library. This library can be used for spatial vector data processing. @@ -70,7 +70,7 @@ public enum GeometryLibrary { * * @see <a href="https://github.com/Esri/geometry-api-java/wiki">API wiki page</a> */ - ESRI, + ESRI("ESRI"), /** * The Java Topology Suite (JTS) library. This open source library provides an object model @@ -90,7 +90,7 @@ public enum GeometryLibrary { * * @since 1.0 */ - JTS, + JTS("JTS"), /** * The GeoAPI geometry interfaces. @@ -111,5 +111,32 @@ public enum GeometryLibrary { * * @since 1.4 */ - GEOAPI + GEOAPI("GeoAPI"); + + /** + * Human-readable name of this library. + */ + private final String name; + + /** + * Creates a new enumeration value. + * + * @param name human-readable name of this library. + */ + private GeometryLibrary(final String name) { + this.name = name; + } + + /** + * Returns the name of this geometry library in a way suitable to user interfaces. + * This is the same as {@link #name()} but sometime with different cases. + * For example, {@link #JAVA2D} is shown as {@code "Java2D"}. + * + * @return human-readable name of this library. + * + * @since 1.5 + */ + public String toString() { + return name; + } } diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java index 1e2d88e2a1..cf021c4af3 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/WeakEntry.java @@ -173,7 +173,7 @@ abstract class WeakEntry<E> extends WeakReference<E> implements Disposable { final LogRecord record = Messages.forLocale(null).getLogRecord(Level.FINEST, Messages.Keys.ChangedContainerCapacity_2, oldTable.length, table.length); record.setSourceMethodName(callerMethod); - record.setSourceClassName(entryType.getEnclosingClass().getName()); + record.setSourceClassName(entryType.getEnclosingClass().getCanonicalName()); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } diff --git a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java index 530ceb6218..4a917c00f1 100644 --- a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java +++ b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/GeoHeifStore.java @@ -318,7 +318,7 @@ public class GeoHeifStore extends DataStore implements Aggregate { * Logs a warning emitted (usually indirectly) by {@link #components()}. */ final void warning(final LogRecord record) { - record.setSourceClassName(GeoHeifStore.class.getSimpleName()); + record.setSourceClassName(GeoHeifStore.class.getCanonicalName()); record.setSourceMethodName("components"); listeners.warning(record); }