diff --git a/.gitignore b/.gitignore index 2b3ab2c0b5d..1f063f79eff 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ target/ **/.cache pom.xml.versionsBackup dependency-reduced-pom.xml +.m2_repo/ # GEdit generated stuff *.*~ diff --git a/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/PrefixDeclProcessor.java b/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/PrefixDeclProcessor.java index 15612f35d5e..fa6352367be 100644 --- a/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/PrefixDeclProcessor.java +++ b/core/queryparser/sparql/src/main/java/org/eclipse/rdf4j/query/parser/sparql/PrefixDeclProcessor.java @@ -69,10 +69,15 @@ public static Map process(ASTOperationContainer qc, Map\n" + + "SELECT ?name WHERE { ?person foaf:name ?name }"; + + // This should not throw an exception + assertDoesNotThrow(() -> parser.parseQuery(query, null)); + + ParsedQuery parsed = parser.parseQuery(query, null); + assertNotNull(parsed); + } + + @Test + public void testDuplicatePrefixDeclarations_DifferentNamespace_ShouldFail() { + // Test that duplicate prefix declarations with different namespaces are rejected + String query = "PREFIX foaf: \n" + + "PREFIX foaf: \n" + + "SELECT ?name WHERE { ?person foaf:name ?name }"; + + // This should throw a MalformedQueryException + assertThatExceptionOfType(MalformedQueryException.class) + .isThrownBy(() -> parser.parseQuery(query, null)) + .withMessageContaining("Multiple prefix declarations") + .withMessageContaining("foaf"); + } + + @Test + public void testDuplicatePrefixDeclarations_EmptyPrefix_SameNamespace_ShouldPass() { + // Test that duplicate default prefix declarations with the same namespace are allowed + String query = "PREFIX : \n" + + "PREFIX : \n" + + "SELECT ?name WHERE { :person :name ?name }"; + + // This should not throw an exception + assertDoesNotThrow(() -> parser.parseQuery(query, null)); + + ParsedQuery parsed = parser.parseQuery(query, null); + assertNotNull(parsed); + } + + @Test + public void testDuplicatePrefixDeclarations_EmptyPrefix_DifferentNamespace_ShouldFail() { + // Test that duplicate default prefix declarations with different namespaces are rejected + String query = "PREFIX : \n" + + "PREFIX : \n" + + "SELECT ?name WHERE { :person :name ?name }"; + + // This should throw a MalformedQueryException + assertThatExceptionOfType(MalformedQueryException.class) + .isThrownBy(() -> parser.parseQuery(query, null)) + .withMessageContaining("Multiple prefix declarations"); + } } diff --git a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtlePrefixDuplicateTest.java b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtlePrefixDuplicateTest.java new file mode 100644 index 00000000000..de87f336022 --- /dev/null +++ b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtlePrefixDuplicateTest.java @@ -0,0 +1,288 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.rio.turtle; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.rio.RDFHandler; +import org.eclipse.rdf4j.rio.RDFParseException; +import org.eclipse.rdf4j.rio.RDFParser; +import org.eclipse.rdf4j.rio.helpers.StatementCollector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for duplicate prefix declarations in Turtle parser. Tests include scenarios with @base to show how different + * placement of the base changes duplicate relative prefix declarations. + */ +public class TurtlePrefixDuplicateTest { + + private RDFParser parser; + private List statements; + private RDFHandler handler; + + @BeforeEach + public void setUp() { + parser = new TurtleParser(); + statements = new ArrayList<>(); + handler = new StatementCollector(statements); + parser.setRDFHandler(handler); + } + + @Test + public void testDuplicatePrefixDeclarations_SameNamespace_ShouldPass() throws Exception { + String turtle = "@prefix foaf: .\n" + + "@prefix foaf: .\n" + + "\n" + + " foaf:name \"John Doe\" .\n"; + + // Should not throw an exception when same prefix maps to same namespace + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Should produce the expected statement + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://xmlns.com/foaf/0.1/name"); + } + + @Test + public void testDuplicatePrefixDeclarations_DifferentNamespace_LastOneWins() throws Exception { + String turtle = "@prefix foaf: .\n" + + "@prefix foaf: .\n" + + "\n" + + " foaf:name \"John Doe\" .\n"; + + // Should not throw an exception - Turtle parsers typically allow redefinition + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // The last prefix declaration should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/different/name"); + } + + @Test + public void testDuplicateDefaultPrefixDeclarations_SameNamespace_ShouldPass() throws Exception { + String turtle = "@prefix : .\n" + + "@prefix : .\n" + + "\n" + + " :name \"John Doe\" .\n"; + + // Should not throw an exception when same default prefix maps to same namespace + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Should produce the expected statement + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/ns#name"); + } + + // Tests with @base showing different scenarios with relative prefix declarations + + @Test + public void testDuplicateRelativePrefix_SameBase_ShouldPass() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Alice\" .\n"; + + // Should not throw an exception when same relative prefix resolves to same absolute namespace + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Should produce the expected statement with resolved absolute namespace + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_BaseChangedBetween_LastWins() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@base .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Bob\" .\n"; + + // Should not throw exception - turtle allows redefinition, last one wins + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // The last prefix declaration should win (resolved with the second base) + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://different.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_ExternalBaseChange_LastWins() throws Exception { + String turtle = "@prefix rel: .\n" + + "@base .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Charlie\" .\n"; + + // Should not throw exception - turtle allows redefinition + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // First prefix uses external base, second uses internal base + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://different.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_MultipleBaseChanges_LastWins() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@base .\n" + + "@base .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"David\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Last prefix with last base should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://third.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_BaseAfterAllPrefixes_EarlierBasesUsed() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@base .\n" + + "@prefix rel: .\n" + + "@base .\n" + + "\n" + + " rel:name \"Eve\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Second prefix with second base should be used (base after prefixes doesn't affect them) + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://second.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_SameExternalBase_ShouldPass() throws Exception { + String turtle = "@prefix rel: .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Frank\" .\n"; + + // Should not throw exception when both relative prefixes resolve to same namespace using external base + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Both prefixes resolve to same namespace using external base + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_SameInternalBase_ShouldPass() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Grace\" .\n"; + + // Should not throw exception when both relative prefixes resolve to same namespace + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Both prefixes resolve to same namespace using internal base + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/ns/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_AbsoluteToRelative_LastWins() throws Exception { + String turtle = "@prefix rel: .\n" + + "@base .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Henry\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Second (relative) prefix should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://relative.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_RelativeToAbsolute_LastWins() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Ivy\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Second (absolute) prefix should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://absolute.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_ComplexRelativePaths_LastWins() throws Exception { + String turtle = "@base .\n" + + "@prefix rel: <../vocab/> .\n" + + "@base .\n" + + "@prefix rel: <../../vocab/> .\n" + + "\n" + + " rel:name \"Jack\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Last prefix with complex relative path should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://example.org/vocab/name"); + } + + @Test + public void testDuplicateRelativePrefix_NoBaseForRelative_ShouldFail() throws Exception { + String turtle = "@prefix rel: .\n" + + "@prefix rel: .\n" + + "\n" + + " rel:name \"Kate\" .\n"; + + // Should throw exception when relative prefix cannot be resolved (no base provided) + assertThrows(RDFParseException.class, () -> { + parser.parse(new StringReader(turtle), null); + }); + } + + @Test + public void testDuplicateDefaultRelativePrefix_BaseChanges_LastWins() throws Exception { + String turtle = "@base .\n" + + "@prefix : .\n" + + "@base .\n" + + "@prefix : .\n" + + "\n" + + " :name \"Luna\" .\n"; + + // Should not throw exception + assertDoesNotThrow(() -> parser.parse(new StringReader(turtle), "http://example.org/")); + + // Last default prefix declaration should win + assertThat(statements).hasSize(1); + assertThat(statements.get(0).getPredicate().toString()).isEqualTo("http://second.org/vocab/name"); + } +} \ No newline at end of file