1212
1313import static org .assertj .core .api .Assertions .assertThat ;
1414import static org .junit .jupiter .api .Assertions .assertDoesNotThrow ;
15+ import static org .junit .jupiter .api .Assertions .assertThrows ;
1516
1617import java .io .StringReader ;
1718import java .util .ArrayList ;
1819import java .util .List ;
1920
2021import org .eclipse .rdf4j .model .Statement ;
2122import org .eclipse .rdf4j .rio .RDFHandler ;
23+ import org .eclipse .rdf4j .rio .RDFParseException ;
2224import org .eclipse .rdf4j .rio .RDFParser ;
2325import org .eclipse .rdf4j .rio .helpers .StatementCollector ;
2426import org .junit .jupiter .api .BeforeEach ;
2527import org .junit .jupiter .api .Test ;
2628
2729/**
28- * Tests for duplicate prefix declarations in Turtle parser.
30+ * Tests for duplicate prefix declarations in Turtle parser. Tests include scenarios with @base to show how different
31+ * placement of the base changes duplicate relative prefix declarations.
2932 */
3033public class TurtlePrefixDuplicateTest {
3134
@@ -85,4 +88,201 @@ public void testDuplicateDefaultPrefixDeclarations_SameNamespace_ShouldPass() th
8588 assertThat (statements ).hasSize (1 );
8689 assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://example.org/ns#name" );
8790 }
91+
92+ // Tests with @base showing different scenarios with relative prefix declarations
93+
94+ @ Test
95+ public void testDuplicateRelativePrefix_SameBase_ShouldPass () throws Exception {
96+ String turtle = "@base <http://example.org/> .\n " +
97+ "@prefix rel: <vocab/> .\n " +
98+ "@prefix rel: <vocab/> .\n " +
99+ "\n " +
100+ "<http://example.org/person> rel:name \" Alice\" .\n " ;
101+
102+ // Should not throw an exception when same relative prefix resolves to same absolute namespace
103+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
104+
105+ // Should produce the expected statement with resolved absolute namespace
106+ assertThat (statements ).hasSize (1 );
107+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://example.org/vocab/name" );
108+ }
109+
110+ @ Test
111+ public void testDuplicateRelativePrefix_BaseChangedBetween_LastWins () throws Exception {
112+ String turtle = "@base <http://example.org/> .\n " +
113+ "@prefix rel: <vocab/> .\n " +
114+ "@base <http://different.org/> .\n " +
115+ "@prefix rel: <vocab/> .\n " +
116+ "\n " +
117+ "<http://example.org/person> rel:name \" Bob\" .\n " ;
118+
119+ // Should not throw exception - turtle allows redefinition, last one wins
120+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
121+
122+ // The last prefix declaration should win (resolved with the second base)
123+ assertThat (statements ).hasSize (1 );
124+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://different.org/vocab/name" );
125+ }
126+
127+ @ Test
128+ public void testDuplicateRelativePrefix_ExternalBaseChange_LastWins () throws Exception {
129+ String turtle = "@prefix rel: <vocab/> .\n " +
130+ "@base <http://different.org/> .\n " +
131+ "@prefix rel: <vocab/> .\n " +
132+ "\n " +
133+ "<http://example.org/person> rel:name \" Charlie\" .\n " ;
134+
135+ // Should not throw exception - turtle allows redefinition
136+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
137+
138+ // First prefix uses external base, second uses internal base
139+ assertThat (statements ).hasSize (1 );
140+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://different.org/vocab/name" );
141+ }
142+
143+ @ Test
144+ public void testDuplicateRelativePrefix_MultipleBaseChanges_LastWins () throws Exception {
145+ String turtle = "@base <http://first.org/> .\n " +
146+ "@prefix rel: <vocab/> .\n " +
147+ "@base <http://second.org/> .\n " +
148+ "@base <http://third.org/> .\n " +
149+ "@prefix rel: <vocab/> .\n " +
150+ "\n " +
151+ "<http://example.org/person> rel:name \" David\" .\n " ;
152+
153+ // Should not throw exception
154+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
155+
156+ // Last prefix with last base should win
157+ assertThat (statements ).hasSize (1 );
158+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://third.org/vocab/name" );
159+ }
160+
161+ @ Test
162+ public void testDuplicateRelativePrefix_BaseAfterAllPrefixes_EarlierBasesUsed () throws Exception {
163+ String turtle = "@base <http://first.org/> .\n " +
164+ "@prefix rel: <vocab/> .\n " +
165+ "@base <http://second.org/> .\n " +
166+ "@prefix rel: <vocab/> .\n " +
167+ "@base <http://third.org/> .\n " +
168+ "\n " +
169+ "<http://example.org/person> rel:name \" Eve\" .\n " ;
170+
171+ // Should not throw exception
172+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
173+
174+ // Second prefix with second base should be used (base after prefixes doesn't affect them)
175+ assertThat (statements ).hasSize (1 );
176+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://second.org/vocab/name" );
177+ }
178+
179+ @ Test
180+ public void testDuplicateRelativePrefix_SameExternalBase_ShouldPass () throws Exception {
181+ String turtle = "@prefix rel: <vocab/> .\n " +
182+ "@prefix rel: <vocab/> .\n " +
183+ "\n " +
184+ "<http://example.org/person> rel:name \" Frank\" .\n " ;
185+
186+ // Should not throw exception when both relative prefixes resolve to same namespace using external base
187+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
188+
189+ // Both prefixes resolve to same namespace using external base
190+ assertThat (statements ).hasSize (1 );
191+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://example.org/vocab/name" );
192+ }
193+
194+ @ Test
195+ public void testDuplicateRelativePrefix_SameInternalBase_ShouldPass () throws Exception {
196+ String turtle = "@base <http://example.org/ns/> .\n " +
197+ "@prefix rel: <vocab/> .\n " +
198+ "@prefix rel: <vocab/> .\n " +
199+ "\n " +
200+ "<http://example.org/person> rel:name \" Grace\" .\n " ;
201+
202+ // Should not throw exception when both relative prefixes resolve to same namespace
203+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
204+
205+ // Both prefixes resolve to same namespace using internal base
206+ assertThat (statements ).hasSize (1 );
207+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://example.org/ns/vocab/name" );
208+ }
209+
210+ @ Test
211+ public void testDuplicateRelativePrefix_AbsoluteToRelative_LastWins () throws Exception {
212+ String turtle = "@prefix rel: <http://absolute.org/vocab/> .\n " +
213+ "@base <http://relative.org/> .\n " +
214+ "@prefix rel: <vocab/> .\n " +
215+ "\n " +
216+ "<http://example.org/person> rel:name \" Henry\" .\n " ;
217+
218+ // Should not throw exception
219+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
220+
221+ // Second (relative) prefix should win
222+ assertThat (statements ).hasSize (1 );
223+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://relative.org/vocab/name" );
224+ }
225+
226+ @ Test
227+ public void testDuplicateRelativePrefix_RelativeToAbsolute_LastWins () throws Exception {
228+ String turtle = "@base <http://relative.org/> .\n " +
229+ "@prefix rel: <vocab/> .\n " +
230+ "@prefix rel: <http://absolute.org/vocab/> .\n " +
231+ "\n " +
232+ "<http://example.org/person> rel:name \" Ivy\" .\n " ;
233+
234+ // Should not throw exception
235+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
236+
237+ // Second (absolute) prefix should win
238+ assertThat (statements ).hasSize (1 );
239+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://absolute.org/vocab/name" );
240+ }
241+
242+ @ Test
243+ public void testDuplicateRelativePrefix_ComplexRelativePaths_LastWins () throws Exception {
244+ String turtle = "@base <http://example.org/path/> .\n " +
245+ "@prefix rel: <../vocab/> .\n " +
246+ "@base <http://example.org/different/path/> .\n " +
247+ "@prefix rel: <../../vocab/> .\n " +
248+ "\n " +
249+ "<http://example.org/person> rel:name \" Jack\" .\n " ;
250+
251+ // Should not throw exception
252+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
253+
254+ // Last prefix with complex relative path should win
255+ assertThat (statements ).hasSize (1 );
256+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://example.org/vocab/name" );
257+ }
258+
259+ @ Test
260+ public void testDuplicateRelativePrefix_NoBaseForRelative_ShouldFail () throws Exception {
261+ String turtle = "@prefix rel: <vocab/> .\n " +
262+ "@prefix rel: <vocab/> .\n " +
263+ "\n " +
264+ "<http://example.org/person> rel:name \" Kate\" .\n " ;
265+
266+ // Should throw exception when relative prefix cannot be resolved (no base provided)
267+ assertThrows (RDFParseException .class , () -> {
268+ parser .parse (new StringReader (turtle ), null );
269+ });
270+ }
271+
272+ @ Test
273+ public void testDuplicateDefaultRelativePrefix_BaseChanges_LastWins () throws Exception {
274+ String turtle = "@base <http://first.org/> .\n " +
275+ "@prefix : <vocab/> .\n " +
276+ "@base <http://second.org/> .\n " +
277+ "@prefix : <vocab/> .\n " +
278+ "\n " +
279+ "<http://example.org/person> :name \" Luna\" .\n " ;
280+
281+ // Should not throw exception
282+ assertDoesNotThrow (() -> parser .parse (new StringReader (turtle ), "http://example.org/" ));
283+
284+ // Last default prefix declaration should win
285+ assertThat (statements ).hasSize (1 );
286+ assertThat (statements .get (0 ).getPredicate ().toString ()).isEqualTo ("http://second.org/vocab/name" );
287+ }
88288}
0 commit comments