From 954d5b256e3b7eaa52bf255ca7c5677e7566e0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Mon, 15 Sep 2025 10:54:10 +0200 Subject: [PATCH 1/4] GH-5123 Ensure NativeStore remains usable after RDF*-star rejection; guard txnLock release --- .../rdf4j/sail/nativerdf/NativeSailStore.java | 17 ++- .../sail/nativerdf/NativeStoreConnection.java | 14 ++- .../NativeStoreRDFStarRejectionTest.java | 108 ++++++++++++++++++ 3 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreRDFStarRejectionTest.java diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java index c074d074126..4331fad3acc 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java @@ -445,7 +445,12 @@ public void clear(Resource... contexts) throws SailException { @Override public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException { - addStatement(subj, pred, obj, explicit, ctx); + try { + addStatement(subj, pred, obj, explicit, ctx); + } catch (IllegalArgumentException e) { + // Ensure upstream handles this as a SailException so branch flush clears pending changes + throw new SailException(e); + } } @Override @@ -477,9 +482,10 @@ public void approveAll(Set approved, Set approvedContexts) } } catch (IOException e) { throw new SailException(e); - } catch (RuntimeException e) { + } catch (IllegalArgumentException e) { + // Wrap invalid value types (e.g., RDF* Triple value) so upstream can rollback/clear changes logger.error("Encountered an unexpected problem while trying to add a statement", e); - throw e; + throw new SailException(e); } finally { sinkStoreAccessLock.unlock(); } @@ -539,9 +545,10 @@ private boolean addStatement(Resource subj, IRI pred, Value obj, boolean explici } } catch (IOException e) { throw new SailException(e); - } catch (RuntimeException e) { + } catch (IllegalArgumentException e) { + // Wrap invalid value types (e.g., RDF* Triple value) so upstream can rollback/clear changes logger.error("Encountered an unexpected problem while trying to add a statement", e); - throw e; + throw new SailException(e); } finally { sinkStoreAccessLock.unlock(); } diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java index eb2a39fd8bc..04d66c1ce8f 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java @@ -76,8 +76,10 @@ protected void commitInternal() throws SailException { try { super.commitInternal(); } finally { - txnLock.release(); - txnLock = null; + if (txnLock != null) { + txnLock.release(); + txnLock = null; + } } nativeStore.notifySailChanged(sailChangedEvent); @@ -91,8 +93,10 @@ protected void rollbackInternal() throws SailException { try { super.rollbackInternal(); } finally { - txnLock.release(); - txnLock = null; + if (txnLock != null) { + txnLock.release(); + txnLock = null; + } } // create a fresh event object. sailChangedEvent = new DefaultSailChangedEvent(nativeStore); @@ -100,6 +104,8 @@ protected void rollbackInternal() throws SailException { @Override protected void addStatementInternal(Resource subj, IRI pred, Value obj, Resource... contexts) throws SailException { + // mark that statements were added so reads can flush pending updates pre-emptively + setStatementsAdded(); // assume the triple is not yet present in the triple store sailChangedEvent.setStatementsAdded(true); diff --git a/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreRDFStarRejectionTest.java b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreRDFStarRejectionTest.java new file mode 100644 index 00000000000..e44546cf185 --- /dev/null +++ b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreRDFStarRejectionTest.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.sail.nativerdf; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; + +import org.eclipse.rdf4j.common.transaction.IsolationLevel; +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.repository.Repository; +import org.eclipse.rdf4j.repository.RepositoryConnection; +import org.eclipse.rdf4j.repository.RepositoryResult; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Reproduces a reported issue where attempting to add Turtle-star data to a NativeStore throws, and subsequently the + * repository becomes unusable for normal operations. After the rejection, the repository should remain usable. + */ +public class NativeStoreRDFStarRejectionTest { + + @TempDir + public File dataDir; + + @Test + public void nativeStoreRejectsTurtleStarButRemainsUsable() { + String data = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "# Basic triple\n" + + "ex:JohnDoe ex:worksAt ex:CompanyX .\n" + + "\n" + + "# RDF* triple (unsupported by NativeStore)\n" + + "<> ex:since \"2022-01-01\"^^xsd:date .\n"; + + Repository repo = new SailRepository(new NativeStore(dataDir)); + + // First: attempt to add data that includes RDF*-star. Expect an exception (rejection). + try (RepositoryConnection conn = repo.getConnection()) { + try { + conn.add(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), null, RDFFormat.TURTLE); + } catch (Exception expected) { + // Expected: Turtle-star should be rejected by NativeStore + } + } + + // Then: repository should still be usable. Getting statements must not throw. + assertDoesNotThrow(() -> { + try (RepositoryConnection conn = repo.getConnection(); + RepositoryResult result = conn.getStatements(null, null, null)) { + // iterate to fully exercise the result set + while (result.hasNext()) { + result.next(); + } + } + }, "Repository became unusable after rejecting Turtle-star input"); + } + + @Test + public void nativeStoreRejectsTurtleStarButRemainsUsableSnapshot() { + String data = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "# Basic triple\n" + + "ex:JohnDoe ex:worksAt ex:CompanyX .\n" + + "\n" + + "# RDF* triple (unsupported by NativeStore)\n" + + "<> ex:since \"2022-01-01\"^^xsd:date .\n"; + + Repository repo = new SailRepository(new NativeStore(dataDir)); + + // First: attempt to add data that includes RDF*-star. Expect an exception (rejection). + try (RepositoryConnection conn = repo.getConnection()) { + try { + conn.begin(IsolationLevels.SNAPSHOT); + conn.add(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), null, RDFFormat.TURTLE); + conn.commit(); + } catch (Exception expected) { + // Expected: Turtle-star should be rejected by NativeStore + } + } + + // Then: repository should still be usable. Getting statements must not throw. + assertDoesNotThrow(() -> { + try (RepositoryConnection conn = repo.getConnection(); + RepositoryResult result = conn.getStatements(null, null, null)) { + // iterate to fully exercise the result set + while (result.hasNext()) { + result.next(); + } + } + }, "Repository became unusable after rejecting Turtle-star input"); + } +} From f6a91d448dbb51b0d7d7a43a01e1c4c2ac2260e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Mon, 15 Sep 2025 12:04:55 +0200 Subject: [PATCH 2/4] GH-5123 catch all throwables and revert changes --- .../rdf4j/sail/base/SailSourceBranch.java | 5 +- .../rdf4j/sail/nativerdf/NativeSailStore.java | 30 +++-- .../sail/nativerdf/NativeStoreConnection.java | 2 - .../NativeSailStoreRuntimeWrappingIT.java | 113 ++++++++++++++++++ 4 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStoreRuntimeWrappingIT.java diff --git a/core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/SailSourceBranch.java b/core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/SailSourceBranch.java index 15a38e54272..f2d777759f8 100644 --- a/core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/SailSourceBranch.java +++ b/core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/SailSourceBranch.java @@ -27,6 +27,7 @@ import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.ModelFactory; import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.impl.DynamicModelFactory; import org.eclipse.rdf4j.sail.SailException; @@ -70,7 +71,7 @@ class SailSourceBranch implements SailSource { /** * The {@link Model} instances that should be used to store {@link SailSink#approve(Resource, IRI, Value, Resource)} - * and {@link SailSink#deprecate(Resource, IRI, Value, Resource)} statements. + * and {@link SailSink#deprecate(Statement)} statements. */ private final ModelFactory modelFactory; @@ -306,7 +307,7 @@ public void flush() throws SailException { prepared = null; } } - } catch (SailException e) { + } catch (Throwable e) { // clear changes if flush fails changes.clear(); prepared = null; diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java index 4331fad3acc..91414f78c18 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java @@ -37,6 +37,7 @@ import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.query.algebra.evaluation.impl.EvaluationStatistics; +import org.eclipse.rdf4j.sail.Sail; import org.eclipse.rdf4j.sail.SailException; import org.eclipse.rdf4j.sail.base.BackingSailSource; import org.eclipse.rdf4j.sail.base.Changeset; @@ -389,7 +390,11 @@ public synchronized void flush() throws SailException { throw new SailException(e); } catch (RuntimeException e) { logger.error("Encountered an unexpected problem while trying to commit", e); - throw e; + if (e instanceof SailException) { + throw e; + } + // Ensure upstream handles this as a SailException so branch flush clears pending changes + throw new SailException(e); } finally { sinkStoreAccessLock.unlock(); } @@ -447,7 +452,10 @@ public void clear(Resource... contexts) throws SailException { public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException { try { addStatement(subj, pred, obj, explicit, ctx); - } catch (IllegalArgumentException e) { + } catch (RuntimeException e) { + if (e instanceof SailException) { + throw e; + } // Ensure upstream handles this as a SailException so branch flush clears pending changes throw new SailException(e); } @@ -482,8 +490,10 @@ public void approveAll(Set approved, Set approvedContexts) } } catch (IOException e) { throw new SailException(e); - } catch (IllegalArgumentException e) { - // Wrap invalid value types (e.g., RDF* Triple value) so upstream can rollback/clear changes + } catch (RuntimeException e) { + if (e instanceof SailException) { + throw e; + } logger.error("Encountered an unexpected problem while trying to add a statement", e); throw new SailException(e); } finally { @@ -545,8 +555,10 @@ private boolean addStatement(Resource subj, IRI pred, Value obj, boolean explici } } catch (IOException e) { throw new SailException(e); - } catch (IllegalArgumentException e) { - // Wrap invalid value types (e.g., RDF* Triple value) so upstream can rollback/clear changes + } catch (RuntimeException e) { + if (e instanceof SailException) { + throw e; + } logger.error("Encountered an unexpected problem while trying to add a statement", e); throw new SailException(e); } finally { @@ -621,7 +633,11 @@ private long removeStatements(Resource subj, IRI pred, Value obj, boolean explic throw new SailException(e); } catch (RuntimeException e) { logger.error("Encountered an unexpected problem while trying to remove statements", e); - throw e; + if (e instanceof SailException) { + throw e; + } + // Ensure upstream handles this as a SailException so branch flush clears pending changes + throw new SailException(e); } finally { sinkStoreAccessLock.unlock(); } diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java index 04d66c1ce8f..63145e1852e 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java @@ -104,8 +104,6 @@ protected void rollbackInternal() throws SailException { @Override protected void addStatementInternal(Resource subj, IRI pred, Value obj, Resource... contexts) throws SailException { - // mark that statements were added so reads can flush pending updates pre-emptively - setStatementsAdded(); // assume the triple is not yet present in the triple store sailChangedEvent.setStatementsAdded(true); diff --git a/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStoreRuntimeWrappingIT.java b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStoreRuntimeWrappingIT.java new file mode 100644 index 00000000000..bcd8a3506fc --- /dev/null +++ b/core/sail/nativerdf/src/test/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStoreRuntimeWrappingIT.java @@ -0,0 +1,113 @@ +/* + * 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.sail.nativerdf; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.Map; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.sail.SailException; +import org.eclipse.rdf4j.sail.base.SailSink; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Verifies that NativeSailStore wraps unexpected RuntimeExceptions in SailException so upstream callers can reliably + * handle failures (e.g., branch flush clearing pending changes). + */ +public class NativeSailStoreRuntimeWrappingIT { + + private File dataDir; + private NativeSailStore store; + + @BeforeEach + public void setUp() throws Exception { + dataDir = Files.createTempDirectory("nativestore-wrap-test").toFile(); + store = new NativeSailStore(dataDir, "spoc"); + } + + @AfterEach + public void tearDown() throws Exception { + if (store != null) { + store.close(); + } + } + + @Test + public void testFlushWrapsRuntimeIntoSailException() throws Exception { + // Replace TripleStore with a stub that throws at commit() + replaceTripleStore(new ThrowOnCommitTripleStore(dataDir, "spoc")); + + // Add a statement to ensure a triplestore transaction is started + SailSink sink = store.getExplicitSailSource().sink(null); + ValueFactory vf = store.getValueFactory(); + IRI s = vf.createIRI("urn:s"); + IRI p = vf.createIRI("urn:p"); + IRI o = vf.createIRI("urn:o"); + sink.approve(s, p, o, (Resource) null); + + // Now flush, expecting a SailException (wrapping the runtime) + assertThatThrownBy(() -> sink.flush()) + .isInstanceOf(SailException.class); + } + + @Test + public void testRemoveStatementsWrapsRuntimeIntoSailException() throws Exception { + // Replace TripleStore with a stub that throws at removeTriplesByContext() + replaceTripleStore(new ThrowOnRemoveTripleStore(dataDir, "spoc")); + + SailSink sink = store.getExplicitSailSource().sink(null); + // Expect SailException when attempting to remove (deprecateByQuery delegates) + assertThatThrownBy(() -> sink.deprecateByQuery(null, null, null, new Resource[] { null })) + .isInstanceOf(SailException.class); + } + + private void replaceTripleStore(TripleStore newTripleStore) throws Exception { + Field f = NativeSailStore.class.getDeclaredField("tripleStore"); + f.setAccessible(true); + f.set(store, newTripleStore); + } + + private static class ThrowOnCommitTripleStore extends TripleStore { + public ThrowOnCommitTripleStore(File dir, String indexSpecStr) throws Exception { + super(dir, indexSpecStr, false); + } + + @Override + public void commit() { + throw new RuntimeException("simulated failure during commit"); + } + } + + private static class ThrowOnRemoveTripleStore extends TripleStore { + public ThrowOnRemoveTripleStore(File dir, String indexSpecStr) throws Exception { + super(dir, indexSpecStr, false); + } + + @Override + public Map removeTriplesByContext(int subjID, int predID, int objID, int contextId, + boolean explicit) { + throw new RuntimeException("simulated failure during removeTriplesByContext"); + } + + @Override + public void startTransaction() { + // no-op; we're only interested in remove path throwing + } + } +} From f2a8938adb1a5fd6f5a2d67e5107c731a656663e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Mon, 15 Sep 2025 12:30:36 +0200 Subject: [PATCH 3/4] GH-5407 Add failing Turtle test for integer-literal + dot at EOF; require issue confirmation before branching --- AGENTS.md | 1 + .../rdf4j/rio/turtle/TurtleParserTest.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2bf80dd6b51..32592908ba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -510,6 +510,7 @@ Do **not** modify existing headers’ years. ## Branch & PR Workflow (Agent) +- Confirm issue number first (mandatory): before creating a branch, pause and request/confirm the GitHub issue number. Do not proceed to branch creation until the issue number is provided or confirmed. - Name branch: `GH--` (kebab‑case slug). - Create branch: `git checkout -b GH-XXXX-your-slug`. - Stage changes: `git add -A` (ensure new Java files have the required header). diff --git a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java index 1ac74f3ac32..fbaac39a455 100644 --- a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java +++ b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java @@ -89,6 +89,27 @@ public void testParseDots() throws IOException { } + @Test + public void testIntegerFollowedByDotAtEOF() throws IOException { + // Reproduces bug: integer literal immediately followed by statement terminator '.' at EOF + String data = prefixes + ":alice :age 30."; // no trailing whitespace/newline + + parser.parse(new StringReader(data), baseURI); + + assertTrue(errorCollector.getWarnings().isEmpty()); + assertTrue(errorCollector.getErrors().isEmpty()); + assertTrue(errorCollector.getFatalErrors().isEmpty()); + + assertEquals(1, statementCollector.getStatements().size()); + Statement st = statementCollector.getStatements().iterator().next(); + assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject()); + assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate()); + assertTrue(st.getObject() instanceof Literal); + Literal lit = (Literal) st.getObject(); + assertEquals("30", lit.getLabel()); + assertEquals(XSD.INTEGER, lit.getDatatype()); + } + @Test public void testParseIllegalURIFatal() throws IOException { String data = " ; . . "; From 05d98155a5888e4fd5e0f1748de5810175c65620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Mon, 15 Sep 2025 12:55:12 +0200 Subject: [PATCH 4/4] GH-5407 Turtle: treat '.' at EOF as statement terminator after digits; fix integer literal-at-EOF parsing --- .../rdf4j/rio/turtle/TurtleParser.java | 6 +++++- .../rdf4j/rio/turtle/TurtleParserTest.java | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleParser.java b/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleParser.java index 93c86c300a8..5057cf5663b 100644 --- a/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleParser.java +++ b/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleParser.java @@ -808,7 +808,11 @@ protected Literal parseNumber() throws IOException, RDFParseException { // read optional fractional digits if (c == '.') { - if (TurtleUtil.isWhitespace(peekCodePoint())) { + int next = peekCodePoint(); + // Treat '.' as statement terminator at EOF only when we already parsed at least one digit + // (e.g., "30.") or when whitespace follows. Otherwise, attempt to parse as decimal + // which will surface a useful error for a stray '.' token. + if ((value.length() > 0 && next == -1) || TurtleUtil.isWhitespace(next)) { // We're parsing an integer that did not have a space before // the // period to end the statement diff --git a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java index fbaac39a455..fc513360ca2 100644 --- a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java +++ b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java @@ -110,6 +110,26 @@ public void testIntegerFollowedByDotAtEOF() throws IOException { assertEquals(XSD.INTEGER, lit.getDatatype()); } + @Test + public void testLetterFollowedByDotAtEOF() throws IOException { + String data = prefixes + ":alice :age \"a\"."; // no trailing whitespace/newline + + parser.parse(new StringReader(data), baseURI); + + assertTrue(errorCollector.getWarnings().isEmpty()); + assertTrue(errorCollector.getErrors().isEmpty()); + assertTrue(errorCollector.getFatalErrors().isEmpty()); + + assertEquals(1, statementCollector.getStatements().size()); + Statement st = statementCollector.getStatements().iterator().next(); + assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject()); + assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate()); + assertTrue(st.getObject() instanceof Literal); + Literal lit = (Literal) st.getObject(); + assertEquals("a", lit.getLabel()); + assertEquals(XSD.STRING, lit.getDatatype()); + } + @Test public void testParseIllegalURIFatal() throws IOException { String data = " ; . . ";