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 078f89d40d Add basic support for Braced URI Literal in the XPath 
expression given to `FilterFactory.property(String)`.
078f89d40d is described below

commit 078f89d40dc59b0f7877fdc434eb743a1c953a31
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Nov 16 15:00:58 2023 +0100

    Add basic support for Braced URI Literal in the XPath expression given to 
`FilterFactory.property(String)`.
---
 .../org/apache/sis/filter/AssociationValue.java    |  10 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |  14 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  34 ++---
 .../main/org/apache/sis/filter/internal/XPath.java | 157 ++++++++++++++++-----
 .../main/org/apache/sis/filter/package-info.java   |   2 +-
 .../test/org/apache/sis/filter/XPathTest.java      |  40 +++++-
 .../main/org/apache/sis/util/iso/Names.java        |   2 +
 .../main/org/apache/sis/storage/FeatureQuery.java  |   2 +-
 .../org/apache/sis/cql/ExpressionReadingTest.java  |   4 +-
 9 files changed, 197 insertions(+), 68 deletions(-)

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 ffd768a4f5..b4fd17293b 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
@@ -21,10 +21,10 @@ import java.util.Set;
 import java.util.List;
 import java.util.Collection;
 import java.util.Optional;
-import java.util.StringJoiner;
 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,9 +124,11 @@ final class AssociationValue<V> extends 
LeafExpression<Feature, V>
      */
     @Override
     public final String getXPath() {
-        final StringJoiner sb = new StringJoiner("/", accessor.isVirtual ? 
PropertyValue.VIRTUAL_PREFIX : "", "");
-        for (final String p : path) sb.add(p);
-        return sb.add(accessor.name).toString();
+        String s = new XPath(path, accessor.name).toString();
+        if (accessor.isVirtual) {
+            s = PropertyValue.VIRTUAL_PREFIX.concat(s);
+        }
+        return s;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index eed4a30fe0..4b8a162e81 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@ -51,7 +51,7 @@ import org.apache.sis.util.internal.AbstractMap;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) to use as inputs.
  * @param  <G>  base class of geometry objects. The implementation-neutral 
type is GeoAPI {@link Geometry},
@@ -222,6 +222,18 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
          * then {@code type} can be <code>{@linkplain Number}.class</code>. If 
property values can be of any type with no
          * conversion desired, then {@code type} should be {@code 
Object.class}.</p>
          *
+         * <h4>Supported XPath syntax</h4>
+         * If the given {@code xpath} contains the "/" character, then all 
path components before the last one
+         * are interpreted as associations to follow. For example if the XPath 
is {@code "client/name"}, then
+         * the {@code ValueReference} applied on feature <var>F</var> will 
first search for an association
+         * named {@code "client"} to feature <var>C</var>, then search for a 
property named {@code "name"}
+         * in feature <var>C</var>.
+         *
+         * <p>The given {@code xpath} may contain scoped names.
+         * For example {@code "foo:client"} is the name {@code "client"} in 
scope {@code "foo"}.
+         * If the scope is an URL, then it needs to be enclosed inside {@code 
"Q{…}"}.
+         * Example: {@code "Q{http://www.foo.com/bar}client"}.</p>
+         *
          * @param  <V>    the type of the values to be fetched (compile-time 
value of {@code type}).
          * @param  xpath  the path to the property whose value will be 
returned by the {@code apply(R)} method.
          * @param  type   the type of the values to be fetched (run-time value 
of {@code <V>}).
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 a3d9160f03..bd930ebdbe 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
@@ -97,38 +97,32 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
      * @throws IllegalArgumentException if the given XPath is not supported.
      */
     @SuppressWarnings("unchecked")
-    static <V> ValueReference<Feature,V> create(String xpath, final Class<V> 
type) {
+    static <V> ValueReference<Feature,V> create(final String xpath, final 
Class<V> type) {
+        final var parsed = new XPath(xpath);
+        List<String> path = parsed.path;
         boolean isVirtual = false;
-        List<String> path = XPath.split(xpath);
-split:  if (path != null) {
+        if (parsed.isAbsolute) {
             /*
              * If the XPath is like "/∗/property" where the root "/" is the 
feature instance,
              * we interpret that as meaning "property of a feature of any 
type", which means
              * to relax the restriction about the set of allowed properties.
              */
-            final String head = path.get(0);                // List and items 
in the list are guaranteed non-empty.
-            isVirtual = head.equals("/*");
-            if (isVirtual || head.charAt(0) != XPath.SEPARATOR) {
-                final int offset = isVirtual ? 1 : 0;       // Skip the "/*/" 
component at index 0.
-                final int last = path.size() - 1;
-                if (last >= offset) {
-                    xpath = path.get(last);
-                    path  = path.subList(offset, last);
-                    break split;                            // Accept the path 
as valid.
-                }
+            isVirtual = (path != null) && path.get(0).equals("*");
+            if (!isVirtual) {
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
+            }
+            path.remove(0);
+            if (path.isEmpty()) {
+                path = null;
             }
-            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
         }
-        /*
-         * At this point, `xpath` is the tip of the path (i.e. prefixes have 
been removed).
-         */
         final PropertyValue<V> tip;
         if (type != Object.class) {
-            tip = new Converted<>(type, xpath, isVirtual);
+            tip = new Converted<>(type, parsed.tip, isVirtual);
         } else {
-            tip = (PropertyValue<V>) new AsObject(xpath, isVirtual);
+            tip = (PropertyValue<V>) new AsObject(parsed.tip, isVirtual);
         }
-        return (path == null || path.isEmpty()) ? tip : new 
AssociationValue<>(path, tip);
+        return (path != null) ? new AssociationValue<>(path, tip) : tip;
     }
 
     /**
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 3f13b475ae..a267b691fa 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
@@ -17,8 +17,8 @@
 package org.apache.sis.filter.internal;
 
 import java.util.List;
+import java.util.Arrays;
 import java.util.ArrayList;
-import org.apache.sis.util.Static;
 import org.apache.sis.util.resources.Errors;
 
 import static org.apache.sis.util.CharSequences.*;
@@ -30,7 +30,7 @@ import static org.apache.sis.util.CharSequences.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class XPath extends Static {
+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.
@@ -39,47 +39,138 @@ public final class XPath extends Static {
     public static final char SEPARATOR = '/';
 
     /**
-     * Do not allow instantiation of this class.
+     * The prefix for names qualified by their URI instead of prefix.
+     * Example: {@code "Q{http://example.com/foo/bar}feature/property"}.
+     *
+     * @see <a 
href="https://www.w3.org/TR/xpath-31/#doc-xpath31-URIQualifiedName";>XPath 3.1 
qualified name</a>
      */
-    private XPath() {
-    }
+    private static final char BRACED_URI_PREFIX = 'Q';
 
     /**
-     * Splits the given URL around the {@code '/'} separator, or returns 
{@code null} if there is no separator.
-     * By convention if the URL is absolute, then the leading {@code '/'} 
character is kept in the first element.
-     * For example, {@code "/∗/property"} is splitted as two elements: {@code 
"/∗"} and {@code "property"}.
-     *
-     * <p>This method trims the whitespaces of components except the last one 
(the tip),
-     * for consistency with the case where this method returns {@code 
null}.</p>
+     * The characters used as delimiters for braced URI literals.
+     * The open bracket should be prefixed by {@value #BRACED_URI_PREFIX},
+     * but this is optional in this implementation.
+     */
+    private static final char OPEN = '{', CLOSE = '}';
+
+    /**
+     * The components of the XPath before the tip, or {@code null} if none.
+     * This list, if non-null, contains at least one element but not the 
{@linkplain #tip}.
+     */
+    public List<String> path;
+
+    /**
+     * The tip of the XPath.
+     * This is the part after the last occurrence of {@value #SEPARATOR},
+     * unless that occurrence was inside curly brackets for qualified name.
+     */
+    public String tip;
+
+    /**
+     * Whether the XPath has a leading {@value #SEPARATOR} character.
+     */
+    public boolean isAbsolute;
+
+    /**
+     * Splits the given XPath around the {@code '/'} separator, except for the 
part between curly brackets.
+     * If a leading {@code '/'} character is present, it is removed and {@link 
#isAbsolute} is set to true.
+     * This method trims the whitespaces of all components.
      *
-     * @param  xpath  the URL to split.
-     * @return the splitted URL with the heading separator kept in the first 
element, or {@code null}
-     *         if there is no separator. If non-null, the list always contains 
at least one element.
+     * @param  xpath  the XPath to split.
      * @throws IllegalArgumentException if the XPath contains at least one 
empty component.
      */
-    public static List<String> split(final String xpath) {
-        int next = xpath.indexOf(SEPARATOR);
-        if (next < 0) {
-            return null;
+    public XPath(final String xpath) {
+        /*
+         * Check whether the XPath is absolute.
+         * This is identified by a leading "/".
+         */
+        int length = xpath.length();
+        int start  = skipLeadingWhitespaces(xpath, 0, length);
+        if (start >= length) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "xpath"));
         }
-        final List<String> components = new ArrayList<>(4);
-        int start = skipLeadingWhitespaces(xpath, 0, next);
-        if (start < next) {
-            // No leading '/' (the characters before it are a path element, 
added below).
-            components.add(xpath.substring(start, 
skipTrailingWhitespaces(xpath, start, next)));
-            start = ++next;
-        } else {
-            // Keep the `start` position on the leading '/'.
-            next++;
+        if (xpath.charAt(start) == SEPARATOR) {
+            start = skipLeadingWhitespaces(xpath, start+1, length);
+            isAbsolute = true;
         }
-        while ((next = xpath.indexOf(SEPARATOR, next)) >= 0) {
-            components.add(trimWhitespaces(xpath, start, next).toString());
-            start = ++next;
+        /*
+         * Check for braced URI literal, for example "Q{http://example.com}";.
+         * The "Q" prefix is mandated by XPath 3.1 specification, but optional 
in this implementation.
+         * Any other prefix is considered an error, as the brackets may have 
another signification.
+         */
+        int open = xpath.indexOf(OPEN, start);
+        if (open >= 0) {
+            final int before = skipLeadingWhitespaces(xpath, start, open);
+            if (before != open && (before != open-1 || xpath.charAt(before) != 
BRACED_URI_PREFIX)) {
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, 
xpath.substring(before)));
+            }
+            final int close = xpath.indexOf(CLOSE, ++open);
+            if (close < 0) {
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.MissingCharacterInElement_2, 
xpath.substring(before), CLOSE));
+            }
+            final String part = trimWhitespaces(xpath, open, close).toString();
+            if (part.indexOf(OPEN) >= 0) {
+                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalCharacter_2, part, 
OPEN));
+            }
+            path = new ArrayList<>(4);
+            path.add(part);
+            start = close + 1;
         }
-        components.add(xpath.substring(start));         // No whitespace 
trimming.
-        if (components.stream().anyMatch(String::isEmpty)) {
+        /*
+         * Add all components before the last "/" characters.
+         * The remaining is the tip, stored separately.
+         */
+        int next;
+        while ((next = xpath.indexOf(SEPARATOR, start)) >= 0) {
+            if (path == null) {
+                path = new ArrayList<>(4);
+            }
+            path.add(trimWhitespaces(xpath, start, next).toString());
+            start = next + 1;
+        }
+        tip = trimWhitespaces(xpath, start, length).toString();
+        if (tip.isEmpty() || (path != null && 
path.stream().anyMatch(String::isEmpty))) {
             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
         }
-        return components;
+    }
+
+    /**
+     * 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.
+     */
+    public XPath(final String[] path, final String tip) {
+        if (path != null) {
+            this.path = Arrays.asList(path);
+        }
+        this.tip = tip;
+    }
+
+    /**
+     * Rewrites the XPath from its components and the tip.
+     *
+     * @return the XPath.
+     */
+    @Override
+    public String toString() {
+        if (!isAbsolute && path == null) {
+            return tip;
+        }
+        final var sb = new StringBuilder(40);
+        if (isAbsolute) sb.append(SEPARATOR);
+        if (path != null) {
+            final int size = path.size();
+            for (int i=0; i<size; i++) {
+                final String part = path.get(i);
+                if (i == 0 && part.indexOf(SEPARATOR) >= 0) {
+                    
sb.append(BRACED_URI_PREFIX).append(OPEN).append(part).append(CLOSE);
+                } else {
+                    sb.append(part).append(SEPARATOR);
+                }
+            }
+        }
+        return sb.append(tip).toString();
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
index 2fa11028a1..7d6c7630ea 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
@@ -57,7 +57,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @since 1.1
  */
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 31412c27b7..541157101a 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
@@ -22,7 +22,7 @@ import org.apache.sis.filter.internal.XPath;
 import org.junit.Test;
 import org.apache.sis.test.TestCase;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 
 /**
@@ -38,13 +38,41 @@ public final class XPathTest extends TestCase {
     }
 
     /**
-     * Tests {@link XPath#split(String)}.
+     * Splits a x-path and verifies the result.
+     *
+     * @param xpath       the x-path to parse.
+     * @param isAbsolute  expected value if {@link XPath#isAbsolute}.
+     * @param path        expected value if {@link XPath#path}. Can be null.
+     * @param tip         expected value if {@link XPath#tip}.
+     */
+    private static void split(final String xpath, final boolean isAbsolute, 
final String[] path, final String tip) {
+        final var p = new XPath(xpath);
+        assertEquals(isAbsolute, p.isAbsolute, "isAbsolute");
+        assertArrayEquals(path, (p.path != null) ? p.path.toArray() : null, 
"path");
+        assertEquals(tip, p.tip, "tip");
+        assertEquals(xpath.replace(" ", ""), p.toString(), "toString()");
+    }
+
+    /**
+     * Tests {@link XPath#XPath(String)}.
      */
     @Test
     public void testSplit() {
-        assertNull(XPath.split("property"));
-        assertArrayEquals(new String[] {"/property"},                    
XPath.split("/property").toArray());
-        assertArrayEquals(new String[] {"Feature", "property", "child"}, 
XPath.split("Feature/property/child").toArray());
-        assertArrayEquals(new String[] {"/Feature", "property"},         
XPath.split("/Feature/property").toArray());
+        split("property", false, null, "property");
+        split("/property", true, null, "property");
+        split("Feature/property/child",         false, new String[] 
{"Feature",  "property"}, "child");
+        split("/Feature/property",              true,  new String[] 
{"Feature"}, "property");
+        split("  Feature  / property / child ", false, new String[] 
{"Feature",  "property"}, "child");
+        split("  / Feature /  property ",       true,  new String[] 
{"Feature"}, "property");
+    }
+
+    /**
+     * Tests with a x-path containing an URL as the property namespace.
+     */
+    @Test
+    public void testQualifiedName() {
+        split("Q{http://example.com/foo/bar}property";,       false, new 
String[] {"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");
+        split("/Q{http://example.com/foo/bar}property";,      true,  new 
String[] {"http://example.com/foo/bar"}, "property");
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/Names.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/Names.java
index 082413384d..e3c5c32071 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/Names.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/Names.java
@@ -27,6 +27,7 @@ import org.opengis.util.NameSpace;
 import org.opengis.util.NameFactory;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.Static;
+import org.apache.sis.util.OptionalCandidate;
 import org.apache.sis.util.UnknownNameException;
 
 import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
@@ -416,6 +417,7 @@ public final class Names extends Static {
      *
      * @since 0.5
      */
+    @OptionalCandidate
     public static Class<?> toClass(final TypeName type) throws 
UnknownNameException {
         if (type == null) {
             return null;
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 172b87705a..52a6ec31d8 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
@@ -779,7 +779,7 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
                     name = valueType.getProperty(xpath).getName();
                     if (name == null || !names.add(name.toString())) {
                         name = null;
-                        xpath = 
xpath.substring(xpath.lastIndexOf(XPath.SEPARATOR) + 1);  // Works also if '/' 
is not found.
+                        xpath = new XPath(xpath).tip;
                         if (!(xpath.isEmpty() || names.contains(xpath))) {
                             text = xpath;
                         }
diff --git 
a/incubator/src/org.apache.sis.cql/test/org/apache/sis/cql/ExpressionReadingTest.java
 
b/incubator/src/org.apache.sis.cql/test/org/apache/sis/cql/ExpressionReadingTest.java
index e61ea86552..c92c1bb63f 100644
--- 
a/incubator/src/org.apache.sis.cql/test/org/apache/sis/cql/ExpressionReadingTest.java
+++ 
b/incubator/src/org.apache.sis.cql/test/org/apache/sis/cql/ExpressionReadingTest.java
@@ -75,11 +75,11 @@ public final class ExpressionReadingTest extends 
CQLTestCase {
 
     @Test
     public void testValueReference3() throws CQLException {
-        final String cql = "ùth{e_$uglY^_pr@perté";
+        final String cql = "ùthe_$uglY^_pr@perté";
         final Object obj = CQL.parseExpression(cql);
         assertTrue(obj instanceof ValueReference);
         final ValueReference expression = (ValueReference) obj;
-        assertEquals("ùth{e_$uglY^_pr@perté", expression.getXPath());
+        assertEquals("ùthe_$uglY^_pr@perté", expression.getXPath());
     }
 
     @Test

Reply via email to