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 <[email protected]>
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