diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/Literal.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/Literal.java index b261e76061c..a51c634327b 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/Literal.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/Literal.java @@ -41,6 +41,36 @@ * @see XML Schema Definition Language (XSD) 1.1 Part 2: Datatypes */ public interface Literal extends Value { + String LTR_SUFFIX = "--ltr"; + String RTL_SUFFIX = "--rtl"; + String BASE_DIR_SEPARATOR = "--"; + + enum BaseDirection { + NONE(""), + LTR(LTR_SUFFIX), + RTL(RTL_SUFFIX); + + private final String suffix; + + BaseDirection(final String suffix) { + this.suffix = suffix; + } + + @Override + public String toString() { + return suffix; + } + + public static BaseDirection fromString(final String dir) { + if (dir == null || dir.isEmpty()) + return NONE; + if (dir.equals(LTR_SUFFIX)) + return LTR; + if (dir.equals(RTL_SUFFIX)) + return RTL; + throw new IllegalArgumentException("Unknown BaseDirection: " + dir); + } + } @Override default boolean isLiteral() { @@ -61,6 +91,10 @@ default boolean isLiteral() { */ Optional getLanguage(); + default BaseDirection getBaseDirection() { + return BaseDirection.NONE; + } + /** * Gets the datatype for this literal. *

diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/ValueFactory.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/ValueFactory.java index 00441731e57..0c13899133b 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/ValueFactory.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/ValueFactory.java @@ -86,6 +86,18 @@ public interface ValueFactory { */ Literal createLiteral(String label, String language); + /** + * Creates a new literal with the supplied label and language attribute. The return value of + * {@link Literal#getDatatype()} for the returned object must be + * {@code rdf:langString}. + * + * @param label The literal's label, must not be null. + * @param language The literal's language attribute, must not be null. + * @param baseDirection The literal's base direction, either "", "--ltr", or "--rtl". + * @return A literal for the specified value and language attribute. + */ + Literal createLiteral(String label, String language, Literal.BaseDirection baseDirection); + /** * Creates a new literal with the supplied label and datatype. * diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractLiteral.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractLiteral.java index 635c12a8847..d64ae697c1f 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractLiteral.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractLiteral.java @@ -77,11 +77,12 @@ public abstract class AbstractLiteral implements Literal { private static final long serialVersionUID = -1286527360744086451L; static boolean reserved(IRI datatype) { - return CoreDatatype.RDF.LANGSTRING.getIri().equals(datatype); + return CoreDatatype.RDF.LANGSTRING.getIri().equals(datatype) + || CoreDatatype.RDF.DIRLANGSTRING.getIri().equals(datatype); } static boolean reserved(CoreDatatype datatype) { - return CoreDatatype.RDF.LANGSTRING == datatype; + return CoreDatatype.RDF.LANGSTRING == datatype || CoreDatatype.RDF.DIRLANGSTRING == datatype; } /** @@ -186,7 +187,7 @@ public String toString() { return getLanguage() - .map(language -> label + '@' + language) + .map(language -> label + '@' + language + getBaseDirection()) .orElseGet(() -> CoreDatatype.XSD.STRING == getCoreDatatype() ? label : label + "^^<" + getDatatype().stringValue() + ">"); @@ -268,10 +269,16 @@ static class TaggedLiteral extends AbstractLiteral { private final String label; private final String language; + private final BaseDirection baseDirection; TaggedLiteral(String label, String language) { + this(label, language, BaseDirection.NONE); + } + + TaggedLiteral(String label, String language, BaseDirection baseDirection) { this.label = label; this.language = language; + this.baseDirection = baseDirection; } @Override @@ -284,14 +291,20 @@ public Optional getLanguage() { return Optional.of(language); } + @Override + public BaseDirection getBaseDirection() { + return baseDirection; + } + @Override public IRI getDatatype() { - return CoreDatatype.RDF.LANGSTRING.getIri(); + return baseDirection == BaseDirection.NONE ? CoreDatatype.RDF.LANGSTRING.getIri() + : CoreDatatype.RDF.DIRLANGSTRING.getIri(); } @Override public CoreDatatype.RDF getCoreDatatype() { - return CoreDatatype.RDF.LANGSTRING; + return baseDirection == BaseDirection.NONE ? CoreDatatype.RDF.LANGSTRING : CoreDatatype.RDF.DIRLANGSTRING; } } diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractValueFactory.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractValueFactory.java index e88070a5af3..4d5ba3f39a3 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractValueFactory.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/AbstractValueFactory.java @@ -146,15 +146,21 @@ public Literal createLiteral(String label, IRI datatype, CoreDatatype coreDataty @Override public Literal createLiteral(String label, String language) { + return createLiteral(label, language, Literal.BaseDirection.NONE); + } + + @Override + public Literal createLiteral(String label, String language, Literal.BaseDirection baseDirection) { Objects.requireNonNull(label, "null label"); Objects.requireNonNull(language, "null language"); + Objects.requireNonNull(baseDirection, "null baseDirection"); if (language.isEmpty()) { throw new IllegalArgumentException("empty language tag"); } - return new TaggedLiteral(label, language); + return new TaggedLiteral(label, language, baseDirection); } @Override diff --git a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java index 50af84d1df3..c48be81ccfd 100644 --- a/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java +++ b/core/model-api/src/main/java/org/eclipse/rdf4j/model/base/CoreDatatype.java @@ -283,7 +283,9 @@ public String toString() { enum RDF implements CoreDatatype { HTML(iri("HTML")), + JSON(iri("JSON")), XMLLITERAL(iri("XMLLiteral")), + DIRLANGSTRING(iri("dirLangString")), LANGSTRING(iri("langString")); public static final String NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; diff --git a/core/model-api/src/test/java/org/eclipse/rdf4j/model/LiteralTest.java b/core/model-api/src/test/java/org/eclipse/rdf4j/model/LiteralTest.java index be492efbef6..d7ddc088be3 100644 --- a/core/model-api/src/test/java/org/eclipse/rdf4j/model/LiteralTest.java +++ b/core/model-api/src/test/java/org/eclipse/rdf4j/model/LiteralTest.java @@ -85,6 +85,7 @@ public abstract class LiteralTest { static final String XSD_DURATION_YEARMONTH = XSD + "yearMonthDuration"; static final String RDF_LANG_STRING = RDF + "langString"; + static final String RDF_DIR_LANG_STRING = RDF + "dirLangString"; /** * Creates a test literal instance. @@ -103,6 +104,16 @@ public abstract class LiteralTest { */ protected abstract Literal literal(String label, String language); + /** + * Creates a test literal instance. + * + * @param label the label of the literal + * @param language the language of the literal + * @param dir the language direction of the literal + * @return a new instance of the concrete literal class under test + */ + protected abstract Literal literal(String label, String language, Literal.BaseDirection dir); + /** * Creates a test literal instance. * @@ -168,14 +179,24 @@ public final void testTaggedConstructor() { final String label = "label"; final String language = "en"; - final Literal literal = literal(label, language); + final Literal languageLiteral = literal(label, language); - assertThat(literal.getLabel()).isEqualTo(label); - assertThat(literal.getLanguage()).contains(language); - assertThat(literal.getDatatype().stringValue()).isEqualTo(RDF_LANG_STRING); + assertThat(languageLiteral.getLabel()).isEqualTo(label); + assertThat(languageLiteral.getLanguage()).contains(language); + assertEquals(Literal.BaseDirection.NONE, languageLiteral.getBaseDirection()); + assertThat(languageLiteral.getDatatype().stringValue()).isEqualTo(RDF_LANG_STRING); + + final Literal directedLanguageLiteral = literal(label, language, Literal.BaseDirection.LTR); + + assertThat(directedLanguageLiteral.getLabel()).isEqualTo(label); + assertThat(directedLanguageLiteral.getLanguage()).contains(language); + assertThat(directedLanguageLiteral.getBaseDirection().toString()).isEqualTo(Literal.LTR_SUFFIX); + assertThat(directedLanguageLiteral.getDatatype().stringValue()).isEqualTo(RDF_DIR_LANG_STRING); assertThatNullPointerException().isThrownBy(() -> literal(null, (String) null)); assertThatNullPointerException().isThrownBy(() -> literal("", (String) null)); + assertThatNullPointerException().isThrownBy(() -> literal("", (String) null, Literal.BaseDirection.NONE)); + assertThatNullPointerException().isThrownBy(() -> literal("", (String) null, Literal.BaseDirection.LTR)); assertThatNullPointerException().isThrownBy(() -> literal(null, "")); assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null)); @@ -198,8 +219,10 @@ public final void testTypedConstructor() { assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null)); assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(XSD_STRING))); assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(RDF_LANG_STRING))); + assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(RDF_DIR_LANG_STRING))); assertThatIllegalArgumentException().isThrownBy(() -> literal("", datatype(RDF_LANG_STRING))); + assertThatIllegalArgumentException().isThrownBy(() -> literal("", datatype(RDF_DIR_LANG_STRING))); } @@ -763,10 +786,13 @@ public void testEqualsAndHashCode() { final Literal plain = literal("plain"); final Literal tagged = literal("tagged", "en"); + final Literal tagged_with_direction = literal("tagged", "en--ltr"); final Literal typed = literal("typed", datatype("http://example.org/datatype")); final Literal _plain = literal(plain.getLabel()); final Literal _tagged = literal(tagged.getLabel(), tagged.getLanguage().orElse("")); + final Literal _tagged_with_direction = literal(tagged_with_direction.getLabel(), + tagged_with_direction.getLanguage().orElse("")); final Literal _typed = literal(typed.getLabel(), typed.getDatatype()); assertThat(plain).isEqualTo(plain); @@ -775,6 +801,9 @@ public void testEqualsAndHashCode() { assertThat(tagged).isEqualTo(tagged); assertThat(tagged).isEqualTo(_tagged); + assertThat(tagged_with_direction).isEqualTo(tagged_with_direction); + assertThat(tagged_with_direction).isEqualTo(_tagged_with_direction); + assertThat(typed).isEqualTo(typed); assertThat(typed).isEqualTo(_typed); @@ -784,16 +813,21 @@ public void testEqualsAndHashCode() { assertThat(plain).isNotEqualTo(tagged); assertThat(plain).isNotEqualTo(typed); assertThat(tagged).isNotEqualTo(typed); + assertThat(tagged_with_direction).isNotEqualTo(plain); + assertThat(tagged_with_direction).isNotEqualTo(tagged); + assertThat(tagged_with_direction).isNotEqualTo(typed); assertThat(plain).isNotEqualTo(literal("other")); assertThat(tagged).isNotEqualTo(literal(tagged.getLabel(), "other")); assertThat(typed).isNotEqualTo(literal(typed.getLabel(), datatype("http://example.org/other"))); + assertThat(_tagged_with_direction).isNotEqualTo(literal(tagged_with_direction.getLabel(), "en--rtl")); // hashCode() should return identical values for literals for which equals() is true - assertThat(plain.hashCode()).isEqualTo(_plain.hashCode()); - assertThat(tagged.hashCode()).isEqualTo(_tagged.hashCode()); - assertThat(typed.hashCode()).isEqualTo(_typed.hashCode()); + assertThat(plain).hasSameHashCodeAs(_plain); + assertThat(tagged).hasSameHashCodeAs(_tagged); + assertThat(tagged_with_direction).hasSameHashCodeAs(_tagged_with_direction); + assertThat(typed).hasSameHashCodeAs(_typed); assertThat(tagged.hashCode()) .as("computed according to contract") @@ -860,11 +894,20 @@ public final void testCoreDatatypeTaggedConstructor() { String label = "label"; String language = "en"; - Literal literal = literal(label, language); + Literal languageLiteral = literal(label, language); + Literal directedLanguageLiteral = literal(label, language, Literal.BaseDirection.LTR); - assertThat(literal.getLabel()).isEqualTo(label); - assertThat(literal.getLanguage()).contains(language); - assertThat(literal.getCoreDatatype()).isEqualTo(CoreDatatype.RDF.LANGSTRING); + assertThat(languageLiteral.getLabel()).isEqualTo(label); + assertThat(languageLiteral.getLanguage()).contains(language); + assertEquals(Literal.BaseDirection.NONE, languageLiteral.getBaseDirection()); + assertThat(languageLiteral.getCoreDatatype()).isEqualTo(CoreDatatype.RDF.LANGSTRING); + assertEquals("\"label\"@en", languageLiteral.toString()); + + assertThat(directedLanguageLiteral.getLabel()).isEqualTo(label); + assertThat(directedLanguageLiteral.getLanguage()).contains(language); + assertEquals(Literal.BaseDirection.LTR, directedLanguageLiteral.getBaseDirection()); + assertThat(directedLanguageLiteral.getCoreDatatype()).isEqualTo(CoreDatatype.RDF.DIRLANGSTRING); + assertEquals("\"label\"@en--ltr", directedLanguageLiteral.toString()); assertThatNullPointerException().isThrownBy(() -> literal(null, (String) null)); assertThatNullPointerException().isThrownBy(() -> literal("", (String) null)); @@ -890,8 +933,10 @@ public final void testCoreDatatypeTypedConstructor() { assertThatNullPointerException().isThrownBy(() -> literal(null, (CoreDatatype) null)); assertThatNullPointerException().isThrownBy(() -> literal(null, CoreDatatype.XSD.STRING)); assertThatNullPointerException().isThrownBy(() -> literal(null, CoreDatatype.RDF.LANGSTRING)); + assertThatNullPointerException().isThrownBy(() -> literal(null, CoreDatatype.RDF.DIRLANGSTRING)); assertThatIllegalArgumentException().isThrownBy(() -> literal("", CoreDatatype.RDF.LANGSTRING)); + assertThatIllegalArgumentException().isThrownBy(() -> literal("", CoreDatatype.RDF.DIRLANGSTRING)); } @@ -911,6 +956,7 @@ public void testCoreDatatypeStringValue() { assertThat(literal(label).stringValue()).isEqualTo(label); assertThat(literal(label, language).stringValue()).isEqualTo(label); + assertThat(literal(label, language, Literal.BaseDirection.LTR).stringValue()).isEqualTo(label); assertThat(literal(label, datatype).stringValue()).isEqualTo(label); } @@ -1442,49 +1488,6 @@ public final void testCoreDatatypeCalendarValue() throws DatatypeConfigurationEx //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Test - public void testCoreDatatypeEqualsAndHashCode() { - - Literal plain = literal("plain"); - Literal tagged = literal("tagged", "en"); - Literal typed = literal("typed", datatype("http://example.org/datatype")); - - Literal _plain = literal(plain.getLabel()); - Literal _tagged = literal(tagged.getLabel(), tagged.getLanguage().orElse("")); - Literal _typed = literal(typed.getLabel(), typed.getDatatype()); - - assertThat(plain).isEqualTo(plain); - assertThat(plain).isEqualTo(_plain); - - assertThat(tagged).isEqualTo(tagged); - assertThat(tagged).isEqualTo(_tagged); - - assertThat(typed).isEqualTo(typed); - assertThat(typed).isEqualTo(_typed); - - assertThat(plain).isNotEqualTo(null); - assertThat(plain).isNotEqualTo(new Object()); - - assertThat(plain).isNotEqualTo(tagged); - assertThat(plain).isNotEqualTo(typed); - assertThat(tagged).isNotEqualTo(typed); - - assertThat(plain).isNotEqualTo(literal("other")); - assertThat(tagged).isNotEqualTo(literal(tagged.getLabel(), "other")); - assertThat(typed).isNotEqualTo(literal(typed.getLabel(), "http://example.org/other")); - - // hashCode() should return identical values for literals for which equals() is true - - assertThat(plain.hashCode()).isEqualTo(_plain.hashCode()); - assertThat(tagged.hashCode()).isEqualTo(_tagged.hashCode()); - assertThat(typed.hashCode()).isEqualTo(_typed.hashCode()); - - assertThat(tagged.hashCode()) - .as("computed according to contract") - .isEqualTo(tagged.getLabel().hashCode()); // !!! label >> label+language+datatype - - } - @Test public final void testCoreDatatypeEqualsAndHashCodeCaseInsensitiveLanguage() { @@ -1528,6 +1531,23 @@ public final void testSerializationWithCoreDatatypeRdfLangString() { assertEquals(CoreDatatype.RDF.LANGSTRING, roundTrip.getCoreDatatype()); } + @Test + public final void testSerializationWithCoreDatatypeRdfDirLangString() { + Literal literal = literal("hello", "en", Literal.BaseDirection.LTR); + assertEquals(CoreDatatype.RDF.DIRLANGSTRING, literal.getCoreDatatype()); + assertThat(literal.getLanguage()).isPresent(); + assertEquals("en", literal.getLanguage().get()); + assertEquals(Literal.BaseDirection.LTR, literal.getBaseDirection()); + + byte[] bytes = objectToBytes(literal); + Literal roundTrip = (Literal) bytesToObject(bytes); + + assertEquals(CoreDatatype.RDF.DIRLANGSTRING, roundTrip.getCoreDatatype()); + assertThat(roundTrip.getLanguage()).isPresent(); + assertEquals("en", roundTrip.getLanguage().get()); + assertEquals(Literal.BaseDirection.LTR, roundTrip.getBaseDirection()); + } + @Test public final void testSerializationWithCoreDatatypeGEO() { Literal literal = literal("1", CoreDatatype.GEO.WKT_LITERAL); @@ -1549,6 +1569,22 @@ public final void testSerializationWithCoreDatatype4() { assertEquals(CoreDatatype.XSD.NONE, roundTrip.getCoreDatatype()); } + @Test + void testBaseDirectionEnumFromString() { + assertThat(Literal.BaseDirection.fromString("")).isEqualTo(Literal.BaseDirection.NONE); + assertThat(Literal.BaseDirection.fromString(null)).isEqualTo(Literal.BaseDirection.NONE); + assertThat(Literal.BaseDirection.fromString(Literal.LTR_SUFFIX)).isEqualTo(Literal.BaseDirection.LTR); + assertThat(Literal.BaseDirection.fromString(Literal.RTL_SUFFIX)).isEqualTo(Literal.BaseDirection.RTL); + assertThrows(IllegalArgumentException.class, () -> Literal.BaseDirection.fromString("--invalid")); + } + + @Test + void testBaseDirectionEnumSuffix() { + assertThat(Literal.BaseDirection.LTR.toString()).isEqualTo(Literal.LTR_SUFFIX); + assertThat(Literal.BaseDirection.RTL.toString()).isEqualTo(Literal.RTL_SUFFIX); + assertThat(Literal.BaseDirection.NONE.toString()).isEmpty(); + } + private byte[] objectToBytes(Serializable object) { try (var byteArrayOutputStream = new ByteArrayOutputStream()) { try (var objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) { diff --git a/core/model-api/src/test/java/org/eclipse/rdf4j/model/ValueFactoryTest.java b/core/model-api/src/test/java/org/eclipse/rdf4j/model/ValueFactoryTest.java index 197b7a45369..5f051a81170 100644 --- a/core/model-api/src/test/java/org/eclipse/rdf4j/model/ValueFactoryTest.java +++ b/core/model-api/src/test/java/org/eclipse/rdf4j/model/ValueFactoryTest.java @@ -71,6 +71,7 @@ import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; +import org.eclipse.rdf4j.model.base.CoreDatatype; import org.junit.jupiter.api.Test; /** @@ -537,4 +538,16 @@ public void testCreateLiteralDate() throws DatatypeConfigurationException { } + @Test + public void testCreateDirLangLiteral() { + final Literal literal = factory().createLiteral("label", "he", Literal.BaseDirection.RTL); + + assertThat(literal).isNotNull(); + assertThat(literal.getLabel()).isEqualTo("label"); + assertThat(literal.getLanguage()).contains("he"); + assertThat(literal.getBaseDirection()).isEqualTo(Literal.BaseDirection.RTL); + assertThat(literal.getBaseDirection().toString()).isEqualTo("--rtl"); + assertThat(literal.getDatatype()).isEqualTo(CoreDatatype.RDF.DIRLANGSTRING.getIri()); + } + } diff --git a/core/model-api/src/test/java/org/eclipse/rdf4j/model/base/AbstractLiteralTest.java b/core/model-api/src/test/java/org/eclipse/rdf4j/model/base/AbstractLiteralTest.java index 03849f7a2c5..148f1693d96 100644 --- a/core/model-api/src/test/java/org/eclipse/rdf4j/model/base/AbstractLiteralTest.java +++ b/core/model-api/src/test/java/org/eclipse/rdf4j/model/base/AbstractLiteralTest.java @@ -11,11 +11,14 @@ package org.eclipse.rdf4j.model.base; +import static org.assertj.core.api.Assertions.assertThat; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.LiteralTest; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.base.AbstractValueFactoryTest.GenericValueFactory; +import org.junit.jupiter.api.Test; /** * Unit tests for {@link AbstractLiteral}. @@ -37,6 +40,11 @@ protected Literal literal(String label, String language) { return factory.createLiteral(label, language); } + @Override + protected Literal literal(String label, String language, Literal.BaseDirection dir) { + return factory.createLiteral(label, language, dir); + } + @Override protected Literal literal(String label, IRI datatype) { return factory.createLiteral(label, datatype); @@ -51,5 +59,4 @@ protected Literal literal(String label, CoreDatatype datatype) { protected IRI datatype(String iri) { return factory.createIRI(iri); } - } diff --git a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RDF.java b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RDF.java index 92bb07e4fe4..e6b94852872 100644 --- a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RDF.java +++ b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RDF.java @@ -87,7 +87,13 @@ public class RDF { /** http://www.w3.org/1999/02/22-rdf-syntax-ns#langString */ public static final IRI LANGSTRING = CoreDatatype.RDF.LANGSTRING.getIri(); + /** http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString */ + public static final IRI DIRLANGSTRING = CoreDatatype.RDF.DIRLANGSTRING.getIri(); + /** http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML */ public static final IRI HTML = CoreDatatype.RDF.HTML.getIri(); + /** http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON */ + public static final IRI JSON = CoreDatatype.RDF.JSON.getIri(); + } diff --git a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleLiteral.java b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleLiteral.java index fb86f383183..e39a424760f 100644 --- a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleLiteral.java +++ b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleLiteral.java @@ -63,6 +63,11 @@ public class SimpleLiteral extends AbstractLiteral { // Cached CoreDatatype, or null if not yet computed. private CoreDatatype coreDatatype = null; + /** + * The literal's base direction. + */ + private BaseDirection baseDirection = BaseDirection.NONE; + /*--------------* * Constructors * *--------------*/ @@ -88,8 +93,13 @@ protected SimpleLiteral(String label) { * @param language The language tag for the literal, must not be null and not be empty. */ protected SimpleLiteral(String label, String language) { + this(label, language, BaseDirection.NONE); + } + + protected SimpleLiteral(String label, String language, BaseDirection baseDirection) { setLabel(label); setLanguage(language); + setBaseDirection(baseDirection); } /** @@ -100,8 +110,9 @@ protected SimpleLiteral(String label, String language) { */ protected SimpleLiteral(String label, IRI datatype) { setLabel(label); - if (org.eclipse.rdf4j.model.vocabulary.RDF.LANGSTRING.equals(datatype)) { - throw new IllegalArgumentException("datatype rdf:langString requires a language tag"); + if (org.eclipse.rdf4j.model.vocabulary.RDF.LANGSTRING.equals(datatype) + || org.eclipse.rdf4j.model.vocabulary.RDF.DIRLANGSTRING.equals(datatype)) { + throw new IllegalArgumentException("datatype rdf:langString or rdf:dirLangString requires a language tag"); } else if (datatype == null) { setDatatype(CoreDatatype.XSD.STRING); } else { @@ -122,8 +133,8 @@ protected SimpleLiteral(String label, IRI datatype, CoreDatatype coreDatatype) { assert datatype != null; assert coreDatatype == CoreDatatype.NONE || datatype == coreDatatype.getIri(); - if (CoreDatatype.RDF.LANGSTRING == coreDatatype) { - throw new IllegalArgumentException("datatype rdf:langString requires a language tag"); + if (CoreDatatype.RDF.LANGSTRING == coreDatatype || CoreDatatype.RDF.DIRLANGSTRING == coreDatatype) { + throw new IllegalArgumentException("datatype rdf:langString or rdf:dirLangString requires a language tag"); } setLabel(label); @@ -133,8 +144,8 @@ protected SimpleLiteral(String label, IRI datatype, CoreDatatype coreDatatype) { protected SimpleLiteral(String label, CoreDatatype datatype) { setLabel(label); - if (datatype == CoreDatatype.RDF.LANGSTRING) { - throw new IllegalArgumentException("datatype rdf:langString requires a language tag"); + if (datatype == CoreDatatype.RDF.LANGSTRING || datatype == CoreDatatype.RDF.DIRLANGSTRING) { + throw new IllegalArgumentException("datatype rdf:langString or rdf:dirLangString requires a language tag"); } else { setDatatype(datatype); } @@ -163,7 +174,16 @@ protected void setLanguage(String language) { } this.language = language; optionalLanguageCache = Optional.of(language); - setDatatype(CoreDatatype.RDF.LANGSTRING); + } + + protected void setBaseDirection(BaseDirection baseDirection) { + Objects.requireNonNull(baseDirection, "null baseDirection"); + this.baseDirection = baseDirection; + if (this.baseDirection != BaseDirection.NONE) { + setDatatype(CoreDatatype.RDF.DIRLANGSTRING); + } else { + setDatatype(CoreDatatype.RDF.LANGSTRING); + } } @Override @@ -174,6 +194,10 @@ public Optional getLanguage() { return optionalLanguageCache; } + public BaseDirection getBaseDirection() { + return baseDirection; + } + protected void setDatatype(IRI datatype) { this.datatype = datatype; coreDatatype = CoreDatatype.from(datatype); @@ -260,6 +284,7 @@ public String toString() { StringBuilder sb = new StringBuilder(label.length() + language.length() + 3); sb.append('"').append(label).append('"'); sb.append('@').append(language); + sb.append(getBaseDirection()); return sb.toString(); } else if (org.eclipse.rdf4j.model.vocabulary.XSD.STRING.equals(datatype) || datatype == null) { StringBuilder sb = new StringBuilder(label.length() + 2); diff --git a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleValueFactory.java b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleValueFactory.java index b9b685b7fcd..e0b208b6c15 100644 --- a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleValueFactory.java +++ b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleValueFactory.java @@ -103,6 +103,11 @@ public Literal createLiteral(String value, String language) { return new SimpleLiteral(value, language); } + @Override + public Literal createLiteral(String value, String language, Literal.BaseDirection baseDirection) { + return new SimpleLiteral(value, language, baseDirection); + } + @Override public Literal createLiteral(boolean b) { return b ? BooleanLiteral.TRUE : BooleanLiteral.FALSE; diff --git a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/ValidatingValueFactory.java b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/ValidatingValueFactory.java index 0cfe2860938..7bddd6d8b15 100644 --- a/core/model/src/main/java/org/eclipse/rdf4j/model/impl/ValidatingValueFactory.java +++ b/core/model/src/main/java/org/eclipse/rdf4j/model/impl/ValidatingValueFactory.java @@ -134,10 +134,15 @@ public Literal createLiteral(String label, IRI datatype, CoreDatatype coreDataty @Override public Literal createLiteral(String label, String language) { + return createLiteral(label, language, Literal.BaseDirection.NONE); + } + + @Override + public Literal createLiteral(String label, String language, Literal.BaseDirection baseDirection) { if (!Literals.isValidLanguageTag(language)) { throw new IllegalArgumentException("Not a valid language tag: " + language); } - return delegate.createLiteral(label, language); + return delegate.createLiteral(label, language, baseDirection); } @Override diff --git a/core/model/src/main/java/org/eclipse/rdf4j/model/util/Literals.java b/core/model/src/main/java/org/eclipse/rdf4j/model/util/Literals.java index cfd77d59ebe..671aa1fc06e 100644 --- a/core/model/src/main/java/org/eclipse/rdf4j/model/util/Literals.java +++ b/core/model/src/main/java/org/eclipse/rdf4j/model/util/Literals.java @@ -473,7 +473,8 @@ public static boolean canCreateLiteral(Object object) { * @return True if the literal has a language tag attached to it and false otherwise. */ public static boolean isLanguageLiteral(Literal literal) { - return literal.getCoreDatatype() == CoreDatatype.RDF.LANGSTRING; + return literal.getCoreDatatype() == CoreDatatype.RDF.LANGSTRING + || literal.getCoreDatatype() == CoreDatatype.RDF.DIRLANGSTRING; } /** @@ -495,7 +496,7 @@ public static String normalizeLanguageTag(String languageTag) throws IllformedLo new Locale.Builder().setLanguageTag(languageTag); // all subtags are case-insensitive - String normalizedTag = languageTag.toLowerCase(); + final String normalizedTag = languageTag.toLowerCase(); String[] subtags = normalizedTag.split("-"); for (int i = 1; i < subtags.length; i++) { diff --git a/core/model/src/test/java/org/eclipse/rdf4j/model/impl/SimpleLiteralTest.java b/core/model/src/test/java/org/eclipse/rdf4j/model/impl/SimpleLiteralTest.java index 61dab2ff8e3..82b6a40d8f6 100644 --- a/core/model/src/test/java/org/eclipse/rdf4j/model/impl/SimpleLiteralTest.java +++ b/core/model/src/test/java/org/eclipse/rdf4j/model/impl/SimpleLiteralTest.java @@ -30,6 +30,11 @@ protected Literal literal(String label, String language) { return new SimpleLiteral(label, language); } + @Override + protected Literal literal(String label, String language, Literal.BaseDirection dir) { + return new SimpleLiteral(label, language, dir); + } + @Override protected Literal literal(String label, IRI datatype) { return new SimpleLiteral(label, datatype); diff --git a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/LanguageHandler.java b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/LanguageHandler.java index 935e87ebbb4..40bbb1cf7df 100644 --- a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/LanguageHandler.java +++ b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/LanguageHandler.java @@ -86,6 +86,28 @@ public interface LanguageHandler { Literal normalizeLanguage(String literalValue, String languageTag, ValueFactory valueFactory) throws LiteralUtilException; + /** + * Normalize both the language tag and the language if appropriate, and use the given value factory to generate a + * literal matching the literal value and language tag. + *

+ * This method must only be called after verifying that {@link #isRecognizedLanguage(String)} returns true for the + * given language tag, and {@link #verifyLanguage(String, String)} also returns true for the given language and + * literal value. + * + * @param literalValue Required literal value to use in the normalization process and to provide the value for the + * resulting literal. + * @param languageTag The language tag which is to be normalized. This tag is available in normalized form from the + * result using {@link Literal#getLanguage()}. + * @param baseDir + * @param valueFactory The {@link ValueFactory} to use to create the result literal. + * @return A {@link Literal} containing the normalized literal value and language tag. + * @throws LiteralUtilException If the language tag was not recognized or verified, or the literal value could not + * be normalized due to an error. + */ + Literal normalizeLanguage(String literalValue, String languageTag, Literal.BaseDirection baseDir, + ValueFactory valueFactory) + throws LiteralUtilException; + /** * A unique key for this language handler to identify it in the LanguageHandlerRegistry. * diff --git a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/NTriplesUtil.java b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/NTriplesUtil.java index ad2195abaf0..486d8e2573d 100644 --- a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/NTriplesUtil.java +++ b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/NTriplesUtil.java @@ -565,6 +565,7 @@ public static void append(Literal lit, Appendable appendable, boolean xsdStringT // Append the literal's language appendable.append("@"); appendable.append(lit.getLanguage().get()); + appendable.append(lit.getBaseDirection().toString()); } else { // SES-1917 : In RDF-1.1, all literals have a type, and if they are not // language literals we display the type for backwards compatibility diff --git a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelper.java b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelper.java index fbb27d608ab..fad0afc2c7b 100644 --- a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelper.java +++ b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelper.java @@ -99,6 +99,7 @@ public static Literal createLiteral(String label, String lang, IRI datatype, Par Literal result = null; String workingLabel = label; Optional workingLang = Optional.ofNullable(lang); + Literal.BaseDirection workingBaseDirection = Literal.BaseDirection.NONE; IRI workingDatatype = datatype; // In RDF-1.1 we must do lang check first as language literals will all @@ -106,31 +107,50 @@ public static Literal createLiteral(String label, String lang, IRI datatype, Par // non-null lang if (workingLang.isPresent() && (workingDatatype == null || RDF.LANGSTRING.equals(workingDatatype))) { boolean recognisedLanguage = false; + + // Split workingLang into language tag and base direction so both can be separately verified + final int index = lang.indexOf(Literal.BASE_DIR_SEPARATOR); + if (index != -1) { + workingLang = Optional.of(lang.substring(0, index)); + final String baseDirectionString = lang.substring(index); + + try { + workingBaseDirection = Literal.BaseDirection.fromString(baseDirectionString); + } catch (IllegalArgumentException e) { + reportFatalError("'" + baseDirectionString + "' is not a valid base direction ", lineNo, columnNo, + errListener); + } + } + for (LanguageHandler nextHandler : parserConfig.get(BasicParserSettings.LANGUAGE_HANDLERS)) { if (nextHandler.isRecognizedLanguage(workingLang.get())) { recognisedLanguage = true; if (parserConfig.get(BasicParserSettings.VERIFY_LANGUAGE_TAGS)) { try { if (!nextHandler.verifyLanguage(workingLabel, workingLang.get())) { - reportError("'" + lang + "' is not a valid language tag ", lineNo, columnNo, + reportError("'" + workingLang.get() + "' is not a valid language tag ", lineNo, + columnNo, BasicParserSettings.VERIFY_LANGUAGE_TAGS, parserConfig, errListener); } } catch (LiteralUtilException e) { reportError("'" + label + " could not be verified by a language handler that recognised it. language was " - + lang, lineNo, columnNo, BasicParserSettings.VERIFY_LANGUAGE_TAGS, parserConfig, + + workingLang.get(), lineNo, columnNo, BasicParserSettings.VERIFY_LANGUAGE_TAGS, + parserConfig, errListener); } } if (parserConfig.get(BasicParserSettings.NORMALIZE_LANGUAGE_TAGS)) { try { - result = nextHandler.normalizeLanguage(workingLabel, workingLang.get(), valueFactory); + result = nextHandler.normalizeLanguage(workingLabel, workingLang.get(), + workingBaseDirection, valueFactory); workingLabel = result.getLabel(); workingLang = result.getLanguage(); workingDatatype = result.getDatatype(); } catch (LiteralUtilException e) { reportError( - "'" + label + "' did not have a valid value for language " + lang + ": " + "'" + label + "' did not have a valid value for language " + workingLang.get() + + ": " + e.getMessage() + " and could not be normalised", lineNo, columnNo, BasicParserSettings.NORMALIZE_LANGUAGE_TAGS, parserConfig, errListener); @@ -141,7 +161,8 @@ public static Literal createLiteral(String label, String lang, IRI datatype, Par if (!recognisedLanguage) { reportError("'" + label + "' was not recognised as a language literal, and could not be verified, with language " - + lang, lineNo, columnNo, BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES, parserConfig, + + workingLang.get(), lineNo, columnNo, BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES, + parserConfig, errListener); } } else if (workingDatatype != null) { @@ -189,15 +210,18 @@ public static Literal createLiteral(String label, String lang, IRI datatype, Par if (result == null) { try { // Removes datatype for langString datatype with no language tag when VERIFY_DATATYPE_VALUES is False. - if ((workingDatatype == null || RDF.LANGSTRING.equals(workingDatatype)) + if ((workingDatatype == null || RDF.LANGSTRING.equals(workingDatatype) + || RDF.DIRLANGSTRING.equals(workingDatatype)) && (workingLang.isEmpty() || workingLang.get().isEmpty()) && !parserConfig.get(BasicParserSettings.VERIFY_DATATYPE_VALUES)) { workingLang = Optional.ofNullable(null); workingDatatype = null; } // Backup for unnormalised language literal creation - if (workingLang.isPresent() && (workingDatatype == null || RDF.LANGSTRING.equals(workingDatatype))) { - result = valueFactory.createLiteral(workingLabel, workingLang.get().intern()); + if (workingLang.isPresent() + && (workingDatatype == null || RDF.LANGSTRING.equals(workingDatatype) + || RDF.DIRLANGSTRING.equals(workingDatatype))) { + result = valueFactory.createLiteral(workingLabel, workingLang.get().intern(), workingBaseDirection); } // Backup for unnormalised datatype literal creation else if (workingDatatype != null) { diff --git a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFStarDecodingValueFactory.java b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFStarDecodingValueFactory.java index a324b58c748..30d84a67de6 100644 --- a/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFStarDecodingValueFactory.java +++ b/core/rio/api/src/main/java/org/eclipse/rdf4j/rio/helpers/RDFStarDecodingValueFactory.java @@ -71,6 +71,11 @@ public Literal createLiteral(String label, String language) { return delegate.createLiteral(label, language); } + @Override + public Literal createLiteral(String label, String language, Literal.BaseDirection baseDirection) { + return delegate.createLiteral(label, language, baseDirection); + } + @Override public Literal createLiteral(String label, IRI datatype) { return delegate.createLiteral(label, datatype); diff --git a/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/AbstractParserTest.java b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/AbstractParserTest.java new file mode 100644 index 00000000000..ec21623b260 --- /dev/null +++ b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/AbstractParserTest.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors, Aduna, and others. + * + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.StringReader; +import java.util.Collection; + +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.base.CoreDatatype; +import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; +import org.eclipse.rdf4j.rio.helpers.ParseErrorCollector; +import org.eclipse.rdf4j.rio.helpers.SimpleParseLocationListener; +import org.eclipse.rdf4j.rio.helpers.StatementCollector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/* +* Testing base class for Turtle, TriG, NTriples, and NQuads parsers + */ +public abstract class AbstractParserTest { + protected RDFParser parser; + protected ParseErrorCollector errorCollector = new ParseErrorCollector(); + protected StatementCollector statementCollector = new StatementCollector(); + protected TestParseLocationListener locationListener = new TestParseLocationListener(); + + @BeforeEach + public void setUp() { + parser = createRDFParser(); + parser.setParseErrorListener(errorCollector); + parser.setRDFHandler(statementCollector); + parser.setParseLocationListener(locationListener); + } + + protected void dirLangStringTestHelper( + final String data, final String expectedLang, final String expectedBaseDir, final boolean normalize, + final boolean shouldFail) { + parser.getParserConfig().set(BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES, true); + parser.getParserConfig().set(BasicParserSettings.NORMALIZE_LANGUAGE_TAGS, normalize); + + try { + parser.parse(new StringReader(data)); + + if (shouldFail) { + fail("default config should result in fatal error / parse exception"); + } + + assertThat(errorCollector.getErrors()).isEmpty(); + + final Collection stmts = statementCollector.getStatements(); + + assertThat(stmts).hasSize(1); + + final Statement stmt = stmts.iterator().next(); + + assertEquals(CoreDatatype.RDF.DIRLANGSTRING.getIri(), ((Literal) stmt.getObject()).getDatatype()); + assertTrue(((Literal) stmt.getObject()).getLanguage().isPresent()); + assertEquals(expectedLang, ((Literal) stmt.getObject()).getLanguage().get()); + assertEquals(expectedBaseDir, ((Literal) stmt.getObject()).getBaseDirection().toString()); + } catch (final Exception e) { + if (!shouldFail) { + fail("parse error on correct data: " + e.getMessage()); + } + } + } + + protected void dirLangStringNoLanguageTestHelper(String data) { + try { + parser.parse(new StringReader(data)); + + assertThat(errorCollector.getErrors()).isEmpty(); + + Collection stmts = statementCollector.getStatements(); + + assertThat(stmts).hasSize(1); + + Statement stmt = stmts.iterator().next(); + + assertEquals(CoreDatatype.XSD.STRING.getIri(), ((Literal) stmt.getObject()).getDatatype()); + } catch (Exception e) { + fail("parse error on correct data: " + e.getMessage()); + } + } + + protected abstract RDFParser createRDFParser(); + + protected static class TestParseLocationListener extends SimpleParseLocationListener { + + public void assertListener(int row, int col) { + assertEquals(row, this.getLineNo(), "Unexpected last row"); + assertEquals(col, this.getColumnNo(), "Unexpected last col"); + } + + } + + @Test + public void dummy() { + } + +} diff --git a/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/RDFWriterTest.java b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/RDFWriterTest.java index 3a4125c54ac..b266dfd1c02 100644 --- a/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/RDFWriterTest.java +++ b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/RDFWriterTest.java @@ -31,6 +31,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; +import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; @@ -50,6 +51,7 @@ import org.eclipse.rdf4j.model.Triple; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.DynamicModelFactory; import org.eclipse.rdf4j.model.impl.LinkedHashModel; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.util.Models; @@ -1938,4 +1940,21 @@ private void assertSameModel(Model expected, Model actual) { assertEquals(actual.filter(subj, pred, obj).size(), actual.filter(subj, pred, obj).size()); } } + + // Helper method for testing dirLangString literals in supported formats (Turtle, NTriples, TriG, NQuads) + public void dirLangStringTest(RDFFormat format) throws Exception { + Model model = new DynamicModelFactory().createEmptyModel(); + String ns = "http://example.org/"; + IRI uri1 = vf.createIRI(ns, "uri1"); + IRI uri2 = vf.createIRI(ns, "uri2"); + model.add(vf.createStatement(uri1, uri2, vf.createLiteral("hello", "en", Literal.BaseDirection.LTR))); + model.add(vf.createStatement(uri1, uri2, vf.createLiteral("שלום", "he", Literal.BaseDirection.RTL))); + + StringWriter stringWriter = new StringWriter(); + Rio.write(model, stringWriter, format); + String output = stringWriter.toString(); + + assertThat(output).contains("\"hello\"@en--ltr"); + assertThat(output).contains("\"שלום\"@he--rtl"); + } } diff --git a/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelperTest.java b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelperTest.java index 2213d046d69..83e7868be60 100644 --- a/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelperTest.java +++ b/core/rio/api/src/test/java/org/eclipse/rdf4j/rio/helpers/RDFParserHelperTest.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -116,6 +117,33 @@ public final void testCreateLiteralLabelAndLanguage() { assertEquals(RDF.LANGSTRING, literal.getDatatype()); } + /** + * Test method for + * {@link org.eclipse.rdf4j.rio.helpers.RDFParserHelper#createLiteral(java.lang.String, java.lang.String, org.eclipse.rdf4j.model.URI, org.eclipse.rdf4j.rio.ParserConfig, org.eclipse.rdf4j.rio.ParseErrorListener, org.eclipse.rdf4j.model.ValueFactory)} + * . + */ + @Test + public final void testCreateLiteralLabelAndLanguageAndDirection() { + Literal literalLTR = RDFParserHelper.createLiteral(LABEL_TESTA, LANG_EN + "--ltr", null, parserConfig, + errListener, + valueFactory); + Literal literalRTL = RDFParserHelper.createLiteral(LABEL_TESTA, "ar--rtl", null, parserConfig, errListener, + valueFactory); + + assertEquals(LABEL_TESTA, literalLTR.getLabel()); + assertEquals(LANG_EN, literalLTR.getLanguage().orElse(null)); + assertEquals(Literal.BaseDirection.LTR, literalLTR.getBaseDirection()); + assertEquals(RDF.DIRLANGSTRING, literalLTR.getDatatype()); + + assertEquals(LABEL_TESTA, literalRTL.getLabel()); + assertEquals("ar", literalRTL.getLanguage().orElse(null)); + assertEquals(Literal.BaseDirection.RTL, literalRTL.getBaseDirection()); + assertEquals(RDF.DIRLANGSTRING, literalRTL.getDatatype()); + + assertThrows(RDFParseException.class, () -> RDFParserHelper.createLiteral(LABEL_TESTA, "he--jsldkfjds", null, + parserConfig, errListener, valueFactory)); + } + /** * Test method for * {@link org.eclipse.rdf4j.rio.helpers.RDFParserHelper#createLiteral(java.lang.String, java.lang.String, org.eclipse.rdf4j.model.URI, org.eclipse.rdf4j.rio.ParserConfig, org.eclipse.rdf4j.rio.ParseErrorListener, org.eclipse.rdf4j.model.ValueFactory)} @@ -171,6 +199,15 @@ public final void testCreateLiteralLabelNoLanguageWithRDFLangStringWithVerify() .isInstanceOf(RDFParseException.class); } + @Test + public final void testCreateLiteralLabelNoLanguageWithRDFDirLangStringWithVerify() { + parserConfig.set(BasicParserSettings.VERIFY_DATATYPE_VALUES, true); + assertTrue(parserConfig.get(BasicParserSettings.VERIFY_DATATYPE_VALUES)); + assertThatThrownBy(() -> RDFParserHelper.createLiteral(LABEL_TESTA, null, RDF.DIRLANGSTRING, parserConfig, + errListener, valueFactory)) + .isInstanceOf(RDFParseException.class); + } + @Test public final void testCreateLiteralLabelNoLanguageWithRDFLangStringWithNoVerify() { parserConfig.set(BasicParserSettings.VERIFY_DATATYPE_VALUES, false); @@ -180,6 +217,15 @@ public final void testCreateLiteralLabelNoLanguageWithRDFLangStringWithNoVerify( assertEquals(XSD.STRING, literal.getDatatype()); } + @Test + public final void testCreateLiteralLabelNoLanguageWithRDFDirLangStringWithNoVerify() { + parserConfig.set(BasicParserSettings.VERIFY_DATATYPE_VALUES, false); + Literal literal = RDFParserHelper.createLiteral(LABEL_TESTA, null, RDF.DIRLANGSTRING, parserConfig, errListener, + valueFactory); + assertFalse(literal.getLanguage().isPresent()); + assertEquals(XSD.STRING, literal.getDatatype()); + } + @Test public final void testReportErrorStringFatalActive() { parserConfig.set(BasicParserSettings.VERIFY_DATATYPE_VALUES, true); diff --git a/core/rio/datatypes/src/main/java/org/eclipse/rdf4j/rio/datatypes/RDFDatatypeHandler.java b/core/rio/datatypes/src/main/java/org/eclipse/rdf4j/rio/datatypes/RDFDatatypeHandler.java index 3868981bfce..d4b70cd2da6 100644 --- a/core/rio/datatypes/src/main/java/org/eclipse/rdf4j/rio/datatypes/RDFDatatypeHandler.java +++ b/core/rio/datatypes/src/main/java/org/eclipse/rdf4j/rio/datatypes/RDFDatatypeHandler.java @@ -37,8 +37,10 @@ public boolean isRecognizedDatatype(IRI datatypeUri) { } return org.eclipse.rdf4j.model.vocabulary.RDF.LANGSTRING.equals(datatypeUri) + || org.eclipse.rdf4j.model.vocabulary.RDF.DIRLANGSTRING.equals(datatypeUri) || org.eclipse.rdf4j.model.vocabulary.RDF.XMLLITERAL.equals(datatypeUri) - || org.eclipse.rdf4j.model.vocabulary.RDF.HTML.equals(datatypeUri); + || org.eclipse.rdf4j.model.vocabulary.RDF.HTML.equals(datatypeUri) + || org.eclipse.rdf4j.model.vocabulary.RDF.JSON.equals(datatypeUri); } @Override diff --git a/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/BCP47LanguageHandler.java b/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/BCP47LanguageHandler.java index e5e91163349..453268bd25a 100644 --- a/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/BCP47LanguageHandler.java +++ b/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/BCP47LanguageHandler.java @@ -65,11 +65,19 @@ public boolean verifyLanguage(String literalValue, String languageTag) throws Li @Override public Literal normalizeLanguage(String literalValue, String languageTag, ValueFactory valueFactory) throws LiteralUtilException { + return normalizeLanguage(literalValue, languageTag, Literal.BaseDirection.NONE, valueFactory); + } + + @Override + public Literal normalizeLanguage(String literalValue, String languageTag, Literal.BaseDirection baseDir, + ValueFactory valueFactory) + throws LiteralUtilException { Objects.requireNonNull(languageTag, "Language tag cannot be null"); Objects.requireNonNull(literalValue, "Literal value cannot be null"); try { - return valueFactory.createLiteral(literalValue, Literals.normalizeLanguageTag(languageTag)); + return valueFactory.createLiteral(literalValue, + Literals.normalizeLanguageTag(languageTag), baseDir); } catch (IllformedLocaleException e) { throw new LiteralUtilException("Could not normalize BCP47 language tag", e); } diff --git a/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/RFC3066LanguageHandler.java b/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/RFC3066LanguageHandler.java index 37ae95506ce..1d89fb1cd33 100644 --- a/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/RFC3066LanguageHandler.java +++ b/core/rio/languages/src/main/java/org/eclipse/rdf4j/rio/languages/RFC3066LanguageHandler.java @@ -67,11 +67,18 @@ public boolean verifyLanguage(String literalValue, String languageTag) throws Li @Override public Literal normalizeLanguage(String literalValue, String languageTag, ValueFactory valueFactory) throws LiteralUtilException { + return normalizeLanguage(literalValue, languageTag, Literal.BaseDirection.NONE, valueFactory); + } + + @Override + public Literal normalizeLanguage(String literalValue, String languageTag, Literal.BaseDirection baseDir, + ValueFactory valueFactory) + throws LiteralUtilException { Objects.requireNonNull(languageTag, "Language tag cannot be null"); Objects.requireNonNull(literalValue, "Literal value cannot be null"); if (isRecognizedLanguage(languageTag)) { - return valueFactory.createLiteral(literalValue, languageTag.toLowerCase().intern()); + return valueFactory.createLiteral(literalValue, languageTag.toLowerCase().intern(), baseDir); } throw new LiteralUtilException("Could not normalize RFC3066 language tag"); diff --git a/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsParserUnitTest.java b/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsParserUnitTest.java index 87b9d1c91ae..dae45494c80 100644 --- a/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsParserUnitTest.java +++ b/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsParserUnitTest.java @@ -30,23 +30,20 @@ import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.rio.AbstractParserTest; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.helpers.AbstractRDFHandler; import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; import org.eclipse.rdf4j.rio.helpers.NTriplesParserSettings; -import org.eclipse.rdf4j.rio.helpers.SimpleParseLocationListener; -import org.eclipse.rdf4j.rio.helpers.StatementCollector; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * JUnit test for the N-Quads parser that uses the tests that are available * online. */ -public abstract class AbstractNQuadsParserUnitTest { +public abstract class AbstractNQuadsParserUnitTest extends AbstractParserTest { /*-----------* * Constants * @@ -60,33 +57,16 @@ public abstract class AbstractNQuadsParserUnitTest { private static final String NTRIPLES_TEST_FILE = "/testcases/ntriples/test.nt"; - private RDFParser parser; - - private TestRDFHandler rdfHandler; - - @BeforeEach - public void setUp() { - parser = createRDFParser(); - rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(this.rdfHandler); - } - - @AfterEach - public void tearDown() { - parser = null; - } - /*---------* * Methods * *---------*/ public void testNQuadsFile() throws Exception { - RDFParser nquadsParser = createRDFParser(); - nquadsParser.setRDFHandler(new AbstractRDFHandler() { + parser.setRDFHandler(new AbstractRDFHandler() { }); try (InputStream in = AbstractNQuadsParserUnitTest.class.getResourceAsStream(NQUADS_TEST_FILE)) { - nquadsParser.parse(in, NQUADS_TEST_URL); + parser.parse(in, NQUADS_TEST_URL); } catch (RDFParseException e) { fail("NQuadsParser failed to parse N-Quads test document: " + e.getMessage()); } @@ -96,12 +76,11 @@ public void testNQuadsFile() throws Exception { * The N-Quads parser must be able to parse the N-Triples test file without error. */ public void testNTriplesFile() throws Exception { - RDFParser nquadsParser = createRDFParser(); - nquadsParser.setRDFHandler(new AbstractRDFHandler() { + parser.setRDFHandler(new AbstractRDFHandler() { }); try (InputStream in = AbstractNQuadsParserUnitTest.class.getResourceAsStream(NTRIPLES_TEST_FILE)) { - nquadsParser.parse(in, NTRIPLES_TEST_URL); + parser.parse(in, NTRIPLES_TEST_URL); } catch (RDFParseException e) { fail("NQuadsParser failed to parse N-Triples test document: " + e.getMessage()); } @@ -186,10 +165,8 @@ public void testParseNoContext() throws RDFHandlerException, IOException, RDFPar public void testParseEmptyLinesAndComments() throws RDFHandlerException, IOException, RDFParseException { final ByteArrayInputStream bais = new ByteArrayInputStream( " \n\n\n# This is a comment\n\n#this is another comment.".getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - assertEquals(rdfHandler.getStatements().size(), 0); + assertEquals(0, statementCollector.getStatements().size()); } /** @@ -200,11 +177,9 @@ public void testParseBasic() throws RDFHandlerException, IOException, RDFParseEx final ByteArrayInputStream bais = new ByteArrayInputStream( " ." .getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - assertEquals(1, rdfHandler.getStatements().size()); - final Statement statement = rdfHandler.getStatements().iterator().next(); + assertEquals(1, statementCollector.getStatements().size()); + final Statement statement = statementCollector.getStatements().iterator().next(); assertEquals("http://www.v/dat/4b", statement.getSubject().stringValue()); assertEquals("http://www.w3.org/20/ica#dtend", statement.getPredicate().stringValue()); assertTrue(statement.getObject() instanceof IRI); @@ -220,11 +195,9 @@ public void testParseBasicBNode() throws RDFHandlerException, IOException, RDFPa final ByteArrayInputStream bais = new ByteArrayInputStream( "_:a123456768 ." .getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - assertThat(rdfHandler.getStatements()).hasSize(1); - final Statement statement = rdfHandler.getStatements().iterator().next(); + assertThat(statementCollector.getStatements()).hasSize(1); + final Statement statement = statementCollector.getStatements().iterator().next(); assertTrue(statement.getSubject() instanceof BNode); assertEquals("http://www.w3.org/20/ica#dtend", statement.getPredicate().stringValue()); assertTrue(statement.getObject() instanceof IRI); @@ -240,11 +213,9 @@ public void testParseBasicLiteral() throws RDFHandlerException, IOException, RDF final ByteArrayInputStream bais = new ByteArrayInputStream( "_:a123456768 \"2010-05-02\" ." .getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - assertThat(rdfHandler.getStatements()).hasSize(1); - final Statement statement = rdfHandler.getStatements().iterator().next(); + assertThat(statementCollector.getStatements()).hasSize(1); + final Statement statement = statementCollector.getStatements().iterator().next(); assertTrue(statement.getSubject() instanceof BNode); assertEquals("http://www.w3.org/20/ica#dtend", statement.getPredicate().stringValue()); assertTrue(statement.getObject() instanceof Literal); @@ -260,10 +231,8 @@ public void testParseBasicLiteralLang() throws RDFHandlerException, IOException, final ByteArrayInputStream bais = new ByteArrayInputStream( " \"2010-05-02\"@en ." .getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - final Statement statement = rdfHandler.getStatements().iterator().next(); + final Statement statement = statementCollector.getStatements().iterator().next(); assertEquals("http://www.v/dat/4b2-21", statement.getSubject().stringValue()); assertEquals("http://www.w3.org/20/ica#dtend", statement.getPredicate().stringValue()); assertTrue(statement.getObject() instanceof Literal); @@ -283,10 +252,8 @@ public void testParseBasicLiteralDatatype() throws RDFHandlerException, IOExcept (" " + " " + "\"2010\"^^ " + ".") .getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.parse(bais, "http://test.base.uri"); - final Statement statement = rdfHandler.getStatements().iterator().next(); + final Statement statement = statementCollector.getStatements().iterator().next(); assertEquals("http://www.v/dat/4b2-21", statement.getSubject().stringValue()); assertEquals("http://www.w3.org/20/ica#dtend", statement.getPredicate().stringValue()); assertTrue(statement.getObject() instanceof Literal); @@ -307,8 +274,6 @@ public void testParseBasicLiteralDatatypePrefix() throws RDFHandlerException, IO final ByteArrayInputStream bais = new ByteArrayInputStream( (" " + " " + "\"2010\"^^xsd:integer " + ".").getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); try { parser.parse(bais, "http://test.base.uri"); fail("Expected exception when passing in a datatype using an N3 style prefix"); @@ -324,19 +289,14 @@ public void testParseBasicLiteralDatatypePrefix() throws RDFHandlerException, IO */ @Test public void testLiteralEscapeManagement1() throws RDFHandlerException, IOException, RDFParseException { - TestParseLocationListener parseLocationListener = new TestParseLocationListener(); - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setParseLocationListener(parseLocationListener); - parser.setRDFHandler(rdfHandler); - final ByteArrayInputStream bais = new ByteArrayInputStream( " \"\\\\\" .".getBytes()); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(1); - // parseLocationListener.assertListener(1, 40); + assertEquals(1, statementCollector.getStatements().size()); + // locationListener.assertListener(1, 40); // FIXME: Enable column numbers when parser supports them - parseLocationListener.assertListener(1, 1); + locationListener.assertListener(1, 1); } /** @@ -344,17 +304,12 @@ public void testLiteralEscapeManagement1() throws RDFHandlerException, IOExcepti */ @Test public void testLiteralEscapeManagement2() throws RDFHandlerException, IOException, RDFParseException { - TestParseLocationListener parseLocationListener = new TestParseLocationListener(); - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setParseLocationListener(parseLocationListener); - parser.setRDFHandler(rdfHandler); - final ByteArrayInputStream bais = new ByteArrayInputStream( " \"Line text 1\\nLine text 2\" .".getBytes()); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(1); - final Value object = rdfHandler.getStatements().iterator().next().getObject(); + assertEquals(1, statementCollector.getStatements().size()); + final Value object = statementCollector.getStatements().iterator().next().getObject(); assertTrue(object instanceof Literal); final String literalContent = ((Literal) object).getLabel(); assertEquals("Line text 1\nLine text 2", literalContent); @@ -365,18 +320,13 @@ public void testLiteralEscapeManagement2() throws RDFHandlerException, IOExcepti */ @Test public void testURIDecodingManagement() throws RDFHandlerException, IOException, RDFParseException { - TestParseLocationListener parseLocationListener = new TestParseLocationListener(); - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setParseLocationListener(parseLocationListener); - parser.setRDFHandler(rdfHandler); - final ByteArrayInputStream bais = new ByteArrayInputStream( " ." .getBytes()); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(1); - final Statement statement = rdfHandler.getStatements().iterator().next(); + assertEquals(1, statementCollector.getStatements().size()); + final Statement statement = statementCollector.getStatements().iterator().next(); final Resource subject = statement.getSubject(); assertTrue(subject instanceof IRI); @@ -401,16 +351,14 @@ public void testURIDecodingManagement() throws RDFHandlerException, IOException, @Test public void testUnicodeLiteralDecoding() throws RDFHandlerException, IOException, RDFParseException { - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); final String INPUT_LITERAL_PLAIN = "[は]"; final String INPUT_LITERAL_ENCODED = "[\\u306F]"; final String INPUT_STRING = String.format(" \"%s\" .", INPUT_LITERAL_ENCODED); final ByteArrayInputStream bais = new ByteArrayInputStream(INPUT_STRING.getBytes()); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(1); - final Literal obj = (Literal) rdfHandler.getStatements().iterator().next().getObject(); + assertEquals(1, statementCollector.getStatements().size()); + final Literal obj = (Literal) statementCollector.getStatements().iterator().next().getObject(); assertEquals(INPUT_LITERAL_PLAIN, obj.getLabel()); } @@ -452,17 +400,12 @@ public void testEndOfStreamReached() throws RDFHandlerException, IOException, RD */ @Test public void testParserWithAllCases() throws IOException, RDFParseException, RDFHandlerException { - TestParseLocationListener parseLocationListerner = new TestParseLocationListener(); - // SpecificTestRDFHandler rdfHandler = new SpecificTestRDFHandler(); - parser.setParseLocationListener(parseLocationListerner); - parser.setRDFHandler(rdfHandler); - BufferedReader br = new BufferedReader(new InputStreamReader( AbstractNQuadsParserUnitTest.class.getResourceAsStream("/testcases/nquads/test1.nq"))); parser.parse(br, "http://test.base.uri"); - rdfHandler.assertHandler(6); - parseLocationListerner.assertListener(8, 1); + assertEquals(6, statementCollector.getStatements().size()); + locationListener.assertListener(8, 1); } /** @@ -470,16 +413,11 @@ public void testParserWithAllCases() throws IOException, RDFParseException, RDFH */ @Test public void testParserWithRealData() throws IOException, RDFParseException, RDFHandlerException { - TestParseLocationListener parseLocationListener = new TestParseLocationListener(); - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setParseLocationListener(parseLocationListener); - parser.setRDFHandler(rdfHandler); - parser.parse(AbstractNQuadsParserUnitTest.class.getResourceAsStream("/testcases/nquads/test2.nq"), "http://test.base.uri"); - rdfHandler.assertHandler(400); - parseLocationListener.assertListener(400, 1); + assertEquals(400, statementCollector.getStatements().size()); + locationListener.assertListener(400, 1); } @Test @@ -566,15 +504,13 @@ public void testStopAtFirstErrorTolerantParsing() throws RDFHandlerException, IO // with // error. " .\n").getBytes()); - final TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.getParserConfig().set(NTriplesParserSettings.FAIL_ON_INVALID_LINES, false); parser.getParserConfig().addNonFatalError(NTriplesParserSettings.FAIL_ON_INVALID_LINES); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(2); - final Collection statements = rdfHandler.getStatements(); + assertEquals(2, statementCollector.getStatements().size()); + final Collection statements = statementCollector.getStatements(); int i = 0; for (Statement nextStatement : statements) { assertEquals("http://s" + i, nextStatement.getSubject().stringValue()); @@ -587,8 +523,6 @@ public void testStopAtFirstErrorTolerantParsing() throws RDFHandlerException, IO private void verifyStatementWithInvalidDatatype(boolean useDatatypeVerification) throws RDFHandlerException, IOException, RDFParseException { - TestRDFHandler rdfHandler = new TestRDFHandler(); - parser.setRDFHandler(rdfHandler); parser.getParserConfig().set(BasicParserSettings.VERIFY_DATATYPE_VALUES, useDatatypeVerification); parser.getParserConfig().set(BasicParserSettings.FAIL_ON_UNKNOWN_DATATYPES, useDatatypeVerification); if (!useDatatypeVerification) { @@ -603,41 +537,70 @@ private void verifyStatementWithInvalidDatatype(boolean useDatatypeVerification) + " .") .getBytes()); parser.parse(bais, "http://base-uri"); - rdfHandler.assertHandler(1); + assertEquals(1, statementCollector.getStatements().size()); + } + + @Test + public void testDirLangStringRTLNoContext() { + final String data = " \"שלום\"@he--rtl"; + dirLangStringTest(data, false, "he", Literal.RTL_SUFFIX, false, false); } - private class TestParseLocationListener extends SimpleParseLocationListener { + @Test + public void testDirLangStringRTLWithContext() { + final String data = " \"שלום\"@he--rtl"; + dirLangStringTest(data, true, "he", Literal.RTL_SUFFIX, false, false); + } - private void assertListener(int row, int col) { - assertEquals(row, this.getLineNo(), "Unexpected last row"); - assertEquals(col, this.getColumnNo(), "Unexpected last col"); - } + @Test + public void testDirLangStringLTRWithNormalizationNoContext() { + String data = " \"Hello\"@en--ltr"; + dirLangStringTest(data, false, "en", Literal.LTR_SUFFIX, true, false); + } + @Test + public void testDirLangStringLTRWithNormalizationWithContext() { + final String data = " \"Hello\"@en--ltr"; + dirLangStringTest(data, true, "en", Literal.LTR_SUFFIX, true, false); } - private class TestRDFHandler extends StatementCollector { + @Test + public void testBadDirLangStringNoContext() { + final String data = " \"hello\"@en--unk"; + dirLangStringTest(data, false, "", "", true, true); + } - private boolean started = false; + @Test + public void testBadDirLangStringWithContext() { + final String data = " \"hello\"@en--unk"; + dirLangStringTest(data, true, "", "", true, true); + } - private boolean ended = false; + @Test + public void testBadCapitalizationDirLangStringNoContext() { + final String data = " \"Hello\"@en--LTR"; + dirLangStringTest(data, false, "", "", true, true); + } - @Override - public void startRDF() throws RDFHandlerException { - super.startRDF(); - started = true; - } + @Test + public void testBadCapitalizationDirLangStringWithContext() { + final String data = " \"Hello\"@en--LTR"; + dirLangStringTest(data, true, "", "", true, true); + } - @Override - public void endRDF() throws RDFHandlerException { - super.endRDF(); - ended = true; - } + @Test + public void testDirLangStringNoLanguage() throws IOException { + final String data = " \"Hello\"^^ ."; + dirLangStringNoLanguageTestHelper(data); + } - public void assertHandler(int expected) { - assertTrue(started, "Never started."); - assertTrue(ended, "Never ended."); - assertEquals(expected, getStatements().size(), "Unexpected number of statements."); - } + private void dirLangStringTest( + final String triple, final boolean withContext, final String expectedLang, final String expectedDir, + final boolean normalize, + final boolean shouldCauseException) { + final String data = triple + (withContext ? " " : "") + " ."; + + dirLangStringTestHelper(data, expectedLang, expectedDir, normalize, shouldCauseException); } @Test diff --git a/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsWriterTest.java b/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsWriterTest.java index 3a086558dd3..30354e3a005 100644 --- a/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsWriterTest.java +++ b/core/rio/nquads/src/test/java/org/eclipse/rdf4j/rio/nquads/AbstractNQuadsWriterTest.java @@ -19,6 +19,7 @@ import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; @@ -155,6 +156,11 @@ public void testBlankNodeContextAddXSDString() throws RDFHandlerException { " \"test literal\"^^ _:")); } + @Test + public void testDirLangString() throws Exception { + dirLangStringTest(RDFFormat.NQUADS); + } + @Override protected RioSetting[] getExpectedSupportedSettings() { return new RioSetting[] { diff --git a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesParserUnitTest.java b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesParserUnitTest.java index 6787dedc80a..0b929775c9b 100644 --- a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesParserUnitTest.java +++ b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesParserUnitTest.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.util.ArrayList; @@ -24,17 +25,19 @@ import java.util.List; import java.util.TreeSet; +import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.impl.LinkedHashModel; import org.eclipse.rdf4j.model.util.Models; import org.eclipse.rdf4j.model.vocabulary.XSD; +import org.eclipse.rdf4j.rio.AbstractParserTest; import org.eclipse.rdf4j.rio.RDFHandlerException; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; import org.eclipse.rdf4j.rio.helpers.NTriplesParserSettings; -import org.eclipse.rdf4j.rio.helpers.ParseErrorCollector; import org.eclipse.rdf4j.rio.helpers.StatementCollector; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -42,20 +45,26 @@ * * @author Peter Ansell */ -public abstract class AbstractNTriplesParserUnitTest { +public abstract class AbstractNTriplesParserUnitTest extends AbstractParserTest { private static final String NTRIPLES_TEST_URL = "http://www.w3.org/2000/10/rdf-tests/rdfcore/ntriples/test.nt"; private static final String NTRIPLES_TEST_FILE = "/testcases/ntriples/test.nt"; + private Model model; + + @BeforeEach + @Override + public void setUp() { + model = new LinkedHashModel(); + statementCollector = new StatementCollector(model); + super.setUp(); + } + @Test public void testNTriplesFile() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - try (InputStream in = this.getClass().getResourceAsStream(NTRIPLES_TEST_FILE)) { - ntriplesParser.parse(in, NTRIPLES_TEST_URL); + parser.parse(in, NTRIPLES_TEST_URL); } catch (RDFParseException e) { fail("Failed to parse N-Triples test document: " + e.getMessage()); } @@ -70,15 +79,11 @@ public void testNTriplesFile() throws Exception { public void testExceptionHandlingWithDefaultSettings() throws Exception { String data = "invalid nt"; - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - try { - ntriplesParser.parse(new StringReader(data), NTRIPLES_TEST_URL); + parser.parse(new StringReader(data), NTRIPLES_TEST_URL); fail("expected RDFParseException due to invalid data"); } catch (RDFParseException expected) { - assertEquals(expected.getLineNumber(), 1); + assertEquals(1, expected.getLineNumber()); } } @@ -86,17 +91,13 @@ public void testExceptionHandlingWithDefaultSettings() throws Exception { public void testExceptionHandlingWithStopAtFirstError() throws Exception { String data = "invalid nt"; - RDFParser ntriplesParser = createRDFParser(); - ntriplesParser.getParserConfig().set(NTriplesParserSettings.FAIL_ON_INVALID_LINES, Boolean.TRUE); - - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); + parser.getParserConfig().set(NTriplesParserSettings.FAIL_ON_INVALID_LINES, Boolean.TRUE); try { - ntriplesParser.parse(new StringReader(data), NTRIPLES_TEST_URL); + parser.parse(new StringReader(data), NTRIPLES_TEST_URL); fail("expected RDFParseException due to invalid data"); } catch (RDFParseException expected) { - assertEquals(expected.getLineNumber(), 1); + assertEquals(1, expected.getLineNumber()); } } @@ -104,13 +105,9 @@ public void testExceptionHandlingWithStopAtFirstError() throws Exception { public void testExceptionHandlingWithoutStopAtFirstError() throws Exception { String data = "invalid nt"; - RDFParser ntriplesParser = createRDFParser(); - ntriplesParser.getParserConfig().addNonFatalError(NTriplesParserSettings.FAIL_ON_INVALID_LINES); - - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); + parser.getParserConfig().addNonFatalError(NTriplesParserSettings.FAIL_ON_INVALID_LINES); - ntriplesParser.parse(new StringReader(data), NTRIPLES_TEST_URL); + parser.parse(new StringReader(data), NTRIPLES_TEST_URL); assertEquals(0, model.size()); assertEquals(0, model.subjects().size()); @@ -122,13 +119,9 @@ public void testExceptionHandlingWithoutStopAtFirstError() throws Exception { public void testExceptionHandlingWithoutStopAtFirstError2() throws Exception { String data = "invalid nt"; - RDFParser ntriplesParser = createRDFParser(); - ntriplesParser.getParserConfig().set(NTriplesParserSettings.FAIL_ON_INVALID_LINES, false); + parser.getParserConfig().set(NTriplesParserSettings.FAIL_ON_INVALID_LINES, false); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - - ntriplesParser.parse(new StringReader(data), NTRIPLES_TEST_URL); + parser.parse(new StringReader(data), NTRIPLES_TEST_URL); assertEquals(0, model.size()); assertEquals(0, model.subjects().size()); @@ -138,10 +131,7 @@ public void testExceptionHandlingWithoutStopAtFirstError2() throws Exception { @Test public void testEscapes() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse( + parser.parse( new StringReader(" \" \\t \\b \\n \\r \\f \\\" \\' \\\\ \" . "), "http://example/"); assertEquals(1, model.size()); @@ -150,10 +140,7 @@ public void testEscapes() throws Exception { @Test public void testEndOfLineCommentNoSpace() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse( + parser.parse( new StringReader(" .#endoflinecomment\n"), "http://example/"); assertEquals(1, model.size()); @@ -162,10 +149,7 @@ public void testEndOfLineCommentNoSpace() throws Exception { @Test public void testEndOfLineCommentWithSpaceBefore() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse( + parser.parse( new StringReader(" . #endoflinecomment\n"), "http://example/"); assertEquals(1, model.size()); @@ -174,10 +158,7 @@ public void testEndOfLineCommentWithSpaceBefore() throws Exception { @Test public void testEndOfLineCommentWithSpaceAfter() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse( + parser.parse( new StringReader(" .# endoflinecomment\n"), "http://example/"); assertEquals(1, model.size()); @@ -186,10 +167,7 @@ public void testEndOfLineCommentWithSpaceAfter() throws Exception { @Test public void testEndOfLineCommentWithSpaceBoth() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse( + parser.parse( new StringReader(" . # endoflinecomment\n"), "http://example/"); assertEquals(1, model.size()); @@ -198,10 +176,7 @@ public void testEndOfLineCommentWithSpaceBoth() throws Exception { @Test public void testEndOfLineCommentsNoSpace() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader( + parser.parse(new StringReader( " .#endoflinecomment\n . # endoflinecomment\n"), "http://example/"); assertEquals(2, model.size()); @@ -209,10 +184,7 @@ public void testEndOfLineCommentsNoSpace() throws Exception { @Test public void testEndOfLineCommentsWithSpaceBefore() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader( + parser.parse(new StringReader( " . #endoflinecomment\n . # endoflinecomment\n"), "http://example/"); assertEquals(2, model.size()); @@ -220,10 +192,7 @@ public void testEndOfLineCommentsWithSpaceBefore() throws Exception { @Test public void testEndOfLineCommentsWithSpaceAfter() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader( + parser.parse(new StringReader( " .# endoflinecomment\n . # endoflinecomment\n"), "http://example/"); assertEquals(2, model.size()); @@ -231,10 +200,7 @@ public void testEndOfLineCommentsWithSpaceAfter() throws Exception { @Test public void testEndOfLineCommentsWithSpaceBoth() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader( + parser.parse(new StringReader( " . # endoflinecomment\n . # endoflinecomment\n"), "http://example/"); assertEquals(2, model.size()); @@ -242,10 +208,7 @@ public void testEndOfLineCommentsWithSpaceBoth() throws Exception { @Test public void testEndOfLineEmptyCommentNoSpace() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader(" .#\n"), + parser.parse(new StringReader(" .#\n"), "http://example/"); assertEquals(1, model.size()); assertEquals(Collections.singleton("urn:test:object"), Models.objectStrings(model)); @@ -253,10 +216,7 @@ public void testEndOfLineEmptyCommentNoSpace() throws Exception { @Test public void testEndOfLineEmptyCommentWithSpaceBefore() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader(" . #\n"), + parser.parse(new StringReader(" . #\n"), "http://example/"); assertEquals(1, model.size()); assertEquals(Collections.singleton("urn:test:object"), Models.objectStrings(model)); @@ -264,10 +224,7 @@ public void testEndOfLineEmptyCommentWithSpaceBefore() throws Exception { @Test public void testEndOfLineEmptyCommentWithSpaceAfter() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader(" .# \n"), + parser.parse(new StringReader(" .# \n"), "http://example/"); assertEquals(1, model.size()); assertEquals(Collections.singleton("urn:test:object"), Models.objectStrings(model)); @@ -275,10 +232,7 @@ public void testEndOfLineEmptyCommentWithSpaceAfter() throws Exception { @Test public void testEndOfLineEmptyCommentWithSpaceBoth() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader(" . # \n"), + parser.parse(new StringReader(" . # \n"), "http://example/"); assertEquals(1, model.size()); assertEquals(Collections.singleton("urn:test:object"), Models.objectStrings(model)); @@ -286,10 +240,7 @@ public void testEndOfLineEmptyCommentWithSpaceBoth() throws Exception { @Test public void testBlankNodeIdentifiersRDF11() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader("_:123 _:456 ."), "http://example/"); + parser.parse(new StringReader("_:123 _:456 ."), "http://example/"); assertEquals(1, model.size()); } @@ -300,14 +251,10 @@ public void testSupportedSettings() { @Test public void testUriWithSpaceShouldFailToParse() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - String nt = " ."; try { - ntriplesParser.parse(new StringReader(nt), NTRIPLES_TEST_URL); + parser.parse(new StringReader(nt), NTRIPLES_TEST_URL); fail("Should have failed to parse invalid N-Triples uri with space"); } catch (RDFParseException ignored) { } @@ -320,14 +267,10 @@ public void testUriWithSpaceShouldFailToParse() throws Exception { @Test public void testUriWithEscapeCharactersShouldFailToParse() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - String nt = " ."; try { - ntriplesParser.parse(new StringReader(nt), NTRIPLES_TEST_URL); + parser.parse(new StringReader(nt), NTRIPLES_TEST_URL); fail("Should have failed to parse invalid N-Triples uri with space"); } catch (RDFParseException ignored) { } @@ -341,10 +284,7 @@ public void testUriWithEscapeCharactersShouldFailToParse() throws Exception { @Test public void testBlankNodeIdentifiersWithUnderScore() throws Exception { // The characters _ and [0-9] may appear anywhere in a blank node label. - RDFParser ntriplesParser = new NTriplesParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader("_:123_ _:_456 ."), NTRIPLES_TEST_URL); + parser.parse(new StringReader("_:123_ _:_456 ."), NTRIPLES_TEST_URL); assertEquals(1, model.size()); assertEquals(1, model.subjects().size()); @@ -355,10 +295,7 @@ public void testBlankNodeIdentifiersWithUnderScore() throws Exception { @Test public void testBlankNodeIdentifiersWithDot() throws Exception { // The character . may appear anywhere except the first or last character. - RDFParser ntriplesParser = new NTriplesParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); - ntriplesParser.parse(new StringReader("_:1.23 _:45.6 ."), NTRIPLES_TEST_URL); + parser.parse(new StringReader("_:1.23 _:45.6 ."), NTRIPLES_TEST_URL); assertEquals(1, model.size()); assertEquals(1, model.subjects().size()); @@ -369,11 +306,8 @@ public void testBlankNodeIdentifiersWithDot() throws Exception { @Test public void testBlankNodeIdentifiersWithDotAsFirstCahracter() { // The character . may appear anywhere except the first or last character. - RDFParser ntriplesParser = new NTriplesParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); try { - ntriplesParser.parse(new StringReader("_:123 _:.456 ."), NTRIPLES_TEST_URL); + parser.parse(new StringReader("_:123 _:.456 ."), NTRIPLES_TEST_URL); fail("Should have failed to parse invalid N-Triples bnode with '.' at the begining of the bnode label"); } catch (Exception e) { } @@ -387,11 +321,8 @@ public void testBlankNodeIdentifiersWithDotAsFirstCahracter() { @Test public void testBlankNodeIdentifiersWithDotAsLastCahracter() { // The character . may appear anywhere except the first or last character. - RDFParser ntriplesParser = new NTriplesParser(); - Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); assertThrows(RDFParseException.class, - () -> ntriplesParser.parse(new StringReader("_:123 _:456. ."), NTRIPLES_TEST_URL)); + () -> parser.parse(new StringReader("_:123 _:456. ."), NTRIPLES_TEST_URL)); assertEquals(0, model.size()); assertEquals(0, model.subjects().size()); assertEquals(0, model.predicates().size()); @@ -412,13 +343,12 @@ public void testBlankNodeIdentifiersWithOtherCharacters() { for (int i = 0; i < charactersList.size(); i++) { Character character = charactersList.get(i); - RDFParser ntriplesParser = new NTriplesParser(); Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); + parser.setRDFHandler(new StatementCollector(model)); String triple = " _:1" + character + " . "; try { - ntriplesParser.parse(new StringReader(triple), NTRIPLES_TEST_URL); + parser.parse(new StringReader(triple), NTRIPLES_TEST_URL); } catch (Exception e) { fail(" Failed to parse triple : " + triple + " containing character '" + character + "' at index " + i + " in charactersList"); @@ -448,12 +378,11 @@ public void testBlankNodeIdentifiersWithOtherCharactersAsFirstCharacter() { charactersList.add('\u203F'); for (Character character : charactersList) { - RDFParser ntriplesParser = new NTriplesParser(); Model model = new LinkedHashModel(); - ntriplesParser.setRDFHandler(new StatementCollector(model)); + parser.setRDFHandler(new StatementCollector(model)); assertThrows(RDFParseException.class, () -> { - ntriplesParser.parse( + parser.parse( new StringReader(" _:" + character + "1 . "), NTRIPLES_TEST_URL); }); @@ -479,12 +408,11 @@ public void handleComment(String comment) throws RDFHandlerException { @Test public void testHandleComment() throws Exception { - RDFParser ntriplesParser = createRDFParser(); Model model = new LinkedHashModel(); String commentStr = "some comment in it's own line"; CommentCollector cc = new CommentCollector(model); - ntriplesParser.setRDFHandler(cc); - ntriplesParser.parse( + parser.setRDFHandler(cc); + parser.parse( new StringReader(" .\n#" + commentStr + "\n ."), "http://example/"); assertEquals(2, model.size()); @@ -494,16 +422,13 @@ public void testHandleComment() throws Exception { @Test public void testLinenumberDatatypeValidation() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - ntriplesParser.getParserConfig().addNonFatalError(BasicParserSettings.VERIFY_DATATYPE_VALUES); - ntriplesParser.getParserConfig().set(BasicParserSettings.VERIFY_DATATYPE_VALUES, true); - ParseErrorCollector collector = new ParseErrorCollector(); - ntriplesParser.setParseErrorListener(collector); + parser.getParserConfig().addNonFatalError(BasicParserSettings.VERIFY_DATATYPE_VALUES); + parser.getParserConfig().set(BasicParserSettings.VERIFY_DATATYPE_VALUES, true); - ntriplesParser.parse( + parser.parse( new StringReader(" \"invalid\"^^<" + XSD.DATETIME.stringValue() + "> ."), NTRIPLES_TEST_URL); - List errors = collector.getErrors(); + List errors = errorCollector.getErrors(); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("(1, 32)"), "Unknown line number"); @@ -511,21 +436,60 @@ public void testLinenumberDatatypeValidation() throws Exception { @Test public void testLinenumberLanguagetagValidation() throws Exception { - RDFParser ntriplesParser = createRDFParser(); - ntriplesParser.getParserConfig().addNonFatalError(BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES); - ntriplesParser.getParserConfig().set(BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES, true); - ntriplesParser.getParserConfig().set(BasicParserSettings.VERIFY_LANGUAGE_TAGS, true); - ParseErrorCollector collector = new ParseErrorCollector(); - ntriplesParser.setParseErrorListener(collector); - - ntriplesParser.parse( + parser.getParserConfig().addNonFatalError(BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES); + parser.getParserConfig().set(BasicParserSettings.FAIL_ON_UNKNOWN_LANGUAGES, true); + parser.getParserConfig().set(BasicParserSettings.VERIFY_LANGUAGE_TAGS, true); + + parser.parse( new StringReader(" \"hello\"@inv+alid ."), NTRIPLES_TEST_URL); - List errors = collector.getErrors(); + List errors = errorCollector.getErrors(); assertEquals(1, errors.size()); assertTrue(errors.get(0).contains("(1, 32)"), "Unknown line number"); } + @Test + public void testDirLangStringLTR() { + final String data = " \"Hello\"@en--ltr ."; + dirLangStringTestHelper(data, "en", Literal.LTR_SUFFIX, false, false); + } + + @Test + public void testDirLangStringRTL() { + final String data = " \"שלום\"@he--rtl ."; + dirLangStringTestHelper(data, "he", Literal.RTL_SUFFIX, false, false); + } + + @Test + public void testDirLangStringLTRWithNormalization() { + final String data = " \"Hello\"@en--ltr ."; + dirLangStringTestHelper(data, "en", Literal.LTR_SUFFIX, true, false); + } + + @Test + public void testDirLangStringRTLWithNormalization() { + final String data = " \"שלום\"@HE--rtl ."; + dirLangStringTestHelper(data, "he", Literal.RTL_SUFFIX, true, false); + } + + @Test + public void testBadDirLangString() { + final String data = " \"hello\"@en--unk ."; + dirLangStringTestHelper(data, "", "", true, true); + } + + @Test + public void testBadCapitalizationDirLangString() { + final String data = " \"Hello\"@en--LTR ."; + dirLangStringTestHelper(data, "", "", true, true); + } + + @Test + public void testDirLangStringNoLanguage() throws IOException { + final String data = " \"Hello\"^^ ."; + dirLangStringNoLanguageTestHelper(data); + } + protected abstract RDFParser createRDFParser(); } diff --git a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesWriterTest.java b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesWriterTest.java index 705727b203a..7cca3430323 100644 --- a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesWriterTest.java +++ b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/AbstractNTriplesWriterTest.java @@ -10,12 +10,14 @@ *******************************************************************************/ package org.eclipse.rdf4j.rio.ntriples; +import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFParserFactory; import org.eclipse.rdf4j.rio.RDFWriterFactory; import org.eclipse.rdf4j.rio.RDFWriterTest; import org.eclipse.rdf4j.rio.RioSetting; import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings; import org.eclipse.rdf4j.rio.helpers.NTriplesWriterSettings; +import org.junit.jupiter.api.Test; /** * @author Jeen Broekstra @@ -34,4 +36,8 @@ protected RioSetting[] getExpectedSupportedSettings() { }; } + @Test + public void testDirLangString() throws Exception { + dirLangStringTest(RDFFormat.NTRIPLES); + } } diff --git a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/NTriplesWriterTest.java b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/NTriplesWriterTest.java index 4b6c195b81d..9284845efff 100644 --- a/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/NTriplesWriterTest.java +++ b/core/rio/ntriples/src/test/java/org/eclipse/rdf4j/rio/ntriples/NTriplesWriterTest.java @@ -10,6 +10,17 @@ *******************************************************************************/ package org.eclipse.rdf4j.rio.ntriples; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.StringWriter; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.impl.DynamicModelFactory; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.junit.jupiter.api.Test; + /** * JUnit test for the RDF/JSON parser. * diff --git a/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/AbstractTriGWriterTest.java b/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/AbstractTriGWriterTest.java index 820f9304fda..f7b2df2fde5 100644 --- a/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/AbstractTriGWriterTest.java +++ b/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/AbstractTriGWriterTest.java @@ -10,12 +10,14 @@ *******************************************************************************/ package org.eclipse.rdf4j.rio.trig; +import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFParserFactory; import org.eclipse.rdf4j.rio.RDFWriterFactory; import org.eclipse.rdf4j.rio.RDFWriterTest; import org.eclipse.rdf4j.rio.RioSetting; import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings; import org.eclipse.rdf4j.rio.helpers.TurtleWriterSettings; +import org.junit.jupiter.api.Test; /** * @author Jeen Broesktra @@ -36,4 +38,9 @@ protected RioSetting[] getExpectedSupportedSettings() { TurtleWriterSettings.ABBREVIATE_NUMBERS }; } + + @Test + public void testDirLangString() throws Exception { + dirLangStringTest(RDFFormat.TRIG); + } } diff --git a/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/TriGParserCustomTest.java b/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/TriGParserCustomTest.java index 7f60657a274..db30bd384fd 100644 --- a/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/TriGParserCustomTest.java +++ b/core/rio/trig/src/test/java/org/eclipse/rdf4j/rio/trig/TriGParserCustomTest.java @@ -16,22 +16,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.IOException; import java.io.StringReader; import java.util.concurrent.TimeUnit; import org.eclipse.rdf4j.model.BNode; +import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.LinkedHashModel; -import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.util.Models; -import org.eclipse.rdf4j.rio.ParserConfig; +import org.eclipse.rdf4j.rio.AbstractParserTest; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.Rio; -import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; -import org.eclipse.rdf4j.rio.helpers.ParseErrorCollector; import org.eclipse.rdf4j.rio.helpers.StatementCollector; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,34 +41,21 @@ * @author Peter Ansell */ @Timeout(value = 10, unit = TimeUnit.MINUTES) -public class TriGParserCustomTest { - private ValueFactory vf; +public class TriGParserCustomTest extends AbstractParserTest { - private ParserConfig settingsNoVerifyLangTag; + private Model model; - private ParseErrorCollector errors; - - private RDFParser parser; - - private StatementCollector statementCollector; - - /** - */ @BeforeEach public void setUp() { - vf = SimpleValueFactory.getInstance(); - settingsNoVerifyLangTag = new ParserConfig(); - settingsNoVerifyLangTag.set(BasicParserSettings.VERIFY_LANGUAGE_TAGS, false); - errors = new ParseErrorCollector(); - parser = Rio.createParser(RDFFormat.TRIG); - statementCollector = new StatementCollector(new LinkedHashModel()); - parser.setRDFHandler(statementCollector); + model = new LinkedHashModel(); + statementCollector = new StatementCollector(model); + super.setUp(); } @Test public void testSPARQLGraphKeyword() throws Exception { - Model model = Rio.parse(new StringReader("GRAPH { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = "GRAPH { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -82,8 +67,8 @@ public void testSPARQLGraphKeyword() throws Exception { @Test public void testGraph() throws Exception { - Model model = Rio.parse(new StringReader(" { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = " { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -95,9 +80,8 @@ public void testGraph() throws Exception { @Test public void testGraphLocalNameGraph() throws Exception { - Model model = Rio.parse( - new StringReader("@prefix graph: .\n graph:a { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = "@prefix graph: .\n graph:a { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -109,9 +93,8 @@ public void testGraphLocalNameGraph() throws Exception { @Test public void testGraphLocalNameIntegerGraph() throws Exception { - Model model = Rio.parse( - new StringReader("@prefix graph: .\n graph:1 { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = "@prefix graph: .\n graph:1 { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -123,9 +106,8 @@ public void testGraphLocalNameIntegerGraph() throws Exception { @Test public void testGraphLocalNameNotGraph() throws Exception { - Model model = Rio.parse( - new StringReader("@prefix ex: .\n ex:a { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = "@prefix ex: .\n ex:a { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -137,9 +119,8 @@ public void testGraphLocalNameNotGraph() throws Exception { @Test public void testGraphLocalNameIntegerNotGraph() throws Exception { - Model model = Rio.parse( - new StringReader("@prefix ex: .\n ex:1 { [] \"Foo\" }"), "", - RDFFormat.TRIG); + String data = "@prefix ex: .\n ex:1 { [] \"Foo\" }"; + parser.parse(new StringReader(data)); assertEquals(1, model.size()); assertNotNull(model.contexts().iterator().next()); @@ -151,40 +132,38 @@ public void testGraphLocalNameIntegerNotGraph() throws Exception { @Test public void testTrailingSemicolon() throws Exception { - Rio.parse(new StringReader("{ ;}"), "", RDFFormat.TRIG); + parser.parse(new StringReader("{ ;}"), ""); } @Test public void testAnonymousGraph1() throws Exception { - Rio.parse(new StringReader("PREFIX : \n GRAPH [] { :s :p :o }"), "", RDFFormat.TRIG); + parser.parse(new StringReader("PREFIX : \n GRAPH [] { :s :p :o }"), ""); } @Test public void testAnonymousGraph2() throws Exception { - Rio.parse(new StringReader("PREFIX : \n [] { :s :p :o }"), "", RDFFormat.TRIG); + parser.parse(new StringReader("PREFIX : \n [] { :s :p :o }"), ""); } @Test public void testTurtle() throws Exception { - Rio.parse(new StringReader(" "), "", RDFFormat.TRIG); + parser.parse(new StringReader(" "), ""); } @Test public void testMinimalWhitespace() throws Exception { - Rio.parse(this.getClass().getResourceAsStream("/testcases/trig/trig-syntax-minimal-whitespace-01.trig"), "", - RDFFormat.TRIG); + parser.parse(this.getClass().getResourceAsStream("/testcases/trig/trig-syntax-minimal-whitespace-01.trig"), ""); } @Test public void testMinimalWhitespaceLine12() throws Exception { - Rio.parse(new StringReader("@prefix : . {_:s:p :o ._:s:p\"Alice\". _:s:p _:o .}"), "", - RDFFormat.TRIG); + parser.parse(new StringReader("@prefix : . {_:s:p :o ._:s:p\"Alice\". _:s:p _:o .}"), ""); } @Test public void testBadPname02() throws Exception { try { - Rio.parse(new StringReader("@prefix : . {:a%2 :p :o .}"), "", RDFFormat.TRIG); + parser.parse(new StringReader("@prefix : . {:a%2 :p :o .}"), ""); fail("Did not receive expected exception"); } catch (RDFParseException e) { @@ -198,35 +177,103 @@ public void testSupportedSettings() { @Test public void testParseTruePrefix() throws Exception { - Rio.parse(new StringReader("@prefix true: . {true:s true:p true:o .}"), "", RDFFormat.TRIG); + parser.parse(new StringReader("@prefix true: . {true:s true:p true:o .}"), ""); } @Test public void testParseTrig_booleanLiteral() throws Exception { String trig = "{\n" + " true.\n" + "}"; - Model m = Rio.parse(new StringReader(trig), "http://ex/", RDFFormat.TRIG); - assertEquals(1, m.size()); + parser.parse(new StringReader(trig), "http://ex/"); + assertEquals(1, model.size()); } @Test public void testParseTrig_booleanLiteral_space() throws Exception { String trig = "{\n" + " true .\n" + "}"; - Model m = Rio.parse(new StringReader(trig), "http://ex/", RDFFormat.TRIG); - assertEquals(1, m.size()); + parser.parse(new StringReader(trig), "http://ex/"); + assertEquals(1, model.size()); } @Test public void testParseTrig_intLiteral() throws Exception { String trig = "{\n" + " 1.\n" + "}"; - Model m = Rio.parse(new StringReader(trig), "http://ex/", RDFFormat.TRIG); - assertEquals(1, Models.objectLiteral(m).get().intValue()); + parser.parse(new StringReader(trig), "http://ex/"); + assertEquals(1, Models.objectLiteral(model).get().intValue()); } @Test public void testParseTrig_doubleLiteral() throws Exception { String trig = "{\n" + " 1.2.\n" + "}"; - Model m = Rio.parse(new StringReader(trig), "http://ex/", RDFFormat.TRIG); - assertEquals(1.2d, Models.objectLiteral(m).get().doubleValue(), 0.01); + parser.parse(new StringReader(trig), "http://ex/"); + assertEquals(1.2d, Models.objectLiteral(model).get().doubleValue(), 0.01); + } + + @Test + public void testDirLangStringRTLNoContext() { + String data = " \"שלום\"@he--rtl"; + dirLangStringTest(data, false, "he", Literal.RTL_SUFFIX, false, false); + } + + @Test + public void testDirLangStringRTLWithContext() { + String data = " \"שלום\"@he--rtl"; + dirLangStringTest(data, true, "he", Literal.RTL_SUFFIX, false, false); + } + + @Test + public void testDirLangStringLTRWithNormalizationNoContext() { + String data = " \"Hello\"@en--ltr"; + dirLangStringTest(data, false, "en", Literal.LTR_SUFFIX, true, false); + } + + @Test + public void testDirLangStringLTRWithNormalizationWithContext() { + String data = " \"Hello\"@en--ltr"; + dirLangStringTest(data, true, "en", Literal.LTR_SUFFIX, true, false); + } + + @Test + public void testBadDirLangStringNoContext() { + String data = " \"hello\"@en--unk"; + dirLangStringTest(data, false, "", "", true, true); + } + + @Test + public void testBadDirLangStringWithContext() { + String data = " \"hello\"@en--unk"; + dirLangStringTest(data, true, "", "", true, true); + } + + @Test + public void testBadCapitalizationDirLangStringNoContext() { + String data = " \"Hello\"@en--LTR"; + dirLangStringTest(data, false, "", "", true, true); + } + + @Test + public void testBadCapitalizationDirLangStringWithContext() { + final String data = " \"Hello\"@en--LTR"; + dirLangStringTest(data, true, "", "", true, true); + } + + @Test + public void testDirLangStringNoLanguage() throws IOException { + final String data = " \"Hello\"^^ ."; + dirLangStringNoLanguageTestHelper(data); } + private void dirLangStringTest( + final String triple, final boolean withContext, final String expectedLang, final String expectedBaseDir, + final boolean normalize, + final boolean shouldCauseException) { + final String data = (withContext ? " { " : "") + triple + " ." + + (withContext ? " }" : ""); + + dirLangStringTestHelper(data, expectedLang, expectedBaseDir, normalize, shouldCauseException); + } + + @Override + public RDFParser createRDFParser() { + return new TriGParser(); + } } diff --git a/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleWriter.java b/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleWriter.java index 75460b08952..e4c052190a6 100644 --- a/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleWriter.java +++ b/core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleWriter.java @@ -682,6 +682,7 @@ protected void writeLiteral(Literal lit) throws IOException { // Append the literal's language writer.write("@"); writer.write(lit.getLanguage().get()); + writer.write(lit.getBaseDirection().toString()); } else if (!xsdStringToPlainLiteral || !XSD.STRING.equals(datatype)) { // Append the literal's datatype (possibly written as an abbreviated // URI) diff --git a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/AbstractTurtleWriterTest.java b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/AbstractTurtleWriterTest.java index 1a4977f12f8..20f6b87d54c 100644 --- a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/AbstractTurtleWriterTest.java +++ b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/AbstractTurtleWriterTest.java @@ -15,12 +15,10 @@ import org.eclipse.rdf4j.model.util.Values; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.XSD; -import org.eclipse.rdf4j.rio.RDFParserFactory; -import org.eclipse.rdf4j.rio.RDFWriterFactory; -import org.eclipse.rdf4j.rio.RDFWriterTest; -import org.eclipse.rdf4j.rio.RioSetting; +import org.eclipse.rdf4j.rio.*; import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings; import org.eclipse.rdf4j.rio.helpers.TurtleWriterSettings; +import org.junit.jupiter.api.Test; /** * @author Jeen Broekstra @@ -49,4 +47,9 @@ protected Model getAbbrevTestModel() { m.add(Values.iri("http://www.example.com/decimal"), RDF.VALUE, Values.literal("55.66", XSD.DECIMAL)); return m; } + + @Test + public void dirLangStringTest() throws Exception { + dirLangStringTest(RDFFormat.TURTLE); + } } 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..69042bf988b 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 @@ -25,6 +25,7 @@ import java.net.URL; import java.util.Collection; import java.util.Iterator; +import java.util.List; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; @@ -32,46 +33,36 @@ import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Triple; import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.base.CoreDatatype; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.DC; import org.eclipse.rdf4j.model.vocabulary.DCTERMS; import org.eclipse.rdf4j.model.vocabulary.FOAF; import org.eclipse.rdf4j.model.vocabulary.XSD; +import org.eclipse.rdf4j.rio.AbstractParserTest; +import org.eclipse.rdf4j.rio.LanguageHandler; import org.eclipse.rdf4j.rio.RDFParseException; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; -import org.eclipse.rdf4j.rio.helpers.ParseErrorCollector; -import org.eclipse.rdf4j.rio.helpers.SimpleParseLocationListener; import org.eclipse.rdf4j.rio.helpers.StatementCollector; import org.eclipse.rdf4j.rio.helpers.TurtleParserSettings; -import org.junit.jupiter.api.BeforeEach; +import org.eclipse.rdf4j.rio.languages.RFC3066LanguageHandler; import org.junit.jupiter.api.Test; /** * @author jeen */ -public class TurtleParserTest { - - private TurtleParser parser; +public class TurtleParserTest extends AbstractParserTest { private final ValueFactory vf = SimpleValueFactory.getInstance(); - private final ParseErrorCollector errorCollector = new ParseErrorCollector(); - - private final StatementCollector statementCollector = new StatementCollector(); - private final String prefixes = "@prefix ex: . \n@prefix : . \n"; private final String baseURI = "http://example.org/"; - private final SimpleParseLocationListener locationListener = new SimpleParseLocationListener(); - - @BeforeEach - public void setUp() { - parser = new TurtleParser(); - parser.setParseErrorListener(errorCollector); - parser.setRDFHandler(statementCollector); - parser.setParseLocationListener(locationListener); + @Override + protected RDFParser createRDFParser() { + return new TurtleParser(); } @Test @@ -275,99 +266,80 @@ public void testLineNumberReporting() throws IOException { final String error = errorCollector.getFatalErrors().get(0); // expected to fail at line 9. assertTrue(error.contains("(9,")); - assertEquals(9, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(9, -1); } } @Test public void testLineNumberReportingNoErrorsSingleLine() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader(" ."); parser.parse(in, baseURI); - assertEquals(1, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(1, -1); } @Test public void testLineNumberReportingNoErrorsSingleLineEndNewline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader(" .\n"); parser.parse(in, baseURI); - assertEquals(2, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(2, -1); } @Test public void testLineNumberReportingNoErrorsMultipleLinesNoEndNewline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader(" .\n ."); parser.parse(in, baseURI); - assertEquals(2, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(2, -1); } @Test public void testLineNumberReportingNoErrorsMultipleLinesEndNewline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader(" .\n .\n"); parser.parse(in, baseURI); - assertEquals(3, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(3, -1); } @Test public void testLineNumberReportingOnlySingleCommentNoEndline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader("# This is just a comment"); parser.parse(in, baseURI); - assertEquals(1, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(1, -1); } @Test public void testLineNumberReportingOnlySingleCommentEndline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader("# This is just a comment\n"); parser.parse(in, baseURI); - assertEquals(2, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(2, -1); } @Test public void testLineNumberReportingOnlySingleCommentCarriageReturn() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader("# This is just a comment\r"); parser.parse(in, baseURI); - assertEquals(2, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(2, -1); } @Test public void testLineNumberReportingOnlySingleCommentCarriageReturnNewline() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader("# This is just a comment\r\n"); parser.parse(in, baseURI); - assertEquals(2, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(2, -1); } @Test public void testLineNumberReportingInLongStringLiterals() throws IOException { - assertEquals(0, locationListener.getLineNo()); - assertEquals(0, locationListener.getColumnNo()); + locationListener.assertListener(0, 0); Reader in = new StringReader(" \"\"\"is\nallowed\nin\na very long string\"\"\" ."); parser.parse(in, baseURI); - assertEquals(4, locationListener.getLineNo()); - assertEquals(-1, locationListener.getColumnNo()); + locationListener.assertListener(4, -1); } @Test @@ -580,4 +552,117 @@ public void testParseRDFStar_TurtleStarDisabled() throws IOException { } } + @Test + public void testParseAdditionalDatatypes() throws IOException { + String data = prefixes + ":s :p \"o\"^^rdf:JSON . \n" + + ":s :p \"o\"^^rdf:HTML . \n" + + ":s :p \"o\"^^rdf:XMLLiteral . "; + Reader r = new StringReader(data); + + try { + parser.getParserConfig().set(BasicParserSettings.FAIL_ON_UNKNOWN_DATATYPES, true); + + parser.parse(r, baseURI); + + assertThat(errorCollector.getErrors()).isEmpty(); + + Collection stmts = statementCollector.getStatements(); + + assertThat(stmts).hasSize(3); + + Iterator iter = stmts.iterator(); + + Statement stmt1 = iter.next(), stmt2 = iter.next(), stmt3 = iter.next(); + + assertEquals(CoreDatatype.RDF.JSON.getIri(), ((Literal) stmt1.getObject()).getDatatype()); + assertEquals(CoreDatatype.RDF.HTML.getIri(), ((Literal) stmt2.getObject()).getDatatype()); + assertEquals(CoreDatatype.RDF.XMLLITERAL.getIri(), ((Literal) stmt3.getObject()).getDatatype()); + } catch (RDFParseException e) { + fail("parse error on correct data: " + e.getMessage()); + } + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-1.ttl + */ + @Test + public void testLanguageDirectionLTR() throws IOException { + String data = " \"Hello\"@en--ltr ."; + dirLangStringTestHelper(data, "en", Literal.LTR_SUFFIX, false, false); + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-1.ttl + */ + @Test + public void testLanguageDirectionLTRWithNormalization() throws IOException { + String data = " \"Hello\"@EN--ltr ."; + dirLangStringTestHelper(data, "en", Literal.LTR_SUFFIX, true, false); + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-2.ttl + */ + @Test + public void testLanguageDirectionRTL() throws IOException { + String data = " \"Hello\"@en--rtl ."; + dirLangStringTestHelper(data, "en", Literal.RTL_SUFFIX, false, false); + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-2.ttl + */ + @Test + public void testLanguageDirectionRTLWithNormalization() throws IOException { + String data = " \"Hello\"@EN--rtl ."; + dirLangStringTestHelper(data, "en", Literal.RTL_SUFFIX, true, false); + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-bad-1.ttl + */ + @Test + public void testBadLanguageDirection() throws IOException { + String data = " \"Hello\"@en--unk ."; + dirLangStringTestHelper(data, "", "", false, true); + } + + /** + * https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/nt-ttl12-langdir-bad-2.ttl + */ + @Test + public void testBadCapitalizationLanguageDirection() throws IOException { + String data = " \"Hello\"@en--LTR ."; + dirLangStringTestHelper(data, "", "", false, true); + } + + @Test + public void testDirLangStringNoLanguage() throws IOException { + String data = " \"Hello\"^^rdf:dirLangString ."; + dirLangStringNoLanguageTestHelper(data); + } + + @Test + public void testRFC3066LanguageHandler() throws IOException { + String data = " \"Hello\"@en--ltr ."; + + try { + List customHandlers = List.of(new RFC3066LanguageHandler()); + parser.getParserConfig().set(BasicParserSettings.LANGUAGE_HANDLERS, customHandlers); + parser.parse(new StringReader(data), baseURI); + + assertThat(errorCollector.getErrors()).isEmpty(); + + Collection stmts = statementCollector.getStatements(); + + assertThat(stmts).hasSize(1); + + Iterator iter = stmts.iterator(); + Statement stmt1 = iter.next(); + + assertEquals(CoreDatatype.RDF.DIRLANGSTRING.getIri(), ((Literal) stmt1.getObject()).getDatatype()); + } catch (RDFParseException e) { + fail("parse error on correct data: " + e.getMessage()); + } + } } diff --git a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleWriterTest.java b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleWriterTest.java index c6069c87c5c..212d7893d8c 100644 --- a/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleWriterTest.java +++ b/core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleWriterTest.java @@ -18,6 +18,7 @@ import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.base.CoreDatatype; import org.eclipse.rdf4j.model.impl.DynamicModelFactory; import org.eclipse.rdf4j.model.util.Models; import org.eclipse.rdf4j.model.vocabulary.RDFS; @@ -754,4 +755,24 @@ public void testIgnoreAbbreviateNumbers() { assertTrue(result.contains("\"-2\"^^")); assertTrue(result.contains("\"55.66\"^^")); } + + @Test + public void testAdditionalDatatypes() { + Model model = new DynamicModelFactory().createEmptyModel(); + String ns = "http://www.example.com/"; + model.add(vf.createIRI(ns, "subject"), vf.createIRI(ns, "predicate"), + vf.createLiteral("object1", CoreDatatype.RDF.JSON)); + model.add(vf.createIRI(ns, "subject"), vf.createIRI(ns, "predicate"), + vf.createLiteral("object2", CoreDatatype.RDF.HTML)); + model.add(vf.createIRI(ns, "subject"), vf.createIRI(ns, "predicate"), + vf.createLiteral("object3", CoreDatatype.RDF.XMLLITERAL)); + + StringWriter stringWriter = new StringWriter(); + Rio.write(model, stringWriter, RDFFormat.TURTLE); + + assertThat(stringWriter.toString()).contains("\"object1\"^^"); + assertThat(stringWriter.toString()).contains("\"object2\"^^"); + assertThat(stringWriter.toString()) + .contains("\"object3\"^^"); + } } diff --git a/core/sail/elasticsearch-store/src/main/java/org/eclipse/rdf4j/sail/elasticsearchstore/ElasticsearchDataStructure.java b/core/sail/elasticsearch-store/src/main/java/org/eclipse/rdf4j/sail/elasticsearchstore/ElasticsearchDataStructure.java index f4dff97822e..638dce115e6 100644 --- a/core/sail/elasticsearch-store/src/main/java/org/eclipse/rdf4j/sail/elasticsearchstore/ElasticsearchDataStructure.java +++ b/core/sail/elasticsearch-store/src/main/java/org/eclipse/rdf4j/sail/elasticsearchstore/ElasticsearchDataStructure.java @@ -243,6 +243,9 @@ private QueryBuilder getQueryBuilder(Resource subject, IRI predicate, Value obje if (((Literal) object).getLanguage().isPresent()) { boolQueryBuilder .must(QueryBuilders.termQuery("object_Lang", ((Literal) object).getLanguage().get())); + boolQueryBuilder + .must(QueryBuilders.termQuery("object_LangDir", + ((Literal) object).getBaseDirection().toString())); } } } @@ -450,7 +453,7 @@ private Map statementToJsonMap(ExtensibleStatement statement) { ((Literal) statement.getObject()).getDatatype().stringValue()); if (((Literal) statement.getObject()).getLanguage().isPresent()) { jsonMap.put("object_Lang", ((Literal) statement.getObject()).getLanguage().get()); - + jsonMap.put("object_LangDir", ((Literal) statement.getObject()).getBaseDirection().toString()); } } return jsonMap; @@ -673,8 +676,8 @@ private static ExtensibleStatement sourceToStatement(Map sourceA objectRes = vf.createBNode(objectString); } else { if (sourceAsMap.containsKey("object_Lang")) { - objectRes = vf.createLiteral(objectString, (String) sourceAsMap.get("object_Lang")); - + objectRes = vf.createLiteral(objectString, (String) sourceAsMap.get("object_Lang"), + Literal.BaseDirection.fromString((String) sourceAsMap.get("object_LangDir"))); } else { objectRes = vf.createLiteral(objectString, vf.createIRI((String) sourceAsMap.get("object_Datatype"))); diff --git a/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/ValueStore.java b/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/ValueStore.java index 342ab9d97e3..0c078d6e6bf 100644 --- a/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/ValueStore.java +++ b/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/ValueStore.java @@ -1531,6 +1531,11 @@ public LmdbLiteral createLiteral(String value, String language) { return new LmdbLiteral(revision, value, language); } + @Override + public LmdbLiteral createLiteral(String value, String language, Literal.BaseDirection baseDirection) { + return new LmdbLiteral(revision, value, language, baseDirection); + } + /*----------------------------------------------------------------------* * Methods for converting model objects to LmdbStore-specific objects * *----------------------------------------------------------------------*/ diff --git a/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/model/LmdbLiteral.java b/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/model/LmdbLiteral.java index d6efea7435d..c2e78aee517 100644 --- a/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/model/LmdbLiteral.java +++ b/core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/model/LmdbLiteral.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.lmdb.model; import java.io.ObjectStreamException; +import java.util.Objects; import java.util.Optional; import org.eclipse.rdf4j.model.IRI; @@ -40,6 +41,8 @@ public class LmdbLiteral extends AbstractLiteral implements LmdbValue { */ private String language; + private BaseDirection baseDirection; + /** * The literal's datatype. */ @@ -79,10 +82,25 @@ public LmdbLiteral(ValueStoreRevision revision, String label, String lang) { } public LmdbLiteral(ValueStoreRevision revision, String label, String lang, long internalID) { + this(revision, label, lang, BaseDirection.NONE, internalID); + } + + public LmdbLiteral(ValueStoreRevision revision, String label, String lang, BaseDirection baseDirection) { + this(revision, label, lang, baseDirection, UNKNOWN_ID); + } + + public LmdbLiteral(ValueStoreRevision revision, String label, String language, BaseDirection baseDirection, + long internalID) { + Objects.requireNonNull(language, "null language"); + Objects.requireNonNull(baseDirection, "null baseDirection"); this.label = label; - this.language = lang; - coreDatatype = CoreDatatype.RDF.LANGSTRING; - datatype = CoreDatatype.RDF.LANGSTRING.getIri(); + this.language = language; + this.baseDirection = baseDirection; + if (baseDirection != BaseDirection.NONE) { + setDatatype(CoreDatatype.RDF.DIRLANGSTRING); + } else { + setDatatype(CoreDatatype.RDF.LANGSTRING); + } setInternalID(internalID, revision); this.initialized = true; } @@ -188,6 +206,11 @@ public Optional getLanguage() { return Optional.ofNullable(language); } + @Override + public BaseDirection getBaseDirection() { + return baseDirection; + } + public void setLanguage(String language) { this.language = language; } diff --git a/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemLiteral.java b/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemLiteral.java index c45353d8a35..8db789f8fb5 100644 --- a/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemLiteral.java +++ b/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemLiteral.java @@ -64,6 +64,19 @@ public MemLiteral(Object creator, String label, String lang) { this.creator = creator; } + /** + * Creates a new Literal which will get the supplied label, language code, and base direction. + * + * @param creator The object that is creating this MemLiteral. + * @param label The label for this literal. + * @param lang The language code of the supplied label. + * @param baseDirection The base direction as a string ("", "--ltr", "--rtl"). + */ + public MemLiteral(Object creator, String label, String lang, BaseDirection baseDirection) { + super(label, lang, baseDirection); + this.creator = creator; + } + /** * Creates a new Literal which will get the supplied label and datatype. * diff --git a/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactory.java b/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactory.java index c638737b43d..ec42be604b8 100644 --- a/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactory.java +++ b/core/sail/memory/src/main/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactory.java @@ -302,7 +302,7 @@ public MemLiteral getOrCreateMemLiteral(Literal literal) { IRI datatype = coreDatatype != CoreDatatype.NONE ? coreDatatype.getIri() : literal.getDatatype(); if (Literals.isLanguageLiteral(literal)) { - return new MemLiteral(this, label, literal.getLanguage().get()); + return new MemLiteral(this, label, literal.getLanguage().get(), literal.getBaseDirection()); } else { try { if (coreDatatype.isXSDDatatype()) { @@ -406,6 +406,11 @@ public Literal createLiteral(String value, String language) { return getOrCreateMemLiteral(super.createLiteral(value, language)); } + @Override + public Literal createLiteral(String value, String language, Literal.BaseDirection baseDirection) { + return getOrCreateMemLiteral(super.createLiteral(value, language, baseDirection)); + } + @Override public Literal createLiteral(String value, IRI datatype) { return getOrCreateMemLiteral(super.createLiteral(value, datatype)); diff --git a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactoryTest.java b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactoryTest.java index 087af609931..f62d86e44c1 100644 --- a/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactoryTest.java +++ b/core/sail/memory/src/test/java/org/eclipse/rdf4j/sail/memory/model/MemValueFactoryTest.java @@ -13,6 +13,8 @@ import static org.assertj.core.api.Assertions.assertThat; import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.base.CoreDatatype; import org.junit.jupiter.api.Test; public class MemValueFactoryTest { @@ -28,4 +30,14 @@ public void testCreateIRI_namespace_localName_whitespace() { assertThat(factory.createIRI(namespace, localName)).isInstanceOf(IRI.class); } + @Test + public void testCreateDirLangLiteral() { + final Literal literal = new MemValueFactory().createLiteral("label", "he", Literal.BaseDirection.RTL); + + assertThat(literal).isNotNull(); + assertThat(literal.getLabel()).isEqualTo("label"); + assertThat(literal.getLanguage()).contains("he"); + assertThat(literal.getBaseDirection().toString()).isEqualTo("--rtl"); + assertThat(literal.getDatatype()).isEqualTo(CoreDatatype.RDF.DIRLANGSTRING.getIri()); + } } diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/ValueStore.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/ValueStore.java index 37787ac610c..f5cfc25f837 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/ValueStore.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/ValueStore.java @@ -742,6 +742,11 @@ public NativeLiteral createLiteral(String value, String language) { return new NativeLiteral(revision, value, language); } + @Override + public NativeLiteral createLiteral(String value, String language, Literal.BaseDirection baseDirection) { + return new NativeLiteral(revision, value, language, baseDirection); + } + @Override public NativeLiteral createLiteral(String value, IRI datatype) { return new NativeLiteral(revision, value, datatype); diff --git a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/model/NativeLiteral.java b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/model/NativeLiteral.java index 876aebe001f..0389e755cfd 100644 --- a/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/model/NativeLiteral.java +++ b/core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/model/NativeLiteral.java @@ -58,6 +58,16 @@ public NativeLiteral(ValueStoreRevision revision, String label, String lang, int setInternalID(internalID, revision); } + public NativeLiteral(ValueStoreRevision revision, String label, String lang, BaseDirection baseDirection) { + this(revision, label, lang, baseDirection, UNKNOWN_ID); + } + + public NativeLiteral(ValueStoreRevision revision, String label, String lang, BaseDirection baseDirection, + int internalID) { + super(label, lang, baseDirection); + setInternalID(internalID, revision); + } + public NativeLiteral(ValueStoreRevision revision, String label, IRI datatype) { this(revision, label, datatype, UNKNOWN_ID); } diff --git a/testsuites/sail/src/main/java/org/eclipse/rdf4j/testsuite/sail/RDFStoreTest.java b/testsuites/sail/src/main/java/org/eclipse/rdf4j/testsuite/sail/RDFStoreTest.java index 47549df7572..4043a7724dd 100644 --- a/testsuites/sail/src/main/java/org/eclipse/rdf4j/testsuite/sail/RDFStoreTest.java +++ b/testsuites/sail/src/main/java/org/eclipse/rdf4j/testsuite/sail/RDFStoreTest.java @@ -323,6 +323,24 @@ public void testLongLangRoundTrip() { testValueRoundTrip(subj, pred, obj); } + @Test + public void testDirLangStringRoundTrip() { + IRI subj = vf.createIRI(EXAMPLE_NS + PICASSO); + IRI pred = vf.createIRI(EXAMPLE_NS + PAINTS); + Literal obj = vf.createLiteral("guernica", "es", Literal.BaseDirection.LTR); + + testValueRoundTrip(subj, pred, obj); + } + + @Test + public void testJSONLiteralRoundTrip() { + IRI subj = vf.createIRI(EXAMPLE_NS + PICASSO); + IRI pred = vf.createIRI(EXAMPLE_NS + PAINTS); + Literal obj = vf.createLiteral("{ \"name\" : \"guernica\", \"dateCreated\": 1937 }", RDF.JSON); + + testValueRoundTrip(subj, pred, obj); + } + private void testValueRoundTrip(Resource subj, IRI pred, Value obj) { con.begin(); con.addStatement(subj, pred, obj);