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 6b91e57ba71127557bac87844c45f276c6b3e1ab Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Nov 23 20:01:21 2023 +0100 `PropertyValue.getXPath()` shall reformat the property name to XPath using "Q{namespace}" syntax when necessary. Conversely, `FeatureQuery` needs to convert XPath to property name. --- .../org/apache/sis/feature/internal/Resources.java | 5 ++ .../sis/feature/internal/Resources.properties | 1 + .../sis/feature/internal/Resources_fr.properties | 1 + .../org/apache/sis/filter/AssociationValue.java | 7 +- .../main/org/apache/sis/filter/PropertyValue.java | 18 +++++- .../main/org/apache/sis/filter/internal/XPath.java | 74 +++++++++++++++++----- .../test/org/apache/sis/filter/XPathTest.java | 20 ++++++ .../main/org/apache/sis/storage/FeatureQuery.java | 18 +++--- 8 files changed, 110 insertions(+), 34 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 b6b8dfc93c..769fa14109 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 @@ -399,6 +399,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short PropertyAlreadyExists_2 = 58; + /** + * Property name “{0}” is invalid because names cannot be XPath. + */ + public static final short PropertyNameCannotBeXPath_1 = 89; + /** * No property named “{1}” has been found in “{0}” feature. */ 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 f74775328c..6f1591f2f9 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 @@ -86,6 +86,7 @@ OptionalLibraryNotFound_2 = The {0} optional library is not available. G OutOfIteratorDomain_2 = The ({0,number}, {1,number}) pixel coordinate is outside iterator domain. PointOutsideCoverageDomain_1 = Point ({0}) is outside the coverage domain. PropertyAlreadyExists_2 = Property \u201c{1}\u201d already exists in feature \u201c{0}\u201d. +PropertyNameCannotBeXPath_1 = Property name \u201c{0}\u201d is invalid because names cannot be XPath. PropertyNotFound_2 = No property named \u201c{1}\u201d has been found in \u201c{0}\u201d feature. SourceImagesDoNotIntersect = Source images do not intersect. TileErrorFlagSet_2 = Tile ({0}, {1}) is unavailable because of error in a previous calculation attempt. 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 1c52e7374d..cdfb25398e 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 @@ -91,6 +91,7 @@ OptionalLibraryNotFound_2 = La biblioth\u00e8que optionnelle {0} n\u2019 OutOfIteratorDomain_2 = La coordonn\u00e9e pixel ({0,number}, {1,number}) est en dehors du domaine de l\u2019it\u00e9rateur. PointOutsideCoverageDomain_1 = Le point ({0}) est en dehors du domaine de la couverture de donn\u00e9es. PropertyAlreadyExists_2 = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb existe d\u00e9j\u00e0 dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb. +PropertyNameCannotBeXPath_1 = Le nom de propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb est invalide parce que ces noms ne doivent pas \u00eatre des XPaths. PropertyNotFound_2 = Aucune propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb. SourceImagesDoNotIntersect = Des images sources ne s\u2019intersectent pas. TileErrorFlagSet_2 = La tuile ({0}, {1}) est indisponible pour cause d\u2019erreur lors d\u2019une tentative ant\u00e9rieure de calcul. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java index 3b8242bf4c..71fd0754e3 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java @@ -24,7 +24,6 @@ import java.util.Optional; import org.apache.sis.feature.Features; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; -import org.apache.sis.filter.internal.XPath; import org.apache.sis.math.FunctionProperty; // Specific to the geoapi-3.1 and geoapi-4.0 branches: @@ -124,11 +123,7 @@ final class AssociationValue<V> extends LeafExpression<Feature, V> */ @Override public final String getXPath() { - String s = new XPath(path, accessor.name).toString(); - if (accessor.isVirtual) { - s = PropertyValue.VIRTUAL_PREFIX.concat(s); - } - return s; + return accessor.getXPath(path); } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java index 6b77bf77e6..1f550a9bfa 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java @@ -76,7 +76,7 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> /** * The prefix in a XPath for considering a property as virtual. */ - static final String VIRTUAL_PREFIX = "/*/"; + private static final String VIRTUAL_PREFIX = "/*/"; /** * Creates a new expression retrieving values from a property of the given name. @@ -142,11 +142,23 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> } /** - * Returns the name of the property whose value will be returned by the {@link #apply(Object)} method. + * Returns the XPath to the property whose value will be returned by the {@link #apply(Object)} method. + * + * @return XPath to the property. */ @Override public final String getXPath() { - return isVirtual ? VIRTUAL_PREFIX.concat(name) : name; + return getXPath(null); + } + + /** + * Returns the XPath to the property, prefixed by the given path. + * + * @param path the path to append as a prefix. + * @return XPath to the property. + */ + final String getXPath(final String[] path) { + return XPath.toString(isVirtual ? VIRTUAL_PREFIX : null, path, name); } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/XPath.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/XPath.java index d8efcd3069..d85f1b3d82 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/XPath.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/XPath.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.iso.DefaultNameSpace; import static org.apache.sis.util.CharSequences.*; +import org.apache.sis.feature.internal.Resources; /** @@ -35,6 +36,8 @@ public final class XPath { * The separator between path components. * Should not be used for URL or Unix name separator, even if the character is the same. * We use this constant for identifying locations in the code where there is some XPath parsing. + * + * @see #toSimpleString() */ public static final char SEPARATOR = '/'; @@ -72,17 +75,9 @@ public final class XPath { public boolean isAbsolute; /** - * Creates a XPath with the given path components. - * The components are assumed already parsed (no braced URI literals). - * - * @param path components of the XPath before the tip, or {@code null} if none. - * @param tip the last component of the XPath. + * Creates an initially empty XPath. Caller is responsible for initializing the fields. */ - public XPath(final String[] path, final String tip) { - if (path != null) { - this.path = Arrays.asList(path); - } - this.tip = tip; + private XPath() { } /** @@ -187,6 +182,22 @@ public final class XPath { } } + /** + * Appends a string representation of the XPath in the given buffer. + * + * @param sb where to write the string representation. + * @return the string builder, for chained method calls. + */ + private StringBuilder toString(final StringBuilder sb) { + if (isAbsolute) sb.append(SEPARATOR); + if (path != null) { + for (final String component : path) { + toBracedURI(component, sb).append(SEPARATOR); + } + } + return toBracedURI(tip, sb); + } + /** * Rewrites the XPath from its components and the tip. * @@ -197,13 +208,44 @@ public final class XPath { if (!isAbsolute && path == null && tip.indexOf(SEPARATOR) < 0) { return tip; } - final var sb = new StringBuilder(40); - if (isAbsolute) sb.append(SEPARATOR); + return toString(new StringBuilder(tip.length() + 10)).toString(); + } + + /** + * Rewrites a property name as an XPath. + * The path components are assumed already parsed (no braced URI literals). + * + * @param prefix a prefix, or {@code null} if none. + * @param path components of the XPath before the tip, or {@code null} if none. + * @param tip the last component of the XPath. + * @return the given path and tip reformatted as an XPath. + */ + public static String toString(final String prefix, final String[] path, final String tip) { + final var x = new XPath(); if (path != null) { - for (final String component : path) { - toBracedURI(component, sb).append(SEPARATOR); - } + x.path = Arrays.asList(path); + } + x.tip = tip; + if (prefix != null) { + return x.toString(new StringBuilder(tip.length() + 10).append(prefix)).toString(); + } else { + return x.toString(); + } + } + + /** + * Rewrites the XPath in a form accepted by feature properties. + * This is the tip, without {@code "Q{namespace}"} escaping. + * + * @param xpath the XPath to convert to a property name. + * @return the XPath as a property name without escape syntax for qualified URIs. + * @throws IllegalArgumentException if the given XPath contains a path instead of only a tip. + */ + public static String toPropertyName(final String xpath) { + final var x = new XPath(xpath); + if (x.path == null) { + return x.tip; } - return toBracedURI(tip, sb).toString(); + throw new IllegalArgumentException(Resources.format(Resources.Keys.PropertyNameCannotBeXPath_1, xpath)); } } diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/XPathTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/XPathTest.java index 77e5277285..d7d9fb94a5 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/XPathTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/XPathTest.java @@ -74,4 +74,24 @@ public final class XPathTest extends TestCase { split("/Q{http://example.com/foo/bar}property", true, null, "http://example.com/foo/bar:property"); split("Q{http://example.com/foo/bar}property/child", false, new String[] {"http://example.com/foo/bar:property"}, "child"); } + + /** + * Tests {@link XPath#toString(String)}. + */ + @Test + public void testToString() { + assertEquals("Q{http://example.com/foo/bar}property", + XPath.toString(null, null, "http://example.com/foo/bar:property")); + assertEquals("/*/Q{http://example.com/foo/bar}property/child", + XPath.toString("/*/", new String[] {"http://example.com/foo/bar:property"}, "child")); + } + + /** + * Tests {@link XPath#toPropertyName(String)}. + */ + @Test + public void testToPropertyName() { + assertEquals("http://example.com/foo/bar:property", + XPath.toPropertyName("Q{http://example.com/foo/bar}property")); + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java index ae094209a9..c841ea117e 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java @@ -84,7 +84,7 @@ import org.opengis.filter.InvalidFilterValueException; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.5 * @since 1.1 */ public class FeatureQuery extends Query implements Cloneable, Serializable { @@ -767,21 +767,20 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { * from the source feature (the Apache SIS implementation does just that). If the name is set, * then we assume that it is correct. Otherwise we take the tip of the XPath. */ - CharSequence text = null; + String tip = null; if (expression instanceof ValueReference<?,?>) { - String xpath = ((ValueReference<?,?>) expression).getXPath().trim(); + tip = XPath.toPropertyName(((ValueReference<?,?>) expression).getXPath()); /* - * Before to take the tip, take the existing `GenericName` instance from the property. - * It should be equivalent, except that it may be a `ScopedName` instead of `LocalName`. + * Take the existing `GenericName` instance from the property. It should be equivalent to + * creating a name from the tip, except that it may be a `ScopedName` instead of `LocalName`. * We do not take `resultType.getName()` because the latter is different if the property * is itself a link to another property (in which case `resultType` is the final target). */ - name = valueType.getProperty(xpath).getName(); + name = valueType.getProperty(tip).getName(); if (name == null || !names.add(name.toString())) { name = null; - xpath = new XPath(xpath).tip; - if (!(xpath.isEmpty() || names.contains(xpath))) { - text = xpath; + if (tip.isEmpty() || names.contains(tip)) { + tip = null; } } } @@ -792,6 +791,7 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { * providing localized names only if explicitly requested. */ if (name == null) { + CharSequence text = tip; if (text == null) do { text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber); } while (!names.add(text.toString()));