Skip to content

Commit e5a6d16

Browse files
Copilothmottestad
andcommitted
Implement duplicate prefix support for SPARQL and add comprehensive tests
Co-authored-by: hmottestad <797185+hmottestad@users.noreply.github.com>
1 parent 908c075 commit e5a6d16

5 files changed

Lines changed: 272 additions & 3 deletions

File tree

core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/PrefixDeclProcessor.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,15 @@ public static Map<String, String> process(ASTOperationContainer qc, Map<String,
6969
String iri = prefixDecl.getIRI().getValue();
7070

7171
if (prefixMap.containsKey(prefix)) {
72-
throw new MalformedQueryException("Multiple prefix declarations for prefix '" + prefix + "'");
72+
String existingIri = prefixMap.get(prefix);
73+
if (!existingIri.equals(iri)) {
74+
throw new MalformedQueryException("Multiple prefix declarations for prefix '" + prefix
75+
+ "' with different namespaces: '" + existingIri + "' and '" + iri + "'");
76+
}
77+
// If the IRI is the same, allow the duplicate (no-op)
78+
} else {
79+
prefixMap.put(prefix, iri);
7380
}
74-
75-
prefixMap.put(prefix, iri);
7681
}
7782

7883
int preDefaultPrefixes = 0;

core/queryparser/sparql/src/test/java/org/eclipse/rdf4j/query/parser/sparql/SPARQLParserTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,4 +1047,59 @@ private Object bytesToObject(byte[] str) {
10471047
throw new RuntimeException(e);
10481048
}
10491049
}
1050+
1051+
@Test
1052+
public void testDuplicatePrefixDeclarations_SameNamespace_ShouldPass() {
1053+
// Test that duplicate prefix declarations with the same namespace are allowed
1054+
String query = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n" +
1055+
"PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n" +
1056+
"SELECT ?name WHERE { ?person foaf:name ?name }";
1057+
1058+
// This should not throw an exception
1059+
assertDoesNotThrow(() -> parser.parseQuery(query, null));
1060+
1061+
ParsedQuery parsed = parser.parseQuery(query, null);
1062+
assertNotNull(parsed);
1063+
}
1064+
1065+
@Test
1066+
public void testDuplicatePrefixDeclarations_DifferentNamespace_ShouldFail() {
1067+
// Test that duplicate prefix declarations with different namespaces are rejected
1068+
String query = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n" +
1069+
"PREFIX foaf: <http://example.org/different/>\n" +
1070+
"SELECT ?name WHERE { ?person foaf:name ?name }";
1071+
1072+
// This should throw a MalformedQueryException
1073+
assertThatExceptionOfType(MalformedQueryException.class)
1074+
.isThrownBy(() -> parser.parseQuery(query, null))
1075+
.withMessageContaining("Multiple prefix declarations")
1076+
.withMessageContaining("foaf");
1077+
}
1078+
1079+
@Test
1080+
public void testDuplicatePrefixDeclarations_EmptyPrefix_SameNamespace_ShouldPass() {
1081+
// Test that duplicate default prefix declarations with the same namespace are allowed
1082+
String query = "PREFIX : <http://example.org/ns#>\n" +
1083+
"PREFIX : <http://example.org/ns#>\n" +
1084+
"SELECT ?name WHERE { :person :name ?name }";
1085+
1086+
// This should not throw an exception
1087+
assertDoesNotThrow(() -> parser.parseQuery(query, null));
1088+
1089+
ParsedQuery parsed = parser.parseQuery(query, null);
1090+
assertNotNull(parsed);
1091+
}
1092+
1093+
@Test
1094+
public void testDuplicatePrefixDeclarations_EmptyPrefix_DifferentNamespace_ShouldFail() {
1095+
// Test that duplicate default prefix declarations with different namespaces are rejected
1096+
String query = "PREFIX : <http://example.org/ns#>\n" +
1097+
"PREFIX : <http://example.org/different/>\n" +
1098+
"SELECT ?name WHERE { :person :name ?name }";
1099+
1100+
// This should throw a MalformedQueryException
1101+
assertThatExceptionOfType(MalformedQueryException.class)
1102+
.isThrownBy(() -> parser.parseQuery(query, null))
1103+
.withMessageContaining("Multiple prefix declarations");
1104+
}
10501105
}

core/rio/api/src/test/java/org/eclipse/rdf4j/rio/helpers/AbstractRDFParserTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public Resource getBNode() {
5353
public Resource getBNode(String id) {
5454
return createNode(id);
5555
}
56+
57+
public void setNamespace(String prefix, String namespace) {
58+
super.setNamespace(prefix, namespace);
59+
}
60+
61+
public String getNamespace(String prefix) throws RDFParseException {
62+
return super.getNamespace(prefix);
63+
}
5664
}
5765

5866
@BeforeEach
@@ -89,4 +97,26 @@ public void testNodeIdHashing() {
8997
assertThat(parser.createNode(longNodeId).stringValue())
9098
.endsWith("2A372A91878F0980C8F53341D2D8A944");
9199
}
100+
101+
@Test
102+
public void testSetNamespace_DuplicateWithSameNamespace() {
103+
// Test that setting the same namespace twice is allowed (last one wins)
104+
parser.setNamespace("foaf", "http://xmlns.com/foaf/0.1/");
105+
parser.setNamespace("foaf", "http://xmlns.com/foaf/0.1/");
106+
107+
// Should not throw an exception - namespace should be available
108+
String namespace = parser.getNamespace("foaf");
109+
assertThat(namespace).isEqualTo("http://xmlns.com/foaf/0.1/");
110+
}
111+
112+
@Test
113+
public void testSetNamespace_DuplicateWithDifferentNamespace() {
114+
// Test that setting different namespaces for same prefix overwrites (existing behavior)
115+
parser.setNamespace("foaf", "http://xmlns.com/foaf/0.1/");
116+
parser.setNamespace("foaf", "http://example.org/different/");
117+
118+
// Should overwrite - current behavior of AbstractRDFParser
119+
String namespace = parser.getNamespace("foaf");
120+
assertThat(namespace).isEqualTo("http://example.org/different/");
121+
}
92122
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.rio.turtle;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
15+
16+
import java.io.StringReader;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
import org.eclipse.rdf4j.model.Statement;
21+
import org.eclipse.rdf4j.rio.RDFHandler;
22+
import org.eclipse.rdf4j.rio.RDFParser;
23+
import org.eclipse.rdf4j.rio.helpers.StatementCollector;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
27+
/**
28+
* Tests for duplicate prefix declarations in Turtle parser.
29+
*/
30+
public class TurtlePrefixDuplicateTest {
31+
32+
private RDFParser parser;
33+
private List<Statement> statements;
34+
private RDFHandler handler;
35+
36+
@BeforeEach
37+
public void setUp() {
38+
parser = new TurtleParser();
39+
statements = new ArrayList<>();
40+
handler = new StatementCollector(statements);
41+
parser.setRDFHandler(handler);
42+
}
43+
44+
@Test
45+
public void testDuplicatePrefixDeclarations_SameNamespace_ShouldPass() throws Exception {
46+
String turtle = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n" +
47+
"@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n" +
48+
"\n" +
49+
"<http://example.org/person> foaf:name \"John Doe\" .\n";
50+
51+
// Should not throw an exception when same prefix maps to same namespace
52+
assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/"));
53+
54+
// Should produce the expected statement
55+
assertThat(statements).hasSize(1);
56+
assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://xmlns.com/foaf/0.1/name");
57+
}
58+
59+
@Test
60+
public void testDuplicatePrefixDeclarations_DifferentNamespace_LastOneWins() throws Exception {
61+
String turtle = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n" +
62+
"@prefix foaf: <http://example.org/different/> .\n" +
63+
"\n" +
64+
"<http://example.org/person> foaf:name \"John Doe\" .\n";
65+
66+
// Should not throw an exception - Turtle parsers typically allow redefinition
67+
assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/"));
68+
69+
// The last prefix declaration should win
70+
assertThat(statements).hasSize(1);
71+
assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/different/name");
72+
}
73+
74+
@Test
75+
public void testDuplicateDefaultPrefixDeclarations_SameNamespace_ShouldPass() throws Exception {
76+
String turtle = "@prefix : <http://example.org/ns#> .\n" +
77+
"@prefix : <http://example.org/ns#> .\n" +
78+
"\n" +
79+
"<http://example.org/person> :name \"John Doe\" .\n";
80+
81+
// Should not throw an exception when same default prefix maps to same namespace
82+
assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/"));
83+
84+
// Should produce the expected statement
85+
assertThat(statements).hasSize(1);
86+
assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/ns#name");
87+
}
88+
}

initial-evidence.txt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
-------------------------------------------------------------------------------
2+
Test set: org.eclipse.rdf4j.query.parser.sparql.SPARQLParserTest
3+
-------------------------------------------------------------------------------
4+
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.101 s <<< FAILURE! -- in org.eclipse.rdf4j.query.parser.sparql.SPARQLParserTest
5+
org.eclipse.rdf4j.query.parser.sparql.SPARQLParserTest.testDuplicatePrefixDeclarations_SameNamespace_ShouldPass -- Time elapsed: 0.070 s <<< FAILURE!
6+
org.opentest4j.AssertionFailedError: Unexpected exception thrown: org.eclipse.rdf4j.query.MalformedQueryException: Multiple prefix declarations for prefix 'foaf'
7+
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
8+
at org.junit.jupiter.api.AssertDoesNotThrow.createAssertionFailedError(AssertDoesNotThrow.java:84)
9+
at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:75)
10+
at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:58)
11+
at org.junit.jupiter.api.Assertions.assertDoesNotThrow(Assertions.java:3196)
12+
at org.eclipse.rdf4j.query.parser.sparql.SPARQLParserTest.testDuplicatePrefixDeclarations_SameNamespace_ShouldPass(SPARQLParserTest.java:1059)
13+
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
14+
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
15+
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
16+
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
17+
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
18+
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
19+
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
20+
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
21+
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
22+
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
23+
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
24+
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
25+
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
26+
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
27+
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
28+
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
29+
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
30+
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
31+
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
32+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
33+
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
34+
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
35+
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
36+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
37+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
38+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
39+
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
40+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
41+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
42+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
43+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
44+
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
45+
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
46+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
47+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
48+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
49+
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
50+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
51+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
52+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
53+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
54+
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
55+
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
56+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
57+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
58+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
59+
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
60+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
61+
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
62+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
63+
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
64+
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
65+
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
66+
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
67+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
68+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
69+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
70+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
71+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
72+
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
73+
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
74+
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
75+
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
76+
at org.apache.maven.surefire.junitplatform.LauncherAdapter.executeWithoutCancellationToken(LauncherAdapter.java:60)
77+
at org.apache.maven.surefire.junitplatform.LauncherAdapter.execute(LauncherAdapter.java:52)
78+
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:203)
79+
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:168)
80+
at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:136)
81+
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)
82+
at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
83+
at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)
84+
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)
85+
Caused by: org.eclipse.rdf4j.query.MalformedQueryException: Multiple prefix declarations for prefix 'foaf'
86+
at org.eclipse.rdf4j.query.parser.sparql.PrefixDeclProcessor.process(PrefixDeclProcessor.java:72)
87+
at org.eclipse.rdf4j.query.parser.sparql.SPARQLParser.parseQuery(SPARQLParser.java:198)
88+
at org.eclipse.rdf4j.query.parser.sparql.SPARQLParserTest.lambda$testDuplicatePrefixDeclarations_SameNamespace_ShouldPass$5(SPARQLParserTest.java:1059)
89+
at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:71)
90+
... 75 more
91+

0 commit comments

Comments
 (0)