From fa33d30ff3967cccfc1437c2d4a701918121a460 Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Wed, 25 Mar 2026 16:58:22 +0100 Subject: [PATCH 01/12] Prologue sent to a prefix handler --- .../query/impl/parser/SparqlAstBuilder.java | 27 +++ .../query/impl/parser/SparqlListener.java | 16 ++ .../next/query/impl/parser/SparqlParser.java | 3 +- .../impl/parser/listener/PrologueFeature.java | 20 ++ .../query/impl/sparql/ast/AskQueryAst.java | 14 +- .../impl/sparql/ast/ConstructQueryAst.java | 22 ++- .../impl/sparql/ast/DescribeQueryAst.java | 14 +- .../next/query/impl/sparql/ast/QueryAst.java | 3 + .../query/impl/sparql/ast/SelectQueryAst.java | 17 +- .../impl/parser/SparqlParserPrologueTest.java | 184 ++++++++++++++++++ 10 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 49eff7f1a..2631dac72 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -1,6 +1,8 @@ package fr.inria.corese.core.next.query.impl.parser; +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; @@ -117,12 +119,34 @@ public final class SparqlAstBuilder { */ private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer(); + private String baseUri = IOConstants.getDefaultBaseURI(); + private Map prefixes = new HashMap<>(); + public SparqlAstBuilder(SparqlParserOptions options) { this.options = options; } // --- Construction entry points (called by listener) --- + public void setBaseUri(String uri) { + if(uri.startsWith("<") && uri.endsWith(">")) { + uri = uri.substring(0, uri.lastIndexOf(">")); + uri = uri.substring(uri.indexOf("<")+1); + } + this.baseUri = uri; + } + + public void addPrefix(String prefix, String uri) { + if(prefix.endsWith(":")) { + prefix = prefix.substring(0, prefix.lastIndexOf(":")); + } + if(uri.startsWith("<") && uri.endsWith(">")) { + uri = uri.substring(0, uri.lastIndexOf(">")); + uri = uri.substring(uri.indexOf("<") +1); + } + this.prefixes.put(prefix, uri); + } + public void enterAskQuery() { queryType = ASTConstants.QUERY_TYPE.ASK; } @@ -332,6 +356,9 @@ public QueryAst getResult() { throw new IllegalStateException("No WHERE clause: did you call exitGroup() for the top-level GroupGraphPattern?"); } DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); + PrefixHandler prefixHandler = new PrefixHandler(true); + prefixHandler.setDefaultNamespace(this.baseUri); + this.prefixes.forEach(prefixHandler::setPrefix); return switch (this.queryType) { case ASK -> buildAskQueryAst(datasetClauseAst); case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java index 5c3230487..470f4c7a7 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlListener.java @@ -163,4 +163,20 @@ public void exitDefaultGraphClause(SparqlParser.DefaultGraphClauseContext ctx) { public void exitNamedGraphClause(SparqlParser.NamedGraphClauseContext ctx) { for (var d : delegates) d.exitNamedGraphClause(ctx); } + + @Override public void enterBaseDecl(SparqlParser.BaseDeclContext ctx) { + for (var d : delegates) d.enterBaseDecl(ctx); + } + + @Override public void exitBaseDecl(SparqlParser.BaseDeclContext ctx) { + for (var d : delegates) d.exitBaseDecl(ctx); + } + + @Override public void enterPrefixDecl(SparqlParser.PrefixDeclContext ctx) { + for (var d : delegates) d.enterPrefixDecl(ctx); + } + + @Override public void exitPrefixDecl(SparqlParser.PrefixDeclContext ctx) { + for (var d : delegates) d.exitPrefixDecl(ctx); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java index a10944d2d..8d91e97f0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java @@ -125,7 +125,8 @@ public QueryAst parse(Reader reader, String baseIRI) { new FilterFeature(builder), new UnionFeature(builder), new DescribeQueryFeature(builder), - new DatasetClauseFeature(builder) + new DatasetClauseFeature(builder), + new PrologueFeature(builder) )); walker.walk(listener, tree); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java new file mode 100644 index 000000000..39cad1fc5 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java @@ -0,0 +1,20 @@ +package fr.inria.corese.core.next.query.impl.parser.listener; + +import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; +import fr.inria.corese.core.next.query.impl.parser.SparqlAstBuilder; + +public class PrologueFeature extends AbstractSparqlFeature { + public PrologueFeature(SparqlAstBuilder builder) { + super(builder); + } + + @Override + public void exitBaseDecl(SparqlParser.BaseDeclContext ctx) { + builder().setBaseUri(ctx.IRI_REF().getText()); + } + + @Override + public void exitPrefixDecl(SparqlParser.PrefixDeclContext ctx) { + builder().addPrefix(ctx.PNAME_NS().getText(), ctx.IRI_REF().getText()); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index f7f51ed00..9cf81b64c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; + import java.util.List; /** @@ -15,7 +17,14 @@ * } * } */ -public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) implements QueryAst { +public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, PrefixHandler prefixHandler) implements QueryAst { + /** + * constructor with default prefix handler + */ + public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) { + this(datasetClause, whereClause, null); + } + public AskQueryAst { if (whereClause == null) { whereClause = new GroupGraphPatternAst(List.of()); @@ -23,5 +32,8 @@ public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst w if(datasetClause == null) { datasetClause = DatasetClauseAst.none(); } + if (prefixHandler == null) { + prefixHandler = new PrefixHandler(true); + } } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index eb9acb30d..e2112d9eb 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; + import java.util.List; /** @@ -34,8 +36,9 @@ public record ConstructQueryAst( ConstructTemplateAst constructTemplate, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, - SolutionModifierAst solutionModifier -) implements QueryAst { + SolutionModifierAst solutionModifier, + PrefixHandler prefixHandler + ) implements QueryAst { public ConstructQueryAst { if (constructTemplate == null) { constructTemplate = new ConstructTemplateAst(List.of()); @@ -49,9 +52,22 @@ public record ConstructQueryAst( if (solutionModifier == null) { solutionModifier = SolutionModifierAst.empty(); } + if (prefixHandler == null) { + prefixHandler = new PrefixHandler(true); + } + } + + /** + * constructor with default prefix handler + */ + public ConstructQueryAst(ConstructTemplateAst template, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) { + this(template, datasetClause, whereClause, solutionModifier, null); } + /** + * constructor with default prefix handler and default solution modifier + */ public ConstructQueryAst(ConstructTemplateAst template, GroupGraphPatternAst whereClause) { - this(template, DatasetClauseAst.none(), whereClause, SolutionModifierAst.empty()); + this(template, DatasetClauseAst.none(), whereClause, SolutionModifierAst.empty(), null); } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index e70fa98e3..e4fa51ab9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; + import java.util.List; /** @@ -21,7 +23,7 @@ * } * } */ -public record DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause) implements QueryAst { +public record DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause, PrefixHandler prefixHandler) implements QueryAst { public DescribeQueryAst { described = described != null ? List.copyOf(described) : List.of(); if (whereClause == null) { @@ -30,6 +32,16 @@ public record DescribeQueryAst(DatasetClauseAst datasetClause, List des if(datasetClause == null) { datasetClause = DatasetClauseAst.none(); } + if(prefixHandler == null) { + prefixHandler = new PrefixHandler(true); + } + } + + /** + * constructor with default prefix handler + */ + public DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause) { + this(datasetClause, described, whereClause, null); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java index 03e29ba1e..ef94e132f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.api.IPrefixHandler; + /** * Minimal SPARQL query AST (SELECT, ASK, CONSTRUCT, DESCRIBE). * Holds the WHERE clause as a group graph pattern; query-specific projection/template can be added later. @@ -7,4 +9,5 @@ public sealed interface QueryAst permits AskQueryAst, ConstructQueryAst, DescribeQueryAst, SelectQueryAst { DatasetClauseAst datasetClause(); GroupGraphPatternAst whereClause(); + IPrefixHandler prefixHandler(); } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index 6f8fc3cb7..0dfcb963e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -1,21 +1,29 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.api.IPrefixHandler; +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; + import java.util.List; /** * Abstract Syntax Tree (AST) representation of a SPARQL {@code SELECT} query. * Holds the projection (SELECT * or SELECT ?v1 ?v2 ...) and the WHERE clause. */ -public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) implements QueryAst { +public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, PrefixHandler prefixHandler) implements QueryAst { /** Constructor with default projection SELECT *. */ public SelectQueryAst(GroupGraphPatternAst whereClause) { this(ProjectionAsts.selectAll(), DatasetClauseAst.none(), whereClause); } - /** Constructor with default solution modifier (no DISTINCT/REDUCED/ORDER BY/LIMIT/OFFSET). */ + /** Constructor with default solution modifier (no DISTINCT/REDUCED/ORDER BY/LIMIT/OFFSET) and default prefix handler. */ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) { - this(projection, datasetClause, whereClause, null); + this(projection, datasetClause, whereClause, null, null); + } + + /** Constructor with default prefix handler */ + public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) { + this(projection, datasetClause, whereClause, solutionModifier, null); } public SelectQueryAst { @@ -31,5 +39,8 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, if (solutionModifier == null) { solutionModifier = SolutionModifierAst.empty(); } + if (prefixHandler == null) { + prefixHandler = new PrefixHandler(true); + } } } \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java new file mode 100644 index 000000000..5e00edb6c --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -0,0 +1,184 @@ +package fr.inria.corese.core.next.query.impl.parser; + +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SparqlParserPrologueTest extends AbstractSparqlParserFeatureTest { + + @Test + @DisplayName("Basic Ask with base") + public void askWithBase() { + String query = """ + BASE + ASK { + ?s ?p ?o . + } + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + } + + @Test + @DisplayName("Basic Construct with base") + public void constructWithBase() { + String query = """ + BASE + CONSTRUCT { + ?o ?p ?s . + } + { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + } + + @Test + @DisplayName("Basic Select with base") + public void describeWithBase() { + String query = """ + BASE + DESCRIBE ?s { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + } + + @Test + @DisplayName("Basic Select with base") + public void selectWithBase() { + String query = """ + BASE + SELECT * { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + } + + @Test + @DisplayName("Basic Select with base and one prefix") + public void selectWithBaseAndOnePrefix() { + String query = """ + BASE + PREFIX test: + SELECT * { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertTrue(ast.prefixHandler().hasPrefix("test")); + assertEquals("test", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); + assertEquals("https://ns.inria.fr/otherTest/#", ast.prefixHandler().getNamespace("test")); + } + + @Test + @DisplayName("Basic Select with base and multiple prefix") + public void selectWithBaseAndMultiplePrefix() { + String query = """ + BASE + PREFIX test1: + PREFIX test2: + PREFIX test3: + SELECT * { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertTrue(ast.prefixHandler().hasPrefix("test1")); + assertEquals("test1", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest1/#", ast.prefixHandler().getNamespace("test1")); + assertTrue(ast.prefixHandler().hasPrefix("test2")); + assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test2")); + assertTrue(ast.prefixHandler().hasPrefix("test3")); + assertEquals("test3", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); + assertEquals("https://ns.inria.fr/otherTest3/#", ast.prefixHandler().getNamespace("test3")); + } + + @Test + @DisplayName("Basic Select with base and multiple prefix with overlap") + public void selectWithBaseAndMultiplePrefixWithOverlap() { + String query = """ + BASE + PREFIX test1: + PREFIX test2: + PREFIX test1: + SELECT * { + ?s ?p ?o . + } LIMIT 10 + """; + // test1 -> ns:otherTest1 + // ns:otherTest1 -> test1 + // test2 -> ns:otherTest1 + // ns:otherTest1 -> test2 + // test1 -> ns:otherTest2 + // ns:otherTest2 -> test1 + + SparqlParser parser = newParserDefault(); + + QueryAst ast = parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertTrue(ast.prefixHandler().hasPrefix("test1")); + assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test1")); + assertTrue(ast.prefixHandler().hasPrefix("test2")); + assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest1/#", ast.prefixHandler().getNamespace("test2")); + assertTrue(ast.prefixHandler().hasPrefix("test1")); + assertEquals("test1", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); + assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test1")); + } + + @Test + @DisplayName("Basic Select with multiple base should throw") + public void selectWithMultipleBase() { + String query = """ + BASE + BASE + SELECT * { + ?s ?p ?o . + } LIMIT 10 + """; + + SparqlParser parser = newParserDefault(); + + assertThrows(QuerySyntaxException.class, () -> { + parser.parse(query); + }); + } +} From 42c979d4485a99eb7702675b74aa32f80ea922e0 Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Wed, 25 Mar 2026 17:44:52 +0100 Subject: [PATCH 02/12] Fixing equality tests --- .../impl/common/prefix/PrefixHandler.java | 22 +++++++++++++++++++ .../query/impl/parser/SparqlAstBuilder.java | 10 +++++++-- .../query/impl/sparql/ast/AskQueryAst.java | 4 +++- .../impl/sparql/ast/ConstructQueryAst.java | 4 +++- .../impl/sparql/ast/DescribeQueryAst.java | 4 +++- .../query/impl/sparql/ast/SelectQueryAst.java | 4 +++- .../impl/parser/SparqlAstBuilderTest.java | 2 ++ 7 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/common/prefix/PrefixHandler.java b/src/main/java/fr/inria/corese/core/next/data/impl/common/prefix/PrefixHandler.java index 70601ab79..813a03b80 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/common/prefix/PrefixHandler.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/common/prefix/PrefixHandler.java @@ -29,6 +29,13 @@ public class PrefixHandler implements IPrefixHandler, Cloneable { */ private String defaultNamespace; + /** + * Creates a new PrefixHandler. + */ + public PrefixHandler() { + this(false); + } + /** * Creates a new PrefixHandler. * @@ -418,6 +425,21 @@ public String toString() { return sb.toString(); } + @Override + public boolean equals(Object other) { + if(! (other instanceof IPrefixHandler)) { + return false; + } + return Objects.equals(this.getDefaultNamespace(), ((IPrefixHandler) other).getDefaultNamespace()) + && Objects.equals(this.getNamespaceMap(), ((IPrefixHandler) other).getNamespaceMap()) + && Objects.equals(this.getPrefixMap(), ((IPrefixHandler) other).getPrefixMap()); + } + + @Override + public int hashCode() { + return Objects.hash(defaultNamespace, prefixToNamespace, namespaceToPrefix); + } + /** * Simple immutable implementation of Namespace interface. * Used internally to create Namespace objects from prefix-URI pairs. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 2631dac72..7d9fb1b3b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -119,8 +119,14 @@ public final class SparqlAstBuilder { */ private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer(); + /** + * Base URI for ":" prefixed IRI resolution + */ private String baseUri = IOConstants.getDefaultBaseURI(); - private Map prefixes = new HashMap<>(); + /** + * map storage + */ + private final Map prefixes = new HashMap<>(); public SparqlAstBuilder(SparqlParserOptions options) { this.options = options; @@ -356,7 +362,7 @@ public QueryAst getResult() { throw new IllegalStateException("No WHERE clause: did you call exitGroup() for the top-level GroupGraphPattern?"); } DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); - PrefixHandler prefixHandler = new PrefixHandler(true); + PrefixHandler prefixHandler = new PrefixHandler(); prefixHandler.setDefaultNamespace(this.baseUri); this.prefixes.forEach(prefixHandler::setPrefix); return switch (this.queryType) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index 9cf81b64c..175ccb149 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; @@ -33,7 +34,8 @@ public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereCla datasetClause = DatasetClauseAst.none(); } if (prefixHandler == null) { - prefixHandler = new PrefixHandler(true); + prefixHandler = new PrefixHandler(); + prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); } } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index e2112d9eb..26f8fded1 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; @@ -53,7 +54,8 @@ public record ConstructQueryAst( solutionModifier = SolutionModifierAst.empty(); } if (prefixHandler == null) { - prefixHandler = new PrefixHandler(true); + prefixHandler = new PrefixHandler(); + prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index e4fa51ab9..eab9d38f9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; @@ -33,7 +34,8 @@ public record DescribeQueryAst(DatasetClauseAst datasetClause, List des datasetClause = DatasetClauseAst.none(); } if(prefixHandler == null) { - prefixHandler = new PrefixHandler(true); + prefixHandler = new PrefixHandler(); + prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index 0dfcb963e..b4680c5b1 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.data.api.IPrefixHandler; import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; @@ -40,7 +41,8 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, solutionModifier = SolutionModifierAst.empty(); } if (prefixHandler == null) { - prefixHandler = new PrefixHandler(true); + prefixHandler = new PrefixHandler(); + prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); } } } \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilderTest.java index 2c133d33f..3b09b7692 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilderTest.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.parser; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.query.impl.sparql.ast.*; import org.junit.jupiter.api.Test; @@ -43,6 +44,7 @@ void shouldBuildEmptyWhereGroupWhenNoTriples() { void shouldBuildSingleBgpWithOneTriple() { SparqlAstBuilder b = newBuilder(); + b.setBaseUri(IOConstants.getDefaultBaseURI()); b.enterSelectQuery(); b.enterGroup(); b.enterBgp(); From 0e52aa9e99080720aebbe42765913a1a589ca667 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Thu, 26 Mar 2026 10:00:05 +0100 Subject: [PATCH 03/12] prologue handling proposition for select query --- .../query/impl/parser/SparqlAstBuilder.java | 23 +++--- .../query/impl/sparql/ast/AskQueryAst.java | 2 +- .../impl/sparql/ast/PrefixDeclarationAst.java | 15 ++++ .../impl/sparql/ast/QueryPrologueAst.java | 38 ++++++++++ .../query/impl/sparql/ast/SelectQueryAst.java | 25 ++++--- .../impl/parser/SparqlParserPrologueTest.java | 71 ++++++++++--------- 6 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 7d9fb1b3b..221dadeff 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -120,16 +120,22 @@ public final class SparqlAstBuilder { private final VariableScopeAnalyzer variableScopeAnalyzer = new VariableScopeAnalyzer(); /** - * Base URI for ":" prefixed IRI resolution + * Effective base URI after prologue (parser options, then possibly {@code BASE}). */ - private String baseUri = IOConstants.getDefaultBaseURI(); + private String baseUri; /** - * map storage + * Prefix declarations in source order (including redeclarations). */ - private final Map prefixes = new HashMap<>(); + private final List prefixDeclarations = new ArrayList<>(); + /** + * Mutable view of prefix mappings for resolution while building; kept in sync with {@link #prefixDeclarations}. + */ + private final PrefixHandler prefixHandler = new PrefixHandler(); public SparqlAstBuilder(SparqlParserOptions options) { this.options = options; + this.baseUri = options.getBaseIRI(); + this.prefixHandler.setDefaultNamespace(this.baseUri); } // --- Construction entry points (called by listener) --- @@ -140,6 +146,7 @@ public void setBaseUri(String uri) { uri = uri.substring(uri.indexOf("<")+1); } this.baseUri = uri; + this.prefixHandler.setDefaultNamespace(uri); } public void addPrefix(String prefix, String uri) { @@ -150,7 +157,8 @@ public void addPrefix(String prefix, String uri) { uri = uri.substring(0, uri.lastIndexOf(">")); uri = uri.substring(uri.indexOf("<") +1); } - this.prefixes.put(prefix, uri); + this.prefixHandler.setPrefix(prefix, uri); + this.prefixDeclarations.add(new PrefixDeclarationAst(prefix, new IriAst(uri))); } public void enterAskQuery() { @@ -362,9 +370,8 @@ public QueryAst getResult() { throw new IllegalStateException("No WHERE clause: did you call exitGroup() for the top-level GroupGraphPattern?"); } DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); - PrefixHandler prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(this.baseUri); - this.prefixes.forEach(prefixHandler::setPrefix); + QueryPrologueAst selectPrologue = new QueryPrologueAst(List.copyOf(prefixDeclarations), new IriAst(baseUri)); + PrefixHandler resolvedPrefixes = selectPrologue.toPrefixHandler(); return switch (this.queryType) { case ASK -> buildAskQueryAst(datasetClauseAst); case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index 175ccb149..b76fcfa93 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -30,7 +30,7 @@ public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereCla if (whereClause == null) { whereClause = new GroupGraphPatternAst(List.of()); } - if(datasetClause == null) { + if (datasetClause == null) { datasetClause = DatasetClauseAst.none(); } if (prefixHandler == null) { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java new file mode 100644 index 000000000..ede5467db --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java @@ -0,0 +1,15 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import java.util.Objects; + +/** + * A {@code PREFIX p: <ns>} declaration from the SPARQL prologue ({@code p} without trailing colon). + */ +public record PrefixDeclarationAst(String prefix, IriAst namespace) { + public PrefixDeclarationAst { + if (prefix == null || prefix.isEmpty()) { + throw new IllegalArgumentException("prefix must be non-null and non-empty"); + } + namespace = Objects.requireNonNull(namespace, "namespace"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java new file mode 100644 index 000000000..83cf21982 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -0,0 +1,38 @@ +package fr.inria.corese.core.next.query.impl.sparql.ast; + +import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.io.common.IOConstants; + +import java.util.List; + +/** + * Snapshot of the SPARQL prologue: prefix declarations in source order and the effective base IRI + * after the prologue (parser options initial base, possibly overridden by {@code BASE}). + *

+ * For now this type is only attached to {@link SelectQueryAst}; other query forms still expose + * prefix/base state via {@link fr.inria.corese.core.next.data.api.IPrefixHandler} on {@link QueryAst}. + */ +public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri) { + public QueryPrologueAst { + prefixDeclarations = prefixDeclarations != null ? List.copyOf(prefixDeclarations) : List.of(); + if (baseIri == null) { + baseIri = new IriAst(IOConstants.getDefaultBaseURI()); + } + } + + public static QueryPrologueAst empty() { + return new QueryPrologueAst(List.of(), new IriAst(IOConstants.getDefaultBaseURI())); + } + + /** + * Rebuilds a {@link PrefixHandler} with the same effective mappings as while parsing. + */ + public PrefixHandler toPrefixHandler() { + PrefixHandler h = new PrefixHandler(); + h.setDefaultNamespace(baseIri.raw()); + for (PrefixDeclarationAst d : prefixDeclarations) { + h.setPrefix(d.prefix(), d.namespace().raw()); + } + return h; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index b4680c5b1..3cd95529e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -1,28 +1,29 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.data.api.IPrefixHandler; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; /** * Abstract Syntax Tree (AST) representation of a SPARQL {@code SELECT} query. * Holds the projection (SELECT * or SELECT ?v1 ?v2 ...) and the WHERE clause. + *

+ * {@link #prologue()} captures PREFIX/BASE for SELECT; {@link #prefixHandler()} is derived from it + * for {@link QueryAst} compatibility. */ -public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, PrefixHandler prefixHandler) implements QueryAst { +public record SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, QueryPrologueAst prologue) implements QueryAst { /** Constructor with default projection SELECT *. */ public SelectQueryAst(GroupGraphPatternAst whereClause) { this(ProjectionAsts.selectAll(), DatasetClauseAst.none(), whereClause); } - /** Constructor with default solution modifier (no DISTINCT/REDUCED/ORDER BY/LIMIT/OFFSET) and default prefix handler. */ + /** Constructor with default solution modifier (no DISTINCT/REDUCED/ORDER BY/LIMIT/OFFSET) and default prologue. */ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause) { this(projection, datasetClause, whereClause, null, null); } - /** Constructor with default prefix handler */ + /** Constructor with default prologue */ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier) { this(projection, datasetClause, whereClause, solutionModifier, null); } @@ -31,7 +32,7 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, if (projection == null) { projection = ProjectionAsts.selectAll(); } - if(datasetClause == null) { + if (datasetClause == null) { datasetClause = DatasetClauseAst.none(); } if (whereClause == null) { @@ -40,9 +41,13 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, if (solutionModifier == null) { solutionModifier = SolutionModifierAst.empty(); } - if (prefixHandler == null) { - prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); + if (prologue == null) { + prologue = QueryPrologueAst.empty(); } } -} \ No newline at end of file + + @Override + public IPrefixHandler prefixHandler() { + return prologue.toPrefixHandler(); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index 5e00edb6c..b954e8167 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -1,6 +1,7 @@ package fr.inria.corese.core.next.query.impl.parser; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -72,8 +73,8 @@ public void selectWithBase() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + SelectQueryAst ast = (SelectQueryAst) parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); } @Test @@ -89,12 +90,12 @@ public void selectWithBaseAndOnePrefix() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); - assertTrue(ast.prefixHandler().hasPrefix("test")); - assertEquals("test", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); - assertEquals("https://ns.inria.fr/otherTest/#", ast.prefixHandler().getNamespace("test")); + SelectQueryAst ast = (SelectQueryAst) parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test")); + assertEquals("test", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); + assertEquals("https://ns.inria.fr/otherTest/#", ast.prologue().toPrefixHandler().getNamespace("test")); } @Test @@ -112,20 +113,20 @@ public void selectWithBaseAndMultiplePrefix() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); - assertTrue(ast.prefixHandler().hasPrefix("test1")); - assertEquals("test1", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest1/#", ast.prefixHandler().getNamespace("test1")); - assertTrue(ast.prefixHandler().hasPrefix("test2")); - assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test2")); - assertTrue(ast.prefixHandler().hasPrefix("test3")); - assertEquals("test3", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); - assertEquals("https://ns.inria.fr/otherTest3/#", ast.prefixHandler().getNamespace("test3")); + SelectQueryAst ast = (SelectQueryAst) parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); + assertEquals("test1", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().toPrefixHandler().getNamespace("test1")); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test2")); + assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test2")); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test3")); + assertEquals("test3", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); + assertEquals("https://ns.inria.fr/otherTest3/#", ast.prologue().toPrefixHandler().getNamespace("test3")); } @Test @@ -149,19 +150,19 @@ public void selectWithBaseAndMultiplePrefixWithOverlap() { SparqlParser parser = newParserDefault(); - QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); - assertTrue(ast.prefixHandler().hasPrefix("test1")); - assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test1")); - assertTrue(ast.prefixHandler().hasPrefix("test2")); - assertEquals("test2", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest1/#", ast.prefixHandler().getNamespace("test2")); - assertTrue(ast.prefixHandler().hasPrefix("test1")); - assertEquals("test1", ast.prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); - assertTrue(ast.prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prefixHandler().getNamespace("test1")); + SelectQueryAst ast = (SelectQueryAst) parser.parse(query); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); + assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test1")); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test2")); + assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().toPrefixHandler().getNamespace("test2")); + assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); + assertEquals("test1", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); + assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test1")); } @Test From 60e562c7756d0205b780ff052b3f66c02af48b84 Mon Sep 17 00:00:00 2001 From: pierrerene Date: Thu, 26 Mar 2026 10:06:31 +0100 Subject: [PATCH 04/12] prologue handling proposition for select query --- .../core/next/query/impl/sparql/ast/QueryPrologueAst.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 83cf21982..814e40a0c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -28,11 +28,11 @@ public static QueryPrologueAst empty() { * Rebuilds a {@link PrefixHandler} with the same effective mappings as while parsing. */ public PrefixHandler toPrefixHandler() { - PrefixHandler h = new PrefixHandler(); - h.setDefaultNamespace(baseIri.raw()); + PrefixHandler prefixHandler = new PrefixHandler(); + prefixHandler.setDefaultNamespace(baseIri.raw()); for (PrefixDeclarationAst d : prefixDeclarations) { - h.setPrefix(d.prefix(), d.namespace().raw()); + prefixHandler.setPrefix(d.prefix(), d.namespace().raw()); } - return h; + return prefixHandler; } } From d9fd01eb159431f3b887d6b6e9ce9490681ad682 Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Thu, 26 Mar 2026 11:14:51 +0100 Subject: [PATCH 05/12] extending queryPrologueAst mechanism to all queries --- .../core/next/query/impl/parser/SparqlAstBuilder.java | 3 +-- .../core/next/query/impl/sparql/ast/AskQueryAst.java | 7 +++---- .../core/next/query/impl/sparql/ast/ConstructQueryAst.java | 7 +++---- .../core/next/query/impl/sparql/ast/DescribeQueryAst.java | 7 +++---- .../corese/core/next/query/impl/sparql/ast/QueryAst.java | 2 +- .../core/next/query/impl/sparql/ast/SelectQueryAst.java | 5 ----- .../next/query/impl/parser/SparqlParserPrologueTest.java | 6 +++--- 7 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 221dadeff..0d8e8f92a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -370,8 +370,7 @@ public QueryAst getResult() { throw new IllegalStateException("No WHERE clause: did you call exitGroup() for the top-level GroupGraphPattern?"); } DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); - QueryPrologueAst selectPrologue = new QueryPrologueAst(List.copyOf(prefixDeclarations), new IriAst(baseUri)); - PrefixHandler resolvedPrefixes = selectPrologue.toPrefixHandler(); + QueryPrologueAst prologueAst = new QueryPrologueAst(List.copyOf(prefixDeclarations), new IriAst(baseUri)); return switch (this.queryType) { case ASK -> buildAskQueryAst(datasetClauseAst); case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index b76fcfa93..9f0306377 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -18,7 +18,7 @@ * } * } */ -public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, PrefixHandler prefixHandler) implements QueryAst { +public record AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, QueryPrologueAst prologue) implements QueryAst { /** * constructor with default prefix handler */ @@ -33,9 +33,8 @@ public AskQueryAst(DatasetClauseAst datasetClause, GroupGraphPatternAst whereCla if (datasetClause == null) { datasetClause = DatasetClauseAst.none(); } - if (prefixHandler == null) { - prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); + if (prologue == null) { + prologue = QueryPrologueAst.empty(); } } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index 26f8fded1..2ef164d5e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -38,7 +38,7 @@ public record ConstructQueryAst( DatasetClauseAst datasetClause, GroupGraphPatternAst whereClause, SolutionModifierAst solutionModifier, - PrefixHandler prefixHandler + QueryPrologueAst prologue ) implements QueryAst { public ConstructQueryAst { if (constructTemplate == null) { @@ -53,9 +53,8 @@ public record ConstructQueryAst( if (solutionModifier == null) { solutionModifier = SolutionModifierAst.empty(); } - if (prefixHandler == null) { - prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); + if (prologue == null) { + prologue = QueryPrologueAst.empty(); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index eab9d38f9..53a3f3a68 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -24,7 +24,7 @@ * } * } */ -public record DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause, PrefixHandler prefixHandler) implements QueryAst { +public record DescribeQueryAst(DatasetClauseAst datasetClause, List described, GroupGraphPatternAst whereClause, QueryPrologueAst prologue) implements QueryAst { public DescribeQueryAst { described = described != null ? List.copyOf(described) : List.of(); if (whereClause == null) { @@ -33,9 +33,8 @@ public record DescribeQueryAst(DatasetClauseAst datasetClause, List des if(datasetClause == null) { datasetClause = DatasetClauseAst.none(); } - if(prefixHandler == null) { - prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(IOConstants.getDefaultBaseURI()); + if(prologue == null) { + prologue = QueryPrologueAst.empty(); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java index ef94e132f..cf1b64c74 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java @@ -9,5 +9,5 @@ public sealed interface QueryAst permits AskQueryAst, ConstructQueryAst, DescribeQueryAst, SelectQueryAst { DatasetClauseAst datasetClause(); GroupGraphPatternAst whereClause(); - IPrefixHandler prefixHandler(); + QueryPrologueAst prologue(); } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index 3cd95529e..29e668f7b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -45,9 +45,4 @@ public SelectQueryAst(ProjectionAst projection, DatasetClauseAst datasetClause, prologue = QueryPrologueAst.empty(); } } - - @Override - public IPrefixHandler prefixHandler() { - return prologue.toPrefixHandler(); - } } diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index b954e8167..43a7ab0dd 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -23,7 +23,7 @@ public void askWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); } @Test @@ -42,7 +42,7 @@ public void constructWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); } @Test @@ -58,7 +58,7 @@ public void describeWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); } @Test From edd698546dcbf5f6dd32740aa1a0c5e02beb51eb Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Thu, 26 Mar 2026 16:47:48 +0100 Subject: [PATCH 06/12] Fix empty prefixes, prefix overlap and relative namespaces --- .../next/data/impl/common/util/IRIUtils.java | 21 ++++ .../query/impl/parser/SparqlAstBuilder.java | 24 ++--- .../impl/sparql/ast/PrefixDeclarationAst.java | 4 +- .../impl/sparql/ast/QueryPrologueAst.java | 53 +++++++++-- .../data/impl/common/util/IRIUtilsTest.java | 17 ++++ .../impl/parser/SparqlParserPrologueTest.java | 95 +++++++++++-------- 6 files changed, 146 insertions(+), 68 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java index fe2abff8f..20a573565 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java @@ -27,6 +27,13 @@ public class IRIUtils { "(?[\\w\\-_]+)?" + // line1 "$" ); + private static final Pattern ABSOLUTE_IRI_PATTERN = Pattern.compile("^(?(" + + "?[\\w\\-]+)" + + ":(\\/\\/)?" + + "(?([\\S\\-\\._\\:]+[\\/\\.\\:\\@\\-])?)+" + + "(?\\?[\\S\\-\\\"\\'_\\:\\?\\=]+)?)" + + "(?[\\S\\-_]+)?" + + "$"); private static final Pattern STANDARD_IRI_PATTERN = Pattern.compile("^(([^:/?#\\s]+):)(\\/\\/([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(.*))?"); private static final int MAX_IRI_LENGTH = 2048; private static final long REGEX_TIMEOUT_MS = 100; @@ -115,6 +122,20 @@ public static String guessLocalName(String iri) { } } + /** + * Detects if an IRI is absolute according to the REGEX given in the recommendation RFC3987 + * @param iri any uri (expecting to be the content between < and > + * @return true if it is compliant with RFC3987. May accept the prefixed for of uri, as there is no way to + * distinguish a prefix from a protocol + */ + public static boolean isAbsoluteIRI(String iri) { + Matcher matcher = matchWithTimeout(ABSOLUTE_IRI_PATTERN, iri); + if (matcher == null || !matcher.matches()) { + return false; + } + return matcher.matches(); + } + /** * Checks if the given string is a valid IRI using a regex pattern extracted from the W3C standards. * @param iriString The string to be checked. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 0d8e8f92a..af4f9fbc2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -127,38 +127,26 @@ public final class SparqlAstBuilder { * Prefix declarations in source order (including redeclarations). */ private final List prefixDeclarations = new ArrayList<>(); - /** - * Mutable view of prefix mappings for resolution while building; kept in sync with {@link #prefixDeclarations}. - */ - private final PrefixHandler prefixHandler = new PrefixHandler(); public SparqlAstBuilder(SparqlParserOptions options) { this.options = options; this.baseUri = options.getBaseIRI(); - this.prefixHandler.setDefaultNamespace(this.baseUri); } // --- Construction entry points (called by listener) --- public void setBaseUri(String uri) { - if(uri.startsWith("<") && uri.endsWith(">")) { - uri = uri.substring(0, uri.lastIndexOf(">")); - uri = uri.substring(uri.indexOf("<")+1); - } this.baseUri = uri; - this.prefixHandler.setDefaultNamespace(uri); } public void addPrefix(String prefix, String uri) { - if(prefix.endsWith(":")) { - prefix = prefix.substring(0, prefix.lastIndexOf(":")); - } - if(uri.startsWith("<") && uri.endsWith(">")) { - uri = uri.substring(0, uri.lastIndexOf(">")); - uri = uri.substring(uri.indexOf("<") +1); + PrefixDeclarationAst declarationAst = new PrefixDeclarationAst(prefix, new IriAst(uri)); + if(this.prefixDeclarations.stream().anyMatch(declaration -> + Objects.equals(declaration.prefix(), declarationAst.prefix()) + )) { + throw new QuerySyntaxException("Prefix " + prefix + " has already been declared"); } - this.prefixHandler.setPrefix(prefix, uri); - this.prefixDeclarations.add(new PrefixDeclarationAst(prefix, new IriAst(uri))); + this.prefixDeclarations.add(declarationAst); } public void enterAskQuery() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java index ede5467db..2ae63bd5c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java @@ -7,8 +7,8 @@ */ public record PrefixDeclarationAst(String prefix, IriAst namespace) { public PrefixDeclarationAst { - if (prefix == null || prefix.isEmpty()) { - throw new IllegalArgumentException("prefix must be non-null and non-empty"); + if (prefix == null ) { + throw new IllegalArgumentException("prefix must be non-null"); } namespace = Objects.requireNonNull(namespace, "namespace"); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 814e40a0c..9a4935abe 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -1,7 +1,9 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; +import fr.inria.corese.core.next.data.impl.common.util.IRIUtils; import fr.inria.corese.core.next.data.impl.io.common.IOConstants; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import java.util.List; @@ -12,11 +14,39 @@ * For now this type is only attached to {@link SelectQueryAst}; other query forms still expose * prefix/base state via {@link fr.inria.corese.core.next.data.api.IPrefixHandler} on {@link QueryAst}. */ -public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri) { +public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri, PrefixHandler prefixHandler) { + public QueryPrologueAst(List prefixDeclarations, IriAst baseIri) { + this(prefixDeclarations, baseIri, null); + } + public QueryPrologueAst { prefixDeclarations = prefixDeclarations != null ? List.copyOf(prefixDeclarations) : List.of(); if (baseIri == null) { baseIri = new IriAst(IOConstants.getDefaultBaseURI()); + } else { + baseIri = new IriAst(trimURI(baseIri.raw())); + } + if (prefixHandler == null) { + prefixHandler = new PrefixHandler(); + if(! IRIUtils.isAbsoluteIRI(baseIri.raw())) { + throw new QuerySyntaxException("Base IRI should be absolute, got " + baseIri.raw()); + } + prefixHandler.setDefaultNamespace(baseIri.raw()); + for (PrefixDeclarationAst d : prefixDeclarations) { + String prefix = trimPrefix(d.prefix()); + if(prefixHandler.hasPrefix(prefix)) { + throw new QuerySyntaxException("Prefix " + prefix + " is declared twice in query"); + } + String namespace = trimURI(d.namespace().raw()); + if(! IRIUtils.isAbsoluteIRI(namespace)) { + if(IRIUtils.isAbsoluteIRI(prefixHandler.getDefaultNamespace() + namespace)) { + namespace = prefixHandler.getDefaultNamespace() + namespace; + } else { + throw new QuerySyntaxException(namespace + " should be absolute or resolve to an absolute IRI using the base IRI"); + } + } + prefixHandler.setPrefix(prefix, namespace); + } } } @@ -24,15 +54,18 @@ public static QueryPrologueAst empty() { return new QueryPrologueAst(List.of(), new IriAst(IOConstants.getDefaultBaseURI())); } - /** - * Rebuilds a {@link PrefixHandler} with the same effective mappings as while parsing. - */ - public PrefixHandler toPrefixHandler() { - PrefixHandler prefixHandler = new PrefixHandler(); - prefixHandler.setDefaultNamespace(baseIri.raw()); - for (PrefixDeclarationAst d : prefixDeclarations) { - prefixHandler.setPrefix(d.prefix(), d.namespace().raw()); + private static String trimURI(String uri) { + if(uri.startsWith("<") && uri.endsWith(">")) { + uri = uri.substring(0, uri.lastIndexOf(">")); + uri = uri.substring(uri.indexOf("<") +1); + } + return uri; + } + + private static String trimPrefix(String prefix) { + if(prefix.endsWith(":")) { + prefix = prefix.substring(0, prefix.lastIndexOf(":")); } - return prefixHandler; + return prefix; } } diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtilsTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtilsTest.java index 59041d4a8..89005dbae 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtilsTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtilsTest.java @@ -75,6 +75,23 @@ public void isStandardIRITest() { } } + @Test + public void isAbsoluteIRITest() { + assertTrue(IRIUtils.isAbsoluteIRI("mailto://user@example.com")); + assertTrue(IRIUtils.isAbsoluteIRI("mongodb://user:password@127.0.0.1:3307")); + assertTrue(IRIUtils.isAbsoluteIRI("https://laconsole.dev")); + assertTrue(IRIUtils.isAbsoluteIRI("http://127.0.0.1:3000"));; + assertTrue(IRIUtils.isAbsoluteIRI("urn:isbn:978-2-7654-0912-0")); + assertTrue(IRIUtils.isAbsoluteIRI("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6")); + assertTrue(IRIUtils.isAbsoluteIRI("urn:ietf:rfc:2648")); + assertTrue(IRIUtils.isAbsoluteIRI("https://www.w3.org/TR/rdf-sparql-query/#iriRefs")); + assertTrue(IRIUtils.isAbsoluteIRI("https://ns.inria.fr/otherTest1/#")); + assertTrue(IRIUtils.isAbsoluteIRI("https://www.w3.org/TR/rdf-sparql-query/#iriRefs")); + assertTrue(IRIUtils.isAbsoluteIRI("http://xmlns.com/foaf/0.1/")); + assertFalse(IRIUtils.isAbsoluteIRI("child/password@127.0.0.1:3307")); + assertFalse(IRIUtils.isAbsoluteIRI("child/otherChild/otherotherchild/#patate")); + } + /** * Helper method to escape strings for display in test failure messages */ diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index 43a7ab0dd..d7cd73e31 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -23,7 +23,7 @@ public void askWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); } @Test @@ -42,7 +42,7 @@ public void constructWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); } @Test @@ -58,7 +58,7 @@ public void describeWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().toPrefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); } @Test @@ -92,10 +92,10 @@ public void selectWithBaseAndOnePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test")); - assertEquals("test", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); - assertEquals("https://ns.inria.fr/otherTest/#", ast.prologue().toPrefixHandler().getNamespace("test")); + assertTrue(ast.prologue().prefixHandler().hasPrefix("test")); + assertEquals("test", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); + assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); + assertEquals("https://ns.inria.fr/otherTest/#", ast.prologue().prefixHandler().getNamespace("test")); } @Test @@ -115,18 +115,18 @@ public void selectWithBaseAndMultiplePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); - assertEquals("test1", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().toPrefixHandler().getNamespace("test1")); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test2")); - assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test2")); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test3")); - assertEquals("test3", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); - assertEquals("https://ns.inria.fr/otherTest3/#", ast.prologue().toPrefixHandler().getNamespace("test3")); + assertTrue(ast.prologue().prefixHandler().hasPrefix("test1")); + assertEquals("test1", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); + assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); + assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().prefixHandler().getNamespace("test1")); + assertTrue(ast.prologue().prefixHandler().hasPrefix("test2")); + assertEquals("test2", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); + assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); + assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().prefixHandler().getNamespace("test2")); + assertTrue(ast.prologue().prefixHandler().hasPrefix("test3")); + assertEquals("test3", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); + assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); + assertEquals("https://ns.inria.fr/otherTest3/#", ast.prologue().prefixHandler().getNamespace("test3")); } @Test @@ -141,28 +141,12 @@ public void selectWithBaseAndMultiplePrefixWithOverlap() { ?s ?p ?o . } LIMIT 10 """; - // test1 -> ns:otherTest1 - // ns:otherTest1 -> test1 - // test2 -> ns:otherTest1 - // ns:otherTest1 -> test2 - // test1 -> ns:otherTest2 - // ns:otherTest2 -> test1 SparqlParser parser = newParserDefault(); - SelectQueryAst ast = (SelectQueryAst) parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); - assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test1")); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test2")); - assertEquals("test2", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().toPrefixHandler().getNamespace("test2")); - assertTrue(ast.prologue().toPrefixHandler().hasPrefix("test1")); - assertEquals("test1", ast.prologue().toPrefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); - assertTrue(ast.prologue().toPrefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().toPrefixHandler().getNamespace("test1")); + assertThrows(QuerySyntaxException.class, () -> { + parser.parse(query); + }); } @Test @@ -182,4 +166,39 @@ public void selectWithMultipleBase() { parser.parse(query); }); } + + @Test + @DisplayName("PREFIX with empty prefix label should be accepted") + public void selectWithDefaultPrefixDeclaration() { + String query = """ + PREFIX : + SELECT * { + ?s :p ?o . + } + """; + + SparqlParser parser = newParserDefault(); + + SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); + assertTrue(ast.prologue().prefixHandler().hasPrefix("")); + assertEquals("", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/default/#")); + assertEquals("https://ns.inria.fr/default/#", ast.prologue().prefixHandler().getNamespace("")); + } + + @Test + @DisplayName("Relative PREFIX IRI should be resolved against effective base") + public void relativePrefixShouldBeResolvedAgainstEffectiveBase() { + String query = """ + BASE + PREFIX ex: + SELECT * { + ?s ex:p ?o . + } + """; + + SparqlParser parser = newParserDefault(); + + SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); + assertEquals("http://example.org/root/ns/", ast.prologue().prefixHandler().getNamespace("ex")); + } } From f398fd4d4c60b79504209d80b80e53734ebf183a Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Thu, 26 Mar 2026 17:09:54 +0100 Subject: [PATCH 07/12] removing unused imports --- .../corese/core/next/query/impl/parser/SparqlAstBuilder.java | 2 -- .../corese/core/next/query/impl/sparql/ast/AskQueryAst.java | 3 --- .../core/next/query/impl/sparql/ast/ConstructQueryAst.java | 3 --- .../core/next/query/impl/sparql/ast/DescribeQueryAst.java | 3 --- .../inria/corese/core/next/query/impl/sparql/ast/QueryAst.java | 2 -- .../corese/core/next/query/impl/sparql/ast/SelectQueryAst.java | 2 -- 6 files changed, 15 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index af4f9fbc2..be46afddc 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -1,8 +1,6 @@ package fr.inria.corese.core.next.query.impl.parser; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java index 9f0306377..ade193d8c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/AskQueryAst.java @@ -1,8 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; - import java.util.List; /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java index 2ef164d5e..9c30bca4d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/ConstructQueryAst.java @@ -1,8 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; - import java.util.List; /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java index 53a3f3a68..6043ba473 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/DescribeQueryAst.java @@ -1,8 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; -import fr.inria.corese.core.next.data.impl.io.common.IOConstants; - import java.util.List; /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java index cf1b64c74..6df83c7bd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryAst.java @@ -1,7 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.api.IPrefixHandler; - /** * Minimal SPARQL query AST (SELECT, ASK, CONSTRUCT, DESCRIBE). * Holds the WHERE clause as a group graph pattern; query-specific projection/template can be added later. diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java index 29e668f7b..fbcf02164 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/SelectQueryAst.java @@ -1,7 +1,5 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.api.IPrefixHandler; - import java.util.List; /** From b6bec394c4922b3d9cff0c815a3486ee9a43961e Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Mon, 30 Mar 2026 11:42:29 +0200 Subject: [PATCH 08/12] Removing prefixHandler --- .../next/data/impl/common/util/IRIUtils.java | 75 -------------- .../common/AbstractTurtleTriGListener.java | 5 +- .../impl/sparql/ast/PrefixDeclarationAst.java | 7 ++ .../impl/sparql/ast/QueryPrologueAst.java | 49 +--------- .../corese/core/next/util/StringUtils.java | 97 +++++++++++++++++++ .../impl/parser/SparqlParserPrologueTest.java | 63 +++++++----- 6 files changed, 151 insertions(+), 145 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/util/StringUtils.java diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java index 20a573565..fc217a80f 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java @@ -281,79 +281,4 @@ public static boolean isInvalidIRICharacter(char c) { }; } - /** - * Returns a human-readable description of a character for error messages. - * - * @param c the character to describe - * @return human-readable description - */ - public static String getCharacterDescription(char c) { - switch (c) { - case 0x00: - return "null character"; - case 0x09: - return "tab"; - case 0x0A: - return "line feed"; - case 0x0D: - return "carriage return"; - case 0x20: - return "space"; - case 0x7F: - return "delete"; - case '<': - return "less than"; - case '>': - return "greater than"; - case '{': - return "left curly bracket"; - case '}': - return "right curly bracket"; - case '\\': - return "backslash"; - case '^': - return "circumflex"; - case '`': - return "grave accent"; - case '|': - return "pipe"; - case '"': - return "quotation mark"; - default: - if (c < 0x20) { - return "control character"; - } else if (c >= 0x80 && c <= 0x9F) { - return "high control character"; - } else { - return String.format("character '%c'", c); - } - } - } - - /** - * Escapes characters in a string for display in error messages. - * - * @param iri the IRI to escape for display - * @return escaped version suitable for error messages - */ - public static String escapeForDisplay(String iri) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < iri.length(); i++) { - char c = iri.charAt(i); - if (c < 0x20 || (c >= 0x7F && c <= 0x9F)) { - // Display control characters as Unicode escapes - sb.append(String.format("\\u%04X", (int) c)); - } else if (c > 0x7E) { - // Display non-ASCII as Unicode escapes for clarity - sb.append(String.format("\\u%04X", (int) c)); - } else if (c == '<' || c == '>' || c == '{' || c == '}' || c == '\\' || c == '^' || c == '`' || c == '|' || c == '"') { - // Display reserved characters with backslash escape - sb.append('\\').append(c); - } else { - // Display normal ASCII characters as-is - sb.append(c); - } - } - return sb.toString(); - } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java index 9e7d120e2..21a606c0a 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java @@ -7,6 +7,7 @@ import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; import fr.inria.corese.core.next.data.impl.exception.ParsingErrorException; import fr.inria.corese.core.next.data.impl.io.parser.util.ParserConstants; +import fr.inria.corese.core.next.util.StringUtils; import java.net.URI; import java.net.URISyntaxException; @@ -652,8 +653,8 @@ private void validateIRI(String iri) throws ParsingErrorException { // Check for forbidden characters if (IRIUtils.isInvalidIRICharacter(c)) { String codePoint = String.format("U+%04X", (int) c); - String charDesc = IRIUtils.getCharacterDescription(c); - String displayIRI = IRIUtils.escapeForDisplay(iri); + String charDesc = StringUtils.getCharacterDescription(c); + String displayIRI = StringUtils.escapeForDisplay(iri); throw new ParsingErrorException( "Invalid character in IRI: " + codePoint + " (" + charDesc + ") " + diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java index 2ae63bd5c..8957ffb5c 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/PrefixDeclarationAst.java @@ -2,6 +2,9 @@ import java.util.Objects; +import static fr.inria.corese.core.next.util.StringUtils.trimChevronIRIs; +import static fr.inria.corese.core.next.util.StringUtils.trimPrefixWithColon; + /** * A {@code PREFIX p: <ns>} declaration from the SPARQL prologue ({@code p} without trailing colon). */ @@ -10,6 +13,10 @@ public record PrefixDeclarationAst(String prefix, IriAst namespace) { if (prefix == null ) { throw new IllegalArgumentException("prefix must be non-null"); } + prefix = trimPrefixWithColon(prefix); namespace = Objects.requireNonNull(namespace, "namespace"); + if(! namespace.raw().isEmpty()) { + namespace = new IriAst(trimChevronIRIs(namespace.raw())); + } } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 9a4935abe..2ad0cf823 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -1,12 +1,11 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; -import fr.inria.corese.core.next.data.impl.common.prefix.PrefixHandler; -import fr.inria.corese.core.next.data.impl.common.util.IRIUtils; import fr.inria.corese.core.next.data.impl.io.common.IOConstants; -import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import java.util.List; +import static fr.inria.corese.core.next.util.StringUtils.trimChevronIRIs; + /** * Snapshot of the SPARQL prologue: prefix declarations in source order and the effective base IRI * after the prologue (parser options initial base, possibly overridden by {@code BASE}). @@ -14,58 +13,18 @@ * For now this type is only attached to {@link SelectQueryAst}; other query forms still expose * prefix/base state via {@link fr.inria.corese.core.next.data.api.IPrefixHandler} on {@link QueryAst}. */ -public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri, PrefixHandler prefixHandler) { - public QueryPrologueAst(List prefixDeclarations, IriAst baseIri) { - this(prefixDeclarations, baseIri, null); - } +public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri) { public QueryPrologueAst { prefixDeclarations = prefixDeclarations != null ? List.copyOf(prefixDeclarations) : List.of(); if (baseIri == null) { baseIri = new IriAst(IOConstants.getDefaultBaseURI()); } else { - baseIri = new IriAst(trimURI(baseIri.raw())); - } - if (prefixHandler == null) { - prefixHandler = new PrefixHandler(); - if(! IRIUtils.isAbsoluteIRI(baseIri.raw())) { - throw new QuerySyntaxException("Base IRI should be absolute, got " + baseIri.raw()); - } - prefixHandler.setDefaultNamespace(baseIri.raw()); - for (PrefixDeclarationAst d : prefixDeclarations) { - String prefix = trimPrefix(d.prefix()); - if(prefixHandler.hasPrefix(prefix)) { - throw new QuerySyntaxException("Prefix " + prefix + " is declared twice in query"); - } - String namespace = trimURI(d.namespace().raw()); - if(! IRIUtils.isAbsoluteIRI(namespace)) { - if(IRIUtils.isAbsoluteIRI(prefixHandler.getDefaultNamespace() + namespace)) { - namespace = prefixHandler.getDefaultNamespace() + namespace; - } else { - throw new QuerySyntaxException(namespace + " should be absolute or resolve to an absolute IRI using the base IRI"); - } - } - prefixHandler.setPrefix(prefix, namespace); - } + baseIri = new IriAst(trimChevronIRIs(baseIri.raw())); } } public static QueryPrologueAst empty() { return new QueryPrologueAst(List.of(), new IriAst(IOConstants.getDefaultBaseURI())); } - - private static String trimURI(String uri) { - if(uri.startsWith("<") && uri.endsWith(">")) { - uri = uri.substring(0, uri.lastIndexOf(">")); - uri = uri.substring(uri.indexOf("<") +1); - } - return uri; - } - - private static String trimPrefix(String prefix) { - if(prefix.endsWith(":")) { - prefix = prefix.substring(0, prefix.lastIndexOf(":")); - } - return prefix; - } } diff --git a/src/main/java/fr/inria/corese/core/next/util/StringUtils.java b/src/main/java/fr/inria/corese/core/next/util/StringUtils.java new file mode 100644 index 000000000..1cf3b3601 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/util/StringUtils.java @@ -0,0 +1,97 @@ +package fr.inria.corese.core.next.util; + +public class StringUtils { + + public static String trimChevronIRIs(String uri) { + uri = uri.trim(); + if(uri.startsWith("<") && uri.endsWith(">")) { + uri = uri.substring(0, uri.lastIndexOf(">")); + uri = uri.substring(uri.indexOf("<") +1); + } + return uri; + } + + public static String trimPrefixWithColon(String prefix) { + prefix = prefix.trim(); + if(prefix.endsWith(":")) { + prefix = prefix.substring(0, prefix.lastIndexOf(":")); + } + return prefix; + } + + /** + * Returns a human-readable description of a character for error messages. + * + * @param c the character to describe + * @return human-readable description + */ + public static String getCharacterDescription(char c) { + switch (c) { + case 0x00: + return "null character"; + case 0x09: + return "tab"; + case 0x0A: + return "line feed"; + case 0x0D: + return "carriage return"; + case 0x20: + return "space"; + case 0x7F: + return "delete"; + case '<': + return "less than"; + case '>': + return "greater than"; + case '{': + return "left curly bracket"; + case '}': + return "right curly bracket"; + case '\\': + return "backslash"; + case '^': + return "circumflex"; + case '`': + return "grave accent"; + case '|': + return "pipe"; + case '"': + return "quotation mark"; + default: + if (c < 0x20) { + return "control character"; + } else if (c >= 0x80 && c <= 0x9F) { + return "high control character"; + } else { + return String.format("character '%c'", c); + } + } + } + + /** + * Escapes characters in a string for display in error messages. + * + * @param iri the IRI to escape for display + * @return escaped version suitable for error messages + */ + public static String escapeForDisplay(String iri) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < iri.length(); i++) { + char c = iri.charAt(i); + if (c < 0x20 || (c >= 0x7F && c <= 0x9F)) { + // Display control characters as Unicode escapes + sb.append(String.format("\\u%04X", (int) c)); + } else if (c > 0x7E) { + // Display non-ASCII as Unicode escapes for clarity + sb.append(String.format("\\u%04X", (int) c)); + } else if (c == '<' || c == '>' || c == '{' || c == '}' || c == '\\' || c == '^' || c == '`' || c == '|' || c == '"') { + // Display reserved characters with backslash escape + sb.append('\\').append(c); + } else { + // Display normal ASCII characters as-is + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index d7cd73e31..3ac9cd5ab 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -5,11 +5,15 @@ import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.junit.jupiter.api.Assertions.*; public class SparqlParserPrologueTest extends AbstractSparqlParserFeatureTest { + private static final Logger logger = LoggerFactory.getLogger(SparqlParserPrologueTest.class); + @Test @DisplayName("Basic Ask with base") public void askWithBase() { @@ -23,7 +27,7 @@ public void askWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); } @Test @@ -42,7 +46,7 @@ public void constructWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); } @Test @@ -58,7 +62,7 @@ public void describeWithBase() { SparqlParser parser = newParserDefault(); QueryAst ast = parser.parse(query); - assertEquals("http://ns.inria.fr/test/", ast.prologue().prefixHandler().getDefaultNamespace()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); } @Test @@ -91,11 +95,12 @@ public void selectWithBaseAndOnePrefix() { SparqlParser parser = newParserDefault(); SelectQueryAst ast = (SelectQueryAst) parser.parse(query); + + logger.debug("{}", ast.prologue()); + assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().prefixHandler().hasPrefix("test")); - assertEquals("test", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest/#")); - assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest/#")); - assertEquals("https://ns.inria.fr/otherTest/#", ast.prologue().prefixHandler().getNamespace("test")); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest/#"))); } @Test @@ -115,18 +120,12 @@ public void selectWithBaseAndMultiplePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().prefixHandler().hasPrefix("test1")); - assertEquals("test1", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest1/#")); - assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest1/#")); - assertEquals("https://ns.inria.fr/otherTest1/#", ast.prologue().prefixHandler().getNamespace("test1")); - assertTrue(ast.prologue().prefixHandler().hasPrefix("test2")); - assertEquals("test2", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest2/#")); - assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest2/#")); - assertEquals("https://ns.inria.fr/otherTest2/#", ast.prologue().prefixHandler().getNamespace("test2")); - assertTrue(ast.prologue().prefixHandler().hasPrefix("test3")); - assertEquals("test3", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/otherTest3/#")); - assertTrue(ast.prologue().prefixHandler().hasNamespace("https://ns.inria.fr/otherTest3/#")); - assertEquals("https://ns.inria.fr/otherTest3/#", ast.prologue().prefixHandler().getNamespace("test3")); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest1/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest2/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest3/#"))); } @Test @@ -180,9 +179,8 @@ public void selectWithDefaultPrefixDeclaration() { SparqlParser parser = newParserDefault(); SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); - assertTrue(ast.prologue().prefixHandler().hasPrefix("")); - assertEquals("", ast.prologue().prefixHandler().getPrefix("https://ns.inria.fr/default/#")); - assertEquals("https://ns.inria.fr/default/#", ast.prologue().prefixHandler().getNamespace("")); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().isEmpty())); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().isEmpty() && prefixDecl.namespace().raw().equals("https://ns.inria.fr/default/#"))); } @Test @@ -199,6 +197,25 @@ public void relativePrefixShouldBeResolvedAgainstEffectiveBase() { SparqlParser parser = newParserDefault(); SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); - assertEquals("http://example.org/root/ns/", ast.prologue().prefixHandler().getNamespace("ex")); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), "ex: is in the prologue"); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") && prefixDecl.namespace().raw().equals("http://example.org/root/ns/")), "the IRI of ex: in http://example.org/root/ns/"); + } + + @Test + @DisplayName("Relative PREFIX IRI should use RFC3986 resolution, not string concatenation") + public void relativePrefixShouldUseRfc3986Resolution() { + String query = """ + BASE + PREFIX ex: + SELECT * { + ?s ex:p ?o . + } + """; + + SparqlParser parser = newParserDefault(); + + SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), "ex: is in the prologue"); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") && prefixDecl.namespace().raw().equals("http://example.org/ns/")), "the IRI of ex: in http://example.org/ns/"); } } From f528903807d69bf452ba04fc596b17308046b09e Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Mon, 30 Mar 2026 13:42:29 +0200 Subject: [PATCH 09/12] resolving relative namespaces --- .../next/data/impl/common/util/IRIUtils.java | 274 +++++++++++++++++ .../common/AbstractTurtleTriGListener.java | 290 +----------------- .../impl/sparql/ast/QueryPrologueAst.java | 9 + 3 files changed, 287 insertions(+), 286 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java index fc217a80f..00a245a59 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/common/util/IRIUtils.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.data.impl.common.util; +import fr.inria.corese.core.next.data.impl.io.parser.util.ParserConstants; + import java.net.URI; import java.net.URISyntaxException; import java.util.Set; @@ -281,4 +283,276 @@ public static boolean isInvalidIRICharacter(char c) { }; } + public static String resolveIRIAgainstBase(String baseIri, String relativePath) { + + if (relativePath.isEmpty()) { + return baseIri; + } + + try { + URI baseUri = new URI(baseIri); + String baseScheme = baseUri.getScheme(); + String baseAuthority = baseUri.getAuthority(); + String basePath = baseUri.getPath(); + String baseQuery = baseUri.getQuery(); + + String[] refParts = parseReference(relativePath); + String refScheme = refParts[0]; + String refAuthority = refParts[1]; + String refPath = refParts[2]; + String refQuery = refParts[3]; + String refFragment = refParts[4]; + + String targetScheme, targetAuthority, targetPath, targetQuery, targetFragment; + + // RFC 3986 Section 5.2.2 - Reference Resolution Algorithm + if (refScheme != null) { + targetScheme = refScheme; + targetAuthority = refAuthority; + targetPath = removeDotSegments(refPath); + targetQuery = refQuery; + } else { + if (refAuthority != null) { + targetScheme = baseScheme; + targetAuthority = refAuthority; + targetPath = removeDotSegments(refPath); + targetQuery = refQuery; + } else { + targetScheme = baseScheme; + targetAuthority = baseAuthority; + if (refPath.isEmpty()) { + targetPath = basePath; + targetQuery = refQuery != null ? refQuery : baseQuery; + } else { + if (refPath.startsWith(ParserConstants.SLASH)) { + targetPath = removeDotSegments(refPath); + } else { + targetPath = removeDotSegments(mergePaths(basePath, refPath)); + } + targetQuery = refQuery; + } + } + } + targetFragment = refFragment; + + return buildURI(targetScheme, targetAuthority, targetPath, targetQuery, targetFragment); + + } catch (URISyntaxException e) { + return performSimpleFallback(baseIri, relativePath); + } + } + + /** + * Constructs a URI from its components. + * + * @param scheme URI scheme (e.g., "http", "file") + * @param authority authority component (host, port, userinfo) + * @param path path component + * @param query query component + * @param fragment fragment identifier + * @return normalized URI string + */ + private static String buildURI(String scheme, String authority, String path, String query, String fragment) { + StringBuilder result = new StringBuilder(); + if (scheme != null) { + result.append(scheme).append(ParserConstants.COLON); + } + if (authority != null) { + result.append(ParserConstants.DOUBLE_SLASH).append(authority); + } + if (path != null) { + result.append(path); + } + if (query != null) { + result.append(ParserConstants.QUERY_MARK).append(query); + } + if (fragment != null) { + result.append(ParserConstants.HASH).append(fragment); + } + return normalizeURI(result.toString()); + } + + /** + * Parses a URI reference into its five components. + * + * @param ref URI reference to parse + * @return array containing [scheme, authority, path, query, fragment] + */ + private static String[] parseReference(String ref) { + String[] parts = new String[5]; + String remaining = ref; + + int fragmentIndex = remaining.indexOf('#'); + if (fragmentIndex >= 0) { + parts[4] = remaining.substring(fragmentIndex + 1); + remaining = remaining.substring(0, fragmentIndex); + } + + int queryIndex = remaining.indexOf('?'); + if (queryIndex >= 0) { + parts[3] = remaining.substring(queryIndex + 1); + remaining = remaining.substring(0, queryIndex); + } + + int colonIndex = remaining.indexOf(':'); + if (colonIndex > 0 && isValidScheme(remaining.substring(0, colonIndex))) { + parts[0] = remaining.substring(0, colonIndex); + remaining = remaining.substring(colonIndex + 1); + } + + if (remaining.startsWith(ParserConstants.DOUBLE_SLASH)) { + int authorityEnd = remaining.indexOf('/', 2); + if (authorityEnd < 0) { + authorityEnd = remaining.length(); + } + parts[1] = remaining.substring(2, authorityEnd); + remaining = remaining.substring(authorityEnd); + } + + parts[2] = remaining; + return parts; + } + + /** + * Merges a base path with a relative path. + * + * @param basePath base path from base URI + * @param refPath relative path from reference + * @return merged path + */ + private static String mergePaths(String basePath, String refPath) { + if (basePath == null || basePath.isEmpty()) { + return ParserConstants.SLASH + refPath; + } + int lastSlash = basePath.lastIndexOf('/'); + return lastSlash >= 0 ? basePath.substring(0, lastSlash + 1) + refPath : refPath; + } + + /** + * Removes dot segments from a path (RFC 3986 Section 5.2.4). + * Processes ".." and "." segments according to the normalization algorithm. + * + * @param path path to normalize + * @return normalized path without dot segments + */ + private static String removeDotSegments(String path) { + if (path == null || path.isEmpty()) { + return ParserConstants.EMPTY_STRING; + } + + String input = path; + StringBuilder output = new StringBuilder(); + + while (!input.isEmpty()) { + if (input.startsWith(ParserConstants.DOUBLE_DOT + ParserConstants.SLASH)) { + input = input.substring(3); + } else if (input.startsWith(ParserConstants.DOT + ParserConstants.SLASH)) { + input = input.substring(2); + } else if (input.startsWith(ParserConstants.SLASH + ParserConstants.DOT + ParserConstants.SLASH)) { + input = ParserConstants.SLASH + input.substring(3); + } else if (input.equals(ParserConstants.SLASH + ParserConstants.DOT)) { + input = ParserConstants.SLASH; + } else if (input.startsWith(ParserConstants.SLASH + ParserConstants.DOUBLE_DOT + ParserConstants.SLASH)) { + input = ParserConstants.SLASH + input.substring(4); + removeLastSegment(output); + } else if (input.equals(ParserConstants.SLASH + ParserConstants.DOUBLE_DOT)) { + input = ParserConstants.SLASH; + removeLastSegment(output); + } else if (input.equals(ParserConstants.POINT) || input.equals(ParserConstants.DOUBLE_DOT)) { + input = ParserConstants.EMPTY_STRING; + } else { + int nextSlash; + if (input.startsWith(ParserConstants.SLASH)) { + nextSlash = input.indexOf(ParserConstants.SLASH, 1); + if (nextSlash >= 0) { + output.append(input, 0, nextSlash); + input = input.substring(nextSlash); + } else { + output.append(input); + input = ParserConstants.EMPTY_STRING; + } + } else { + nextSlash = input.indexOf(ParserConstants.SLASH); + if (nextSlash >= 0) { + output.append(input, 0, nextSlash); + input = input.substring(nextSlash); + } else { + output.append(input); + input = ParserConstants.EMPTY_STRING; + } + } + } + } + + return output.toString(); + } + + /** + * Removes the last path segment from the output buffer. + * Used during dot segment removal when processing ".." segments. + * + * @param output string builder containing the path being constructed + */ + private static void removeLastSegment(StringBuilder output) { + String outputStr = output.toString(); + int lastSlash = outputStr.lastIndexOf(ParserConstants.SLASH); + output.setLength(Math.max(lastSlash, 0)); + } + + /** + * Provides a fallback resolution mechanism when RFC 3986 parsing fails. + * + * @param base base URI + * @param relative relative IRI reference + * @return resolved IRI using simple concatenation rules + */ + private static String performSimpleFallback(String base, String relative) { + if (relative.isEmpty()) { + return base; + } + if (base.endsWith(ParserConstants.SLASH)) { + return base + relative; + } + int lastSlash = base.lastIndexOf('/'); + return lastSlash >= 0 ? base.substring(0, lastSlash + 1) + relative : base + ParserConstants.SLASH + relative; + } + + /** + * Normalizes URI strings, ensuring proper format for file:// URIs. + * + * @param uri URI to normalize + * @return normalized URI string + */ + public static String normalizeURI(String uri) { + if (uri == null) { + return null; + } + if (uri.startsWith(ParserConstants.FILE_PROTOCOL_SIMPLE) && !uri.startsWith(ParserConstants.FILE_PROTOCOL_TRIPLE_SLASH)) { + if (!uri.startsWith(ParserConstants.FILE_PROTOCOL)) { + uri = uri.replace(ParserConstants.FILE_PROTOCOL_SIMPLE, ParserConstants.FILE_PROTOCOL_TRIPLE_SLASH); + } + } + return uri; + } + + /** + * Validates a URI scheme according to RFC 3986. + * A valid scheme must start with a letter and contain only letters, digits, '+', '-', or '.'. + * + * @param scheme scheme to validate + * @return true if the scheme is valid, false otherwise + */ + public static boolean isValidScheme(String scheme) { + if (scheme == null || scheme.isEmpty() || !Character.isLetter(scheme.charAt(0))) { + return false; + } + for (int i = 1; i < scheme.length(); i++) { + char c = scheme.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '+' && c != '-' && c != '.') { + return false; + } + } + return true; + } + } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java index 21a606c0a..b96e49364 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/common/AbstractTurtleTriGListener.java @@ -12,6 +12,9 @@ import java.net.URI; import java.net.URISyntaxException; +import static fr.inria.corese.core.next.data.impl.common.util.IRIUtils.isAbsoluteIRI; +import static fr.inria.corese.core.next.data.impl.common.util.IRIUtils.normalizeURI; + /** * Base class for RDF parsers (Turtle, TriG) providing common functionality. * Implements IRI resolution according to RFC 3986, Unicode escape handling, @@ -159,292 +162,7 @@ public String resolveIRIAgainstBase(String iri) { if (isAbsoluteIRI(iri)) { return iri; } - - if (iri.isEmpty()) { - return effectiveBase; - } - - try { - URI baseUri = new URI(effectiveBase); - String baseScheme = baseUri.getScheme(); - String baseAuthority = baseUri.getAuthority(); - String basePath = baseUri.getPath(); - String baseQuery = baseUri.getQuery(); - - String[] refParts = parseReference(iri); - String refScheme = refParts[0]; - String refAuthority = refParts[1]; - String refPath = refParts[2]; - String refQuery = refParts[3]; - String refFragment = refParts[4]; - - String targetScheme, targetAuthority, targetPath, targetQuery, targetFragment; - - // RFC 3986 Section 5.2.2 - Reference Resolution Algorithm - if (refScheme != null) { - targetScheme = refScheme; - targetAuthority = refAuthority; - targetPath = removeDotSegments(refPath); - targetQuery = refQuery; - } else { - if (refAuthority != null) { - targetScheme = baseScheme; - targetAuthority = refAuthority; - targetPath = removeDotSegments(refPath); - targetQuery = refQuery; - } else { - targetScheme = baseScheme; - targetAuthority = baseAuthority; - if (refPath.isEmpty()) { - targetPath = basePath; - targetQuery = refQuery != null ? refQuery : baseQuery; - } else { - if (refPath.startsWith(ParserConstants.SLASH)) { - targetPath = removeDotSegments(refPath); - } else { - targetPath = removeDotSegments(mergePaths(basePath, refPath)); - } - targetQuery = refQuery; - } - } - } - targetFragment = refFragment; - - return buildURI(targetScheme, targetAuthority, targetPath, targetQuery, targetFragment); - - } catch (URISyntaxException e) { - return performSimpleFallback(effectiveBase, iri); - } - } - - /** - * Constructs a URI from its components. - * - * @param scheme URI scheme (e.g., "http", "file") - * @param authority authority component (host, port, userinfo) - * @param path path component - * @param query query component - * @param fragment fragment identifier - * @return normalized URI string - */ - public String buildURI(String scheme, String authority, String path, String query, String fragment) { - StringBuilder result = new StringBuilder(); - if (scheme != null) { - result.append(scheme).append(ParserConstants.COLON); - } - if (authority != null) { - result.append(ParserConstants.DOUBLE_SLASH).append(authority); - } - if (path != null) { - result.append(path); - } - if (query != null) { - result.append(ParserConstants.QUERY_MARK).append(query); - } - if (fragment != null) { - result.append(ParserConstants.HASH).append(fragment); - } - return normalizeURI(result.toString()); - } - - /** - * Parses a URI reference into its five components. - * - * @param ref URI reference to parse - * @return array containing [scheme, authority, path, query, fragment] - */ - public String[] parseReference(String ref) { - String[] parts = new String[5]; - String remaining = ref; - - int fragmentIndex = remaining.indexOf('#'); - if (fragmentIndex >= 0) { - parts[4] = remaining.substring(fragmentIndex + 1); - remaining = remaining.substring(0, fragmentIndex); - } - - int queryIndex = remaining.indexOf('?'); - if (queryIndex >= 0) { - parts[3] = remaining.substring(queryIndex + 1); - remaining = remaining.substring(0, queryIndex); - } - - int colonIndex = remaining.indexOf(':'); - if (colonIndex > 0 && isValidScheme(remaining.substring(0, colonIndex))) { - parts[0] = remaining.substring(0, colonIndex); - remaining = remaining.substring(colonIndex + 1); - } - - if (remaining.startsWith(ParserConstants.DOUBLE_SLASH)) { - int authorityEnd = remaining.indexOf('/', 2); - if (authorityEnd < 0) { - authorityEnd = remaining.length(); - } - parts[1] = remaining.substring(2, authorityEnd); - remaining = remaining.substring(authorityEnd); - } - - parts[2] = remaining; - return parts; - } - - /** - * Merges a base path with a relative path. - * - * @param basePath base path from base URI - * @param refPath relative path from reference - * @return merged path - */ - public String mergePaths(String basePath, String refPath) { - if (basePath == null || basePath.isEmpty()) { - return ParserConstants.SLASH + refPath; - } - int lastSlash = basePath.lastIndexOf('/'); - return lastSlash >= 0 ? basePath.substring(0, lastSlash + 1) + refPath : refPath; - } - - /** - * Removes dot segments from a path (RFC 3986 Section 5.2.4). - * Processes ".." and "." segments according to the normalization algorithm. - * - * @param path path to normalize - * @return normalized path without dot segments - */ - public String removeDotSegments(String path) { - if (path == null || path.isEmpty()) { - return ParserConstants.EMPTY_STRING; - } - - String input = path; - StringBuilder output = new StringBuilder(); - - while (!input.isEmpty()) { - if (input.startsWith(ParserConstants.DOUBLE_DOT + ParserConstants.SLASH)) { - input = input.substring(3); - } else if (input.startsWith(ParserConstants.DOT + ParserConstants.SLASH)) { - input = input.substring(2); - } else if (input.startsWith(ParserConstants.SLASH + ParserConstants.DOT + ParserConstants.SLASH)) { - input = ParserConstants.SLASH + input.substring(3); - } else if (input.equals(ParserConstants.SLASH + ParserConstants.DOT)) { - input = ParserConstants.SLASH; - } else if (input.startsWith(ParserConstants.SLASH + ParserConstants.DOUBLE_DOT + ParserConstants.SLASH)) { - input = ParserConstants.SLASH + input.substring(4); - removeLastSegment(output); - } else if (input.equals(ParserConstants.SLASH + ParserConstants.DOUBLE_DOT)) { - input = ParserConstants.SLASH; - removeLastSegment(output); - } else if (input.equals(ParserConstants.POINT) || input.equals(ParserConstants.DOUBLE_DOT)) { - input = ParserConstants.EMPTY_STRING; - } else { - int nextSlash; - if (input.startsWith(ParserConstants.SLASH)) { - nextSlash = input.indexOf(ParserConstants.SLASH, 1); - if (nextSlash >= 0) { - output.append(input, 0, nextSlash); - input = input.substring(nextSlash); - } else { - output.append(input); - input = ParserConstants.EMPTY_STRING; - } - } else { - nextSlash = input.indexOf(ParserConstants.SLASH); - if (nextSlash >= 0) { - output.append(input, 0, nextSlash); - input = input.substring(nextSlash); - } else { - output.append(input); - input = ParserConstants.EMPTY_STRING; - } - } - } - } - - return output.toString(); - } - - /** - * Removes the last path segment from the output buffer. - * Used during dot segment removal when processing ".." segments. - * - * @param output string builder containing the path being constructed - */ - public void removeLastSegment(StringBuilder output) { - String outputStr = output.toString(); - int lastSlash = outputStr.lastIndexOf(ParserConstants.SLASH); - output.setLength(Math.max(lastSlash, 0)); - } - - /** - * Provides a fallback resolution mechanism when RFC 3986 parsing fails. - * - * @param base base URI - * @param relative relative IRI reference - * @return resolved IRI using simple concatenation rules - */ - public String performSimpleFallback(String base, String relative) { - if (relative.isEmpty()) { - return base; - } - if (base.endsWith(ParserConstants.SLASH)) { - return base + relative; - } - int lastSlash = base.lastIndexOf('/'); - return lastSlash >= 0 ? base.substring(0, lastSlash + 1) + relative : base + ParserConstants.SLASH + relative; - } - - /** - * Normalizes URI strings, ensuring proper format for file:// URIs. - * - * @param uri URI to normalize - * @return normalized URI string - */ - public String normalizeURI(String uri) { - if (uri == null) { - return null; - } - if (uri.startsWith(ParserConstants.FILE_PROTOCOL_SIMPLE) && !uri.startsWith(ParserConstants.FILE_PROTOCOL_TRIPLE_SLASH)) { - if (!uri.startsWith(ParserConstants.FILE_PROTOCOL)) { - uri = uri.replace(ParserConstants.FILE_PROTOCOL_SIMPLE, ParserConstants.FILE_PROTOCOL_TRIPLE_SLASH); - } - } - return uri; - } - - /** - * Determines whether an IRI is absolute (contains a valid scheme). - * - * @param iri IRI to check - * @return true if the IRI is absolute, false otherwise - */ - public boolean isAbsoluteIRI(String iri) { - if (iri == null || iri.isEmpty()) { - return false; - } - int colonIndex = iri.indexOf(':'); - if (colonIndex == -1 || colonIndex == 0) { - return false; - } - return isValidScheme(iri.substring(0, colonIndex)); - } - - /** - * Validates a URI scheme according to RFC 3986. - * A valid scheme must start with a letter and contain only letters, digits, '+', '-', or '.'. - * - * @param scheme scheme to validate - * @return true if the scheme is valid, false otherwise - */ - public boolean isValidScheme(String scheme) { - if (scheme == null || scheme.isEmpty() || !Character.isLetter(scheme.charAt(0))) { - return false; - } - for (int i = 1; i < scheme.length(); i++) { - char c = scheme.charAt(i); - if (!Character.isLetterOrDigit(c) && c != '+' && c != '-' && c != '.') { - return false; - } - } - return true; + return IRIUtils.resolveIRIAgainstBase(effectiveBase, iri); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 2ad0cf823..8190e2933 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -1,5 +1,6 @@ package fr.inria.corese.core.next.query.impl.sparql.ast; +import fr.inria.corese.core.next.data.impl.common.util.IRIUtils; import fr.inria.corese.core.next.data.impl.io.common.IOConstants; import java.util.List; @@ -22,6 +23,14 @@ public record QueryPrologueAst(List prefixDeclarations, Ir } else { baseIri = new IriAst(trimChevronIRIs(baseIri.raw())); } + // resolving relative namespaces in prefix declarations + IriAst finalBaseIri = baseIri; + prefixDeclarations = prefixDeclarations.stream().map(prefixDecl -> { + if(IRIUtils.isAbsoluteIRI(prefixDecl.namespace().raw())) { + return prefixDecl; + } + return new PrefixDeclarationAst(prefixDecl.prefix(), new IriAst(IRIUtils.resolveIRIAgainstBase(finalBaseIri.raw(), prefixDecl.namespace().raw()))); + }).toList(); } public static QueryPrologueAst empty() { From 8b77932d526e6888e0357fe22a70dc9bd8f3fc2c Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Mon, 30 Mar 2026 13:48:49 +0200 Subject: [PATCH 10/12] fixes --- .../corese/core/next/query/impl/parser/SparqlAstBuilder.java | 3 +++ .../next/query/impl/parser/listener/PrologueFeature.java | 3 ++- .../next/query/impl/parser/SparqlParserPrologueTest.java | 5 +---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index be46afddc..4f39577b5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -134,6 +134,9 @@ public SparqlAstBuilder(SparqlParserOptions options) { // --- Construction entry points (called by listener) --- public void setBaseUri(String uri) { + if(this.baseUri != null && !this.baseUri.equals(options.getBaseIRI())) { + throw new QuerySyntaxException("Base URI already set, multiple BASE declarations are forbidden."); + } this.baseUri = uri; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java index 39cad1fc5..e35f73c20 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/listener/PrologueFeature.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.impl.parser.antlr.SparqlParser; import fr.inria.corese.core.next.query.impl.parser.SparqlAstBuilder; +import fr.inria.corese.core.next.util.StringUtils; public class PrologueFeature extends AbstractSparqlFeature { public PrologueFeature(SparqlAstBuilder builder) { @@ -10,7 +11,7 @@ public PrologueFeature(SparqlAstBuilder builder) { @Override public void exitBaseDecl(SparqlParser.BaseDeclContext ctx) { - builder().setBaseUri(ctx.IRI_REF().getText()); + builder().setBaseUri(StringUtils.trimChevronIRIs(ctx.IRI_REF().getText())); } @Override diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index 3ac9cd5ab..9c7df069d 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -12,8 +12,6 @@ public class SparqlParserPrologueTest extends AbstractSparqlParserFeatureTest { - private static final Logger logger = LoggerFactory.getLogger(SparqlParserPrologueTest.class); - @Test @DisplayName("Basic Ask with base") public void askWithBase() { @@ -96,8 +94,6 @@ public void selectWithBaseAndOnePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); - logger.debug("{}", ast.prologue()); - assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test"))); assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest/#"))); @@ -197,6 +193,7 @@ public void relativePrefixShouldBeResolvedAgainstEffectiveBase() { SparqlParser parser = newParserDefault(); SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), "ex: is in the prologue"); assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") && prefixDecl.namespace().raw().equals("http://example.org/root/ns/")), "the IRI of ex: in http://example.org/root/ns/"); } From 88681398cff70bc7c6f3f4f80380fa463faa0312 Mon Sep 17 00:00:00 2001 From: Pierre Maillot Date: Mon, 30 Mar 2026 14:33:02 +0200 Subject: [PATCH 11/12] rebase fix --- .../query/impl/parser/SparqlAstBuilder.java | 25 ++++++++++--------- .../next/query/impl/parser/SparqlParser.java | 10 +------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java index 4f39577b5..90475b32d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlAstBuilder.java @@ -361,10 +361,10 @@ public QueryAst getResult() { DatasetClauseAst datasetClauseAst = new DatasetClauseAst(datasetDefaultGraphs, datasetNamedGraphs); QueryPrologueAst prologueAst = new QueryPrologueAst(List.copyOf(prefixDeclarations), new IriAst(baseUri)); return switch (this.queryType) { - case ASK -> buildAskQueryAst(datasetClauseAst); - case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst); - case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst); - case SELECT -> buildSelectQueryAst(datasetClauseAst); + case ASK -> buildAskQueryAst(datasetClauseAst, prologueAst); + case CONSTRUCT -> buildConstructQueryAst(datasetClauseAst, prologueAst); + case DESCRIBE -> buildDescribeQueryAst(datasetClauseAst, prologueAst); + case SELECT -> buildSelectQueryAst(datasetClauseAst, prologueAst); case UNDEFINED -> throw new QueryEvaluationException("Could not determine the type of query during parsing"); }; } @@ -398,36 +398,37 @@ private void ensureNoOpenBgp() { /** * Builds the AST for ASK queries. */ - private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst) { - return new AskQueryAst(datasetClauseAst, whereClause); + private AskQueryAst buildAskQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { + return new AskQueryAst(datasetClauseAst, whereClause, prologue); } /** * Builds the AST for SELECT queries. */ - private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst) { + private SelectQueryAst buildSelectQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { validateSelectQueryScope(); - return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier()); + return new SelectQueryAst(projection, datasetClauseAst, whereClause, buildSolutionModifier(), prologue); } /** * Builds the AST for DESCRIBE queries. */ - private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst) { + private DescribeQueryAst buildDescribeQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { // TODO #306: validate variable scope for DESCRIBE modifiers when DescribeQueryAst carries them. - return new DescribeQueryAst(datasetClauseAst, describeResources, whereClause); + return new DescribeQueryAst(datasetClauseAst, describeResources, whereClause, prologue); } /** * Builds the AST for CONSTRUCT queries. */ - private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst) { + private ConstructQueryAst buildConstructQueryAst(DatasetClauseAst datasetClauseAst, QueryPrologueAst prologue) { // TODO #306: validate variable scope for CONSTRUCT modifiers when ConstructQueryAst carries them. return new ConstructQueryAst( constructTemplate != null ? constructTemplate : new ConstructTemplateAst(List.of()), datasetClauseAst, whereClause, - buildSolutionModifier()); + buildSolutionModifier(), + prologue); } /** diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java index 8d91e97f0..097fd9b79 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/SparqlParser.java @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import fr.inria.corese.core.next.query.impl.parser.listener.*; import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; @@ -26,15 +27,6 @@ import fr.inria.corese.core.next.query.api.exception.QueryValidationException; import fr.inria.corese.core.next.query.api.io.parser.QueryOptions; import fr.inria.corese.core.next.query.api.sparql.options.BaseIRIOptions; -import fr.inria.corese.core.next.query.impl.parser.listener.AskQueryFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.BgpFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.ConstructQueryFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.DatasetClauseFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.DescribeQueryFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.FilterFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.SelectQueryFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.SolutionModifierFeature; -import fr.inria.corese.core.next.query.impl.parser.listener.UnionFeature; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; public class SparqlParser extends AbstractQueryParser { From 23f98eb6ba80d62ddd81abff13d414f03fb924db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Mon, 30 Mar 2026 17:33:09 +0200 Subject: [PATCH 12/12] Reject relative BASE IRIs in query prologue --- .../impl/sparql/ast/QueryPrologueAst.java | 4 + .../impl/parser/SparqlParserPrologueTest.java | 129 ++++++++++++------ 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 8190e2933..c6700b411 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -2,6 +2,7 @@ import fr.inria.corese.core.next.data.impl.common.util.IRIUtils; import fr.inria.corese.core.next.data.impl.io.common.IOConstants; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import java.util.List; @@ -23,6 +24,9 @@ public record QueryPrologueAst(List prefixDeclarations, Ir } else { baseIri = new IriAst(trimChevronIRIs(baseIri.raw())); } + if (!IRIUtils.isAbsoluteIRI(baseIri.raw())) { + throw new QuerySyntaxException("Base IRI should be absolute, got " + baseIri.raw()); + } // resolving relative namespaces in prefix declarations IriAst finalBaseIri = baseIri; prefixDeclarations = prefixDeclarations.stream().map(prefixDecl -> { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java index 9c7df069d..97f59b355 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserPrologueTest.java @@ -1,20 +1,23 @@ package fr.inria.corese.core.next.query.impl.parser; -import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; -import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import static org.junit.jupiter.api.Assertions.*; +import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; -public class SparqlParserPrologueTest extends AbstractSparqlParserFeatureTest { +@SuppressWarnings("java:S5976") +class SparqlParserPrologueTest extends AbstractSparqlParserFeatureTest { @Test @DisplayName("Basic Ask with base") - public void askWithBase() { + void askWithBase() { String query = """ BASE ASK { @@ -30,7 +33,7 @@ public void askWithBase() { @Test @DisplayName("Basic Construct with base") - public void constructWithBase() { + void constructWithBase() { String query = """ BASE CONSTRUCT { @@ -49,7 +52,7 @@ public void constructWithBase() { @Test @DisplayName("Basic Select with base") - public void describeWithBase() { + void describeWithBase() { String query = """ BASE DESCRIBE ?s { @@ -65,7 +68,7 @@ public void describeWithBase() { @Test @DisplayName("Basic Select with base") - public void selectWithBase() { + void selectWithBase() { String query = """ BASE SELECT * { @@ -81,7 +84,7 @@ public void selectWithBase() { @Test @DisplayName("Basic Select with base and one prefix") - public void selectWithBaseAndOnePrefix() { + void selectWithBaseAndOnePrefix() { String query = """ BASE PREFIX test: @@ -95,13 +98,16 @@ public void selectWithBaseAndOnePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("test"))); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test") + && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest/#"))); } @Test @DisplayName("Basic Select with base and multiple prefix") - public void selectWithBaseAndMultiplePrefix() { + void selectWithBaseAndMultiplePrefix() { String query = """ BASE PREFIX test1: @@ -116,17 +122,26 @@ public void selectWithBaseAndMultiplePrefix() { SelectQueryAst ast = (SelectQueryAst) parser.parse(query); assertEquals("http://ns.inria.fr/test/", ast.prologue().baseIri().raw()); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest1/#"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest2/#"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3"))); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3") && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest3/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1"))); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test1") + && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest1/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2"))); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test2") + && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest2/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3"))); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("test3") + && prefixDecl.namespace().raw().equals("https://ns.inria.fr/otherTest3/#"))); } @Test @DisplayName("Basic Select with base and multiple prefix with overlap") - public void selectWithBaseAndMultiplePrefixWithOverlap() { + void selectWithBaseAndMultiplePrefixWithOverlap() { String query = """ BASE PREFIX test1: @@ -146,7 +161,7 @@ public void selectWithBaseAndMultiplePrefixWithOverlap() { @Test @DisplayName("Basic Select with multiple base should throw") - public void selectWithMultipleBase() { + void selectWithMultipleBase() { String query = """ BASE BASE @@ -164,7 +179,7 @@ public void selectWithMultipleBase() { @Test @DisplayName("PREFIX with empty prefix label should be accepted") - public void selectWithDefaultPrefixDeclaration() { + void selectWithDefaultPrefixDeclaration() { String query = """ PREFIX : SELECT * { @@ -176,43 +191,71 @@ public void selectWithDefaultPrefixDeclaration() { SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().isEmpty())); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().isEmpty() && prefixDecl.namespace().raw().equals("https://ns.inria.fr/default/#"))); + assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().isEmpty() + && prefixDecl.namespace().raw().equals("https://ns.inria.fr/default/#"))); + } + + @Test + @DisplayName("Relative BASE should be rejected because BASE must be absolute") + void relativeBaseShouldBeRejected() { + String query = """ + BASE + SELECT * { + ?s ?p ?o . + } + """; + + SparqlParser parser = newParserDefault(); + + assertThrows(QuerySyntaxException.class, () -> parser.parse(query)); } @Test @DisplayName("Relative PREFIX IRI should be resolved against effective base") - public void relativePrefixShouldBeResolvedAgainstEffectiveBase() { + void relativePrefixShouldBeResolvedAgainstEffectiveBase() { String query = """ - BASE - PREFIX ex: - SELECT * { - ?s ex:p ?o . - } - """; + BASE + PREFIX ex: + SELECT * { + ?s ex:p ?o . + } + """; SparqlParser parser = newParserDefault(); SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), "ex: is in the prologue"); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") && prefixDecl.namespace().raw().equals("http://example.org/root/ns/")), "the IRI of ex: in http://example.org/root/ns/"); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), + "ex: is in the prologue"); + assertTrue( + ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") + && prefixDecl.namespace().raw().equals("http://example.org/root/ns/")), + "the IRI of ex: in http://example.org/root/ns/"); } @Test @DisplayName("Relative PREFIX IRI should use RFC3986 resolution, not string concatenation") - public void relativePrefixShouldUseRfc3986Resolution() { + void relativePrefixShouldUseRfc3986Resolution() { String query = """ - BASE - PREFIX ex: - SELECT * { - ?s ex:p ?o . - } - """; + BASE + PREFIX ex: + SELECT * { + ?s ex:p ?o . + } + """; SparqlParser parser = newParserDefault(); SelectQueryAst ast = assertDoesNotThrow(() -> (SelectQueryAst) parser.parse(query)); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), "ex: is in the prologue"); - assertTrue(ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") && prefixDecl.namespace().raw().equals("http://example.org/ns/")), "the IRI of ex: in http://example.org/ns/"); + assertTrue( + ast.prologue().prefixDeclarations().stream().anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex")), + "ex: is in the prologue"); + assertTrue( + ast.prologue().prefixDeclarations().stream() + .anyMatch(prefixDecl -> prefixDecl.prefix().equals("ex") + && prefixDecl.namespace().raw().equals("http://example.org/ns/")), + "the IRI of ex: in http://example.org/ns/"); } }