From 512680a1710dc76f99ddb0bd9a33f6e2d646b5aa Mon Sep 17 00:00:00 2001 From: abdounabdessamad Date: Fri, 30 May 2025 14:28:26 +0200 Subject: [PATCH 1/4] Serializer Turtle / JSON-LD / RDFXML --- build.gradle.kts | 7 +- .../serialization/Rdf4jSerializationUtil.java | 205 ++++++++++++++++++ src/main/java/module-info.java | 4 + .../Rdf4jSerializationUtilTest.java | 201 +++++++++++++++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java create mode 100644 src/test/java/fr/inria/corese/core/next/api/model/serialization/Rdf4jSerializationUtilTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 6a08e7797..93bb202d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -149,7 +149,12 @@ extraJavaModuleInfo { automaticModule("com.github.jsonld-java:jsonld-java", "jsonld.java") // Module for JSON-LD Java automaticModule("commons-lang:commons-lang", "commons.lang") // Module for Commons Lang automaticModule("fr.inria.lille.shexjava:shexjava-core", "shexjava.core") // Module for ShexJava core - automaticModule("org.eclipse.rdf4j:rdf4j-model", "rdf4j.model") // Module for RDF4J model + // Nouveaux modules RDF4J + automaticModule("org.eclipse.rdf4j:rdf4j-rio-api", "rdf4j.rio.api") + automaticModule("org.eclipse.rdf4j:rdf4j-rio-turtle", "org.eclipse.rdf4j.rio.turtle") + automaticModule("org.eclipse.rdf4j:rdf4j-rio-jsonld", "org.eclipse.rdf4j.rio.jsonld") + automaticModule("org.eclipse.rdf4j:rdf4j-rio-rdfxml", "org.eclipse.rdf4j.rio.rdfxml") + automaticModule("org.eclipse.rdf4j:rdf4j-model", "rdf4j.model.api") } diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java new file mode 100644 index 000000000..552b09b2a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java @@ -0,0 +1,205 @@ +package fr.inria.corese.core.next.api.base.model.serialization; + +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.eclipse.rdf4j.rio.WriterConfig; +import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings; + +import java.io.OutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Utility class for serializing RDF4J {@link Model} objects to various RDF formats. + * This class provides static methods to write RDF data to an {@link OutputStream} + * in formats such as Turtle, JSON-LD, RDF/XML, and N-Triples, leveraging the RDF4J Rio API. + * It handles common serialization configurations like pretty-printing and + * provides dynamic format selection based on string identifiers. + */ +public class Rdf4jSerializationUtil { + + /** + * A unmodifiable map to quickly look up {@link RDFFormat} instances + * based on common string identifiers (case-insensitive). + */ + private static final Map FORMAT_MAP = initFormatMap(); + + /** + * The default {@link WriterConfig} used for serialization, + * configured for pretty-printing. + */ + private static final WriterConfig DEFAULT_CONFIG = createDefaultConfig(); + + /** + * Initializes and returns an unmodifiable map of supported RDF format strings to their + * corresponding {@link RDFFormat} objects. + * + * @return An unmodifiable {@link Map} containing format string to {@link RDFFormat} mappings. + */ + private static Map initFormatMap() { + Map map = new HashMap<>(); + + // Common file extension for Turtle + map.put("turtle", RDFFormat.TURTLE); + map.put("ttl", RDFFormat.TURTLE); + + // Common alternative for JSON-LD + map.put("jsonld", RDFFormat.JSONLD); + map.put("json-ld", RDFFormat.JSONLD); + + // Common alternative for RDF/XML + map.put("rdfxml", RDFFormat.RDFXML); + map.put("xml", RDFFormat.RDFXML); + + return Collections.unmodifiableMap(map); + } + + /** + * Creates and returns a default {@link WriterConfig} instance. + * This configuration enables pretty-printing for human-readable output. + * Deprecated JSON-LD specific settings have been removed. + * + * @return A {@link WriterConfig} instance with default serialization settings. + */ + private static WriterConfig createDefaultConfig() { + WriterConfig config = new WriterConfig(); + + config.set(BasicWriterSettings.PRETTY_PRINT, true); + + + return config; + } + + /** + * Returns a set of string identifiers for all supported RDF serialization formats. + * These identifiers can be used with the {@link #serialize(Model, OutputStream, String)} + * and {@link #getRdfFormat(String)} methods. + * + * @return A {@link Set} of {@link String} representing the supported RDF format identifiers. + */ + public static Set getSupportedFormats() { + return FORMAT_MAP.keySet(); + } + + /** + * Serializes an RDF4J {@link Model} to the specified {@link OutputStream} in Turtle format. + * The serialization uses the default writer configuration (e.g., pretty-printing enabled). + * + * @param model The RDF4J {@link Model} to serialize. Must not be {@code null}. + * @param outputStream The {@link OutputStream} to write the serialized data to. Must not be {@code null}. + * @throws IOException If an I/O error occurs during serialization. + * @throws NullPointerException If {@code model} or {@code outputStream} is {@code null}. + * @see #serialize(Model, OutputStream, RDFFormat) + */ + public static void serializeToTurtle(Model model, OutputStream outputStream) throws IOException { + serialize(model, outputStream, RDFFormat.TURTLE); + } + + /** + * Serializes an RDF4J {@link Model} to the specified {@link OutputStream} in JSON-LD format. + * The serialization uses the default writer configuration (e.g., pretty-printing enabled). + * + * @param model The RDF4J {@link Model} to serialize. Must not be {@code null}. + * @param outputStream The {@link OutputStream} to write the serialized data to. Must not be {@code null}. + * @throws IOException If an I/O error occurs during serialization. + * @throws NullPointerException If {@code model} or {@code outputStream} is {@code null}. + * @see #serialize(Model, OutputStream, RDFFormat) + */ + public static void serializeToJsonLd(Model model, OutputStream outputStream) throws IOException { + serialize(model, outputStream, RDFFormat.JSONLD); + } + + /** + * Serializes an RDF4J {@link Model} to the specified {@link OutputStream} in RDF/XML format. + * The serialization uses the default writer configuration (e.g., pretty-printing enabled). + * + * @param model The RDF4J {@link Model} to serialize. Must not be {@code null}. + * @param outputStream The {@link OutputStream} to write the serialized data to. Must not be {@code null}. + * @throws IOException If an I/O error occurs during serialization. + * @throws NullPointerException If {@code model} or {@code outputStream} is {@code null}. + * @see #serialize(Model, OutputStream, RDFFormat) + */ + public static void serializeToRdfXml(Model model, OutputStream outputStream) throws IOException { + serialize(model, outputStream, RDFFormat.RDFXML); + } + + + /** + * Resolves an {@link RDFFormat} from a given string identifier. + * The lookup is case-insensitive. + * + * @param formatString The string representing the desired RDF format (e.g., "turtle", "jsonld", "rdfxml", "ntriples"). + * Must not be {@code null}. + * @return The corresponding {@link RDFFormat} enum. + * @throws IllegalArgumentException If the provided {@code formatString} is not recognized as a supported format. + * @throws NullPointerException If {@code formatString} is {@code null}. + */ + public static RDFFormat getRdfFormat(String formatString) throws IllegalArgumentException { + Objects.requireNonNull(formatString, "Format string cannot be null"); + RDFFormat format = FORMAT_MAP.get(formatString.toLowerCase()); + if (format == null) { + throw new IllegalArgumentException("Unsupported format: " + formatString + + ". Supported formats: " + getSupportedFormats()); + } + return format; + } + + /** + * Serializes an RDF4J {@link Model} to a specified {@link OutputStream}, + * with the format determined dynamically by a string identifier. + * This method is useful when the target format is known at runtime. + * + * @param model The RDF4J {@link Model} to serialize. Must not be {@code null}. + * @param outputStream The {@link OutputStream} to write the serialized data to. Must not be {@code null}. + * @param formatString The string identifier of the desired RDF format (e.g., "turtle", "jsonld", "rdfxml", "ntriples"). + * Must not be {@code null}. + * @throws IOException If an I/O error occurs during serialization or if the underlying + * {@link Rio #write(Model, OutputStream, RDFFormat, WriterConfig)} call fails. + * @throws IllegalArgumentException If the provided {@code formatString} is not recognized. + * @throws NullPointerException If {@code model}, {@code outputStream}, or {@code formatString} is {@code null}. + */ + public static void serialize(Model model, OutputStream outputStream, String formatString) throws IOException { + Objects.requireNonNull(model, "Model cannot be null"); + Objects.requireNonNull(outputStream, "OutputStream cannot be null"); + + RDFFormat format = getRdfFormat(formatString); + serialize(model, outputStream, format); + } + + /** + * Internal method to perform the actual serialization using RDF4J Rio. + * This method applies the {@link #DEFAULT_CONFIG}. + * + * @param model The RDF4J {@link Model} to serialize. + * @param outputStream The {@link OutputStream} to write the serialized data to. + * @param format The {@link RDFFormat} to use for serialization. + * @throws IOException If an I/O error occurs during serialization. + */ + private static void serialize(Model model, OutputStream outputStream, RDFFormat format) throws IOException { + try { + Rio.write(model, outputStream, format, DEFAULT_CONFIG); + } catch (Exception e) { + + throw new IOException("Failed to serialize RDF model to " + format.getName(), e); + } + } + + /** + * Creates a custom {@link WriterConfig} instance with a specified pretty-printing setting. + * This method can be used if a serialization operation requires different settings + * than the {@link #DEFAULT_CONFIG}. + * + * @param prettyPrint A boolean indicating whether pretty-printing should be enabled. + * @return A new {@link WriterConfig} instance configured with the specified pretty-printing setting. + */ + public static WriterConfig createCustomConfig(boolean prettyPrint) { + WriterConfig config = new WriterConfig(); + config.set(BasicWriterSettings.PRETTY_PRINT, prettyPrint); + return config; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index da52e9920..b194a85cf 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -14,6 +14,10 @@ requires org.apache.commons.lang3; requires org.slf4j; + // Modules RDF4J + requires rdf4j.rio.api; + requires rdf4j.model.api; + exports fr.inria.corese.core.load; exports fr.inria.corese.core.load.result; exports fr.inria.corese.core; diff --git a/src/test/java/fr/inria/corese/core/next/api/model/serialization/Rdf4jSerializationUtilTest.java b/src/test/java/fr/inria/corese/core/next/api/model/serialization/Rdf4jSerializationUtilTest.java new file mode 100644 index 000000000..40bf17771 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/api/model/serialization/Rdf4jSerializationUtilTest.java @@ -0,0 +1,201 @@ +package fr.inria.corese.core.next.api.model.serialization; + +import fr.inria.corese.core.next.api.base.model.serialization.Rdf4jSerializationUtil; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.impl.LinkedHashModel; +import org.eclipse.rdf4j.model.vocabulary.FOAF; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.rio.RDFParseException; +import org.eclipse.rdf4j.rio.Rio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Unit tests for the {@link Rdf4jSerializationUtil} class. + * This class verifies the correct serialization of RDF4J Models + * to various RDF formats (Turtle, JSON-LD, RDF/XML, N-Triples) and tests + * the utility methods for format resolution and error handling. + */ +class Rdf4jSerializationUtilTest { + + private static final Logger logger = LoggerFactory.getLogger(Rdf4jSerializationUtilTest.class); + + + /** + * Tests the serialization of an RDF4J Model to Turtle format. + * Verifies that the output can be parsed back into an equivalent model. + * + * @throws IOException If an I/O error occurs during serialization. + * @throws RDFParseException If the serialized output cannot be parsed. + */ + @Test + void testSerializeToTurtle() throws IOException, RDFParseException { + Model originalModel = createTestModel(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Rdf4jSerializationUtil.serializeToTurtle(originalModel, out); + + String result = out.toString(); + logger.debug("testSerializeToTurtle Output:\n{}" , result); + + + ByteArrayInputStream in = new ByteArrayInputStream(result.getBytes()); + Model reparsedModel = Rio.parse(in, "", RDFFormat.TURTLE); + assertEquals(originalModel, reparsedModel, "The re-parsed Turtle model should be equal to the original model."); + } + + /** + * Tests the serialization of an RDF4J Model to JSON-LD format. + * Verifies that the output can be parsed back into an equivalent model. + * + * @throws IOException If an I/O error occurs during serialization. + * @throws RDFParseException If the serialized output cannot be parsed. + */ + @Test + void testSerializeToJsonLd() throws IOException, RDFParseException { + Model originalModel = createTestModel(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Rdf4jSerializationUtil.serializeToJsonLd(originalModel, out); + + String result = out.toString(); + logger.debug("testSerializeToJsonLd Output:\n{}" , result); + + ByteArrayInputStream in = new ByteArrayInputStream(result.getBytes()); + Model reparsedModel = Rio.parse(in, "", RDFFormat.JSONLD); + assertEquals(originalModel, reparsedModel, "The re-parsed JSON-LD model should be equal to the original model."); + } + + /** + * Tests the serialization of an RDF4J Model to RDF/XML format. + * Verifies that the output can be parsed back into an equivalent model. + * + * @throws IOException If an I/O error occurs during serialization. + * @throws RDFParseException If the serialized output cannot be parsed. + */ + @Test + void testSerializeToRdfXml() throws IOException, RDFParseException { + Model originalModel = createTestModel(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Rdf4jSerializationUtil.serializeToRdfXml(originalModel, out); + + String result = out.toString(); + logger.debug("testSerializeToRdfXml Output:\n{}" , result); + + ByteArrayInputStream in = new ByteArrayInputStream(result.getBytes()); + Model reparsedModel = Rio.parse(in, "", RDFFormat.RDFXML); + assertEquals(originalModel, reparsedModel, "The re-parsed RDF/XML model should be equal to the original model."); + } + + + + /** + * Tests the generic serialization method using a format string, specifically for Turtle. + * Verifies that the output can be parsed back into an equivalent model. + * + * @throws IOException If an I/O error occurs during serialization. + * @throws RDFParseException If the serialized output cannot be parsed. + */ + @Test + void testSerializeWithFormatString() throws IOException, RDFParseException { + Model originalModel = createTestModel(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + Rdf4jSerializationUtil.serialize(originalModel, out, "turtle"); + + String result = out.toString(); + logger.debug("testSerializeWithFormatString (Turtle) Output:\n{}" , result); + + ByteArrayInputStream in = new ByteArrayInputStream(result.getBytes()); + Model reparsedModel = Rio.parse(in, "", RDFFormat.TURTLE); + assertEquals(originalModel, reparsedModel, "The re-parsed Turtle model (from string method) should be equal to the original model."); + } + + /** + * Tests the {@link Rdf4jSerializationUtil#getRdfFormat(String)} method for supported formats. + * Verifies that the correct {@link RDFFormat} enum is returned for valid input strings. + */ + @Test + void testGetRdfFormat() { + assertEquals(RDFFormat.TURTLE, Rdf4jSerializationUtil.getRdfFormat("turtle")); + assertEquals(RDFFormat.TURTLE, Rdf4jSerializationUtil.getRdfFormat("TTL")); + assertEquals(RDFFormat.JSONLD, Rdf4jSerializationUtil.getRdfFormat("jsonld")); + assertEquals(RDFFormat.RDFXML, Rdf4jSerializationUtil.getRdfFormat("rdfxml")); + } + + /** + * Tests the {@link Rdf4jSerializationUtil#getRdfFormat(String)} method's error handling + * for unsupported format strings. + * Verifies that an {@link IllegalArgumentException} is thrown as expected. + */ + @Test + void testGetRdfFormatThrowsForUnsupportedFormat() { + assertThrows(IllegalArgumentException.class, + () -> Rdf4jSerializationUtil.getRdfFormat("unsupported")); + } + + /** + * Tests the null checks within the serialization methods. + * Verifies that {@link NullPointerException}s are thrown when null arguments are provided + * for model, output stream, or format string. + */ + @Test + void testNullChecks() { + Model model = createTestModel(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + + assertThrows(NullPointerException.class, + () -> Rdf4jSerializationUtil.serialize(null, out, "turtle")); + + + assertThrows(NullPointerException.class, + () -> Rdf4jSerializationUtil.serialize(model, null, "turtle")); + + assertThrows(NullPointerException.class, + () -> Rdf4jSerializationUtil.serialize(model, out, null)); + } + + /** + * Tests the {@link Rdf4jSerializationUtil#getSupportedFormats()} method. + * Verifies that the returned set contains expected format identifiers and has the correct size. + */ + @Test + void testGetSupportedFormats() { + var formats = Rdf4jSerializationUtil.getSupportedFormats(); + assertTrue(formats.contains("turtle")); + assertTrue(formats.contains("jsonld")); + assertTrue(formats.contains("rdfxml")); + assertEquals(6, formats.size()); + } + + /** + * Creates a simple RDF4J Model for testing purposes. + * The model contains a single triple: ex:test rdf:type foaf:Person. + * + * @return A {@link Model} instance populated with test data. + */ + private Model createTestModel() { + + ValueFactory vf = SimpleValueFactory.getInstance(); + Model model = new LinkedHashModel(); + model.add( + vf.createIRI("http://example.org/test"), + RDF.TYPE, + FOAF.PERSON + ); + return model; + } +} From ee8ed8aff4e57ac3a858b42579e18f8d575c22e0 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 3 Jun 2025 15:01:47 +0200 Subject: [PATCH 2/4] Serializer Turtle / JSON-LD / RDFXML --- .../serialization/Rdf4jSerializationUtil.java | 22 +----- .../core/next/impl/temp/CoreseModel.java | 63 +++++++++++++++- .../next/impl/temp/CoreseValueConverter.java | 71 ++++++++++++++++++- 3 files changed, 131 insertions(+), 25 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java index 552b09b2a..32242e6de 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/Rdf4jSerializationUtil.java @@ -23,16 +23,9 @@ */ public class Rdf4jSerializationUtil { - /** - * A unmodifiable map to quickly look up {@link RDFFormat} instances - * based on common string identifiers (case-insensitive). - */ + private static final Map FORMAT_MAP = initFormatMap(); - /** - * The default {@link WriterConfig} used for serialization, - * configured for pretty-printing. - */ private static final WriterConfig DEFAULT_CONFIG = createDefaultConfig(); /** @@ -189,17 +182,4 @@ private static void serialize(Model model, OutputStream outputStream, RDFFormat } } - /** - * Creates a custom {@link WriterConfig} instance with a specified pretty-printing setting. - * This method can be used if a serialization operation requires different settings - * than the {@link #DEFAULT_CONFIG}. - * - * @param prettyPrint A boolean indicating whether pretty-printing should be enabled. - * @return A new {@link WriterConfig} instance configured with the specified pretty-printing setting. - */ - public static WriterConfig createCustomConfig(boolean prettyPrint) { - WriterConfig config = new WriterConfig(); - config.set(BasicWriterSettings.PRETTY_PRINT, prettyPrint); - return config; - } } diff --git a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseModel.java b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseModel.java index e91c7c009..859fc10a4 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseModel.java +++ b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseModel.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.impl.temp; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -20,11 +22,17 @@ import fr.inria.corese.core.next.api.Statement; import fr.inria.corese.core.next.api.Value; import fr.inria.corese.core.next.api.base.model.AbstractModel; +import fr.inria.corese.core.next.api.base.model.serialization.Rdf4jSerializationUtil; + +import org.eclipse.rdf4j.model.impl.TreeModel; + /** * CoreseModel provides an implementation of the RDF Model interface * backed by a Corese Graph instance. It supports basic RDF operations * such as add, remove, contains, filtering, and namespace management. + * This class has been extended to include a method for serializing + * its content into various RDF4J formats using Rdf4jSerializationUtil. */ public class CoreseModel extends AbstractModel { @@ -139,6 +147,8 @@ public boolean add(Resource subject, IRI predicate, Value object, Resource... co for (Node coreseContext : coreseContexts) { if (coreseContext != null) { changed |= this.addEdgeToGraph(coreseSubj, coresePred, coreseObj, coreseContext); + } else { + changed |= this.addEdgeToGraph(coreseSubj, coresePred, coreseObj, null); } } return changed; @@ -228,7 +238,8 @@ protected void removeFilteredTermIteration( @Override public Iterator iterator() { - return getFilterIterator(null, null, null); + + return getFilterIterator(null, null, null, (Resource[]) null); } @Override @@ -294,7 +305,7 @@ private Iterable selectEdgesFromCorese(Node subject, Node predicate, Node /** * Add one statement ({@code subj}, {@code pred}, {@code obj}, {@code context}) * in the Corese graph. - * + * * @param subject Subject of the statement. * @param predicate Predicate of the statement. * @param object Object of the statement. @@ -327,7 +338,7 @@ private boolean addEdgeToGraph(Node subject, Node predicate, Node object, Node c * specified, statements with a context matching one of these will match. Note: * to match statements without an associated context, specify the value null and * explicitly cast it to type Resource. - * + * * @param subject The subject of the statements to match, null to match * statements with any subject. * @param predicate The Predicate of the statements to match, null to match @@ -394,4 +405,50 @@ public void remove() { return new CoreseModelIterator(statements.iterator()); } + /** + * Serializes this CoreseModel to the specified OutputStream in a given RDF4J format. + * This method first converts the CoreseModel's statements into an + * org.eclipse.rdf4j.model.Model, then uses Rdf4jSerializationUtil for serialization. + * + * @param outputStream The OutputStream to write the serialized data to. Must not be null. + * @param formatString The string identifier of the desired RDF format (e.g., "turtle", "jsonld", "rdfxml"). Must not be null. + * @throws IOException If an I/O error occurs during serialization or if an unsupported Corese value type is encountered. + * @throws IllegalArgumentException If the provided formatString is not recognized by Rdf4jSerializationUtil. + * @throws NullPointerException If outputStream or formatString is null. + */ + public void serializeToRdf4jFormat(OutputStream outputStream, String formatString) throws IOException { + Objects.requireNonNull(outputStream, "OutputStream cannot be null"); + Objects.requireNonNull(formatString, "Format string cannot be null"); + + + org.eclipse.rdf4j.model.Model rdf4jModel = new TreeModel(); + + for (fr.inria.corese.core.next.api.Namespace ns : this.getNamespaces()) { + rdf4jModel.setNamespace(ns.getPrefix(), ns.getName()); + } + + + for (Statement coreseStatement : this) { + + + org.eclipse.rdf4j.model.Resource rdf4jSubject = (org.eclipse.rdf4j.model.Resource) converter.valuetoRdf4jValue(converter.toCoreseNode(coreseStatement.getSubject()).getDatatypeValue()); + org.eclipse.rdf4j.model.IRI rdf4jPredicate = (org.eclipse.rdf4j.model.IRI) converter.valuetoRdf4jValue(converter.toCoreseNode(coreseStatement.getPredicate()).getDatatypeValue()); + org.eclipse.rdf4j.model.Value rdf4jObject = converter.valuetoRdf4jValue(converter.toCoreseNode(coreseStatement.getObject()).getDatatypeValue()); + + org.eclipse.rdf4j.model.Resource rdf4jContext = null; + if (coreseStatement.getContext() != null) { + rdf4jContext = converter.resourcetoRdf4jValueContext(converter.toCoreseNode(coreseStatement.getContext()).getDatatypeValue()); + } + + + if (rdf4jContext != null) { + rdf4jModel.add(rdf4jSubject, rdf4jPredicate, rdf4jObject, rdf4jContext); + } else { + rdf4jModel.add(rdf4jSubject, rdf4jPredicate, rdf4jObject); + } + } + + + Rdf4jSerializationUtil.serialize(rdf4jModel, outputStream, formatString); + } } diff --git a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java index 66501ab85..5ceae4070 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java +++ b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java @@ -10,6 +10,10 @@ import fr.inria.corese.core.next.api.ValueFactory; import fr.inria.corese.core.sparql.api.IDatatype; import fr.inria.corese.core.sparql.datatype.DatatypeMap; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.XMLSchema; + /** * Utility class for converting between Corese-compatible Node objects @@ -20,9 +24,14 @@ public class CoreseValueConverter { // Factory for creating Corese-compatible Value instances private final ValueFactory factory = new CoreseAdaptedValueFactory(); + private final org.eclipse.rdf4j.model.ValueFactory rdf4jFactory = SimpleValueFactory.getInstance(); + // Constant representing the default Corese graph context private static final Node DEFAULT_CORESE_CONTEXT = DatatypeMap.createResource(ExpType.DEFAULT_GRAPH); + private static final String HTTP_SCHEME_PREFIX = "http://"; + private static final String COLON = ":"; + // --- Rdf4j to Corese conversion methods --- /** @@ -81,7 +90,7 @@ public Node toCoreseContext(Resource context) { */ public Node[] toCoreseContextArray(Resource[] contexts) { if (contexts == null || (contexts.length == 1 && contexts[0] == null)) { - return new Node[] { DEFAULT_CORESE_CONTEXT }; + return new Node[]{DEFAULT_CORESE_CONTEXT}; } if (contexts.length == 0) { return new Node[0]; @@ -138,4 +147,64 @@ public Resource toRdf4jValueContext(Node node) { return DEFAULT_CORESE_CONTEXT.equals(node) ? null : (Resource) toRdf4jValue(node); } + /** + * Converts a Corese context node (which is an IDatatype) back to an RDF4J Resource. + * + * @param node Corese context node + * @return RDF4J Resource or null if it's the default context + */ + public org.eclipse.rdf4j.model.Resource resourcetoRdf4jValueContext(Node node) { + if (node == null) { + return null; + } + if (DEFAULT_CORESE_CONTEXT.equals(node)) { + return null; + } + + return (org.eclipse.rdf4j.model.Resource) valuetoRdf4jValue(node.getDatatypeValue()); + } + + /** + * Converts a Corese IDatatype to an RDF4J Value. + * This is the primary conversion logic from Corese internal value representation to RDF4J. + * + * @param dt Corese IDatatype to convert + * @return RDF4J Value equivalent + */ + public org.eclipse.rdf4j.model.Value valuetoRdf4jValue(IDatatype dt) { + if (dt == null) { + return null; + } + if (dt.isURI()) { + return rdf4jFactory.createIRI(dt.getLabel()); + } + if (dt.isBlank()) { + return rdf4jFactory.createBNode(dt.getLabel()); + } + if (dt.isLiteral()) { + + boolean hasValidLangTag = dt.getLang() != null && !dt.getLang().isEmpty() && !dt.getLang().contains(COLON); + boolean isRdfLangStringDatatype = dt.getDatatypeURI() != null && dt.getDatatypeURI().equals(RDF.LANGSTRING.stringValue()); + if (isRdfLangStringDatatype && hasValidLangTag) { + return rdf4jFactory.createLiteral(dt.getLabel(), dt.getLang()); + } + + boolean isImplicitXsdString = dt.getDatatypeURI() != null && dt.getDatatypeURI().equals(XMLSchema.STRING.stringValue()); + if (dt.getDatatypeURI() != null && !isRdfLangStringDatatype) { + return rdf4jFactory.createLiteral(dt.getLabel(), rdf4jFactory.createIRI(dt.getDatatypeURI())); + } + + if (isImplicitXsdString || (dt.getLang() != null && dt.getLang().contains(HTTP_SCHEME_PREFIX)) || !hasValidLangTag) { + return rdf4jFactory.createLiteral(dt.getLabel()); + } + if (dt.getLang() != null) { + return rdf4jFactory.createLiteral(dt.getLabel(), dt.getLang()); + } + return rdf4jFactory.createLiteral(dt.getLabel()); + } + + throw new IllegalArgumentException("Unsupported Corese IDatatype type for conversion to RDF4J Value: " + dt.getClass()); + } + + } From 03fc26571b2e09bce8c7b28cd767e1d4e2ca20ac Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Tue, 3 Jun 2025 16:50:21 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Revue=20Sonar=20du=20s=C3=A9rialiseur=20:?= =?UTF-8?q?=20Turtle=20/=20JSON-LD=20/=20RDF/XML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/impl/temp/CoreseValueConverter.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java index 5ceae4070..ea4ca6db3 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java +++ b/src/main/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverter.java @@ -12,8 +12,6 @@ import fr.inria.corese.core.sparql.datatype.DatatypeMap; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; -import org.eclipse.rdf4j.model.vocabulary.XMLSchema; - /** * Utility class for converting between Corese-compatible Node objects @@ -182,29 +180,35 @@ public org.eclipse.rdf4j.model.Value valuetoRdf4jValue(IDatatype dt) { return rdf4jFactory.createBNode(dt.getLabel()); } if (dt.isLiteral()) { - - boolean hasValidLangTag = dt.getLang() != null && !dt.getLang().isEmpty() && !dt.getLang().contains(COLON); - boolean isRdfLangStringDatatype = dt.getDatatypeURI() != null && dt.getDatatypeURI().equals(RDF.LANGSTRING.stringValue()); - if (isRdfLangStringDatatype && hasValidLangTag) { - return rdf4jFactory.createLiteral(dt.getLabel(), dt.getLang()); - } - - boolean isImplicitXsdString = dt.getDatatypeURI() != null && dt.getDatatypeURI().equals(XMLSchema.STRING.stringValue()); - if (dt.getDatatypeURI() != null && !isRdfLangStringDatatype) { - return rdf4jFactory.createLiteral(dt.getLabel(), rdf4jFactory.createIRI(dt.getDatatypeURI())); - } - - if (isImplicitXsdString || (dt.getLang() != null && dt.getLang().contains(HTTP_SCHEME_PREFIX)) || !hasValidLangTag) { - return rdf4jFactory.createLiteral(dt.getLabel()); - } - if (dt.getLang() != null) { - return rdf4jFactory.createLiteral(dt.getLabel(), dt.getLang()); - } - return rdf4jFactory.createLiteral(dt.getLabel()); + return convertLiteralToRdf4jValue(dt); } throw new IllegalArgumentException("Unsupported Corese IDatatype type for conversion to RDF4J Value: " + dt.getClass()); } + /** + * Helper method to convert a Corese IDatatype representing a literal + * into an RDF4J Literal, handling various cases including problematic ones. + * + * @param dt The Corese IDatatype (must be a literal) + * @return The corresponding RDF4J Literal + */ + private org.eclipse.rdf4j.model.Literal convertLiteralToRdf4jValue(IDatatype dt) { + + boolean hasLang = dt.getLang() != null && !dt.getLang().isEmpty(); + boolean langContainsColon = hasLang && dt.getLang().contains(COLON); + boolean langContainsHttpPrefix = hasLang && dt.getLang().contains(HTTP_SCHEME_PREFIX); + boolean isRdfLangString = dt.getDatatypeURI() != null && dt.getDatatypeURI().equals(RDF.LANGSTRING.stringValue()); + + if (isRdfLangString && hasLang && !langContainsColon && !langContainsHttpPrefix) { + return rdf4jFactory.createLiteral(dt.getLabel(), dt.getLang()); + } + + if (dt.getDatatypeURI() != null && !isRdfLangString) { + return rdf4jFactory.createLiteral(dt.getLabel(), rdf4jFactory.createIRI(dt.getDatatypeURI())); + } + + return rdf4jFactory.createLiteral(dt.getLabel()); + } } From 1c93c78b24b7f109527e01f34f56d74ed7d8389c Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 4 Jun 2025 09:24:43 +0200 Subject: [PATCH 4/4] =?UTF-8?q?TU=20=20du=20s=C3=A9rialiseur=20:=20Turtle?= =?UTF-8?q?=20/=20JSON-LD=20/=20RDF/XML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/next/impl/temp/CoreseModelTest.java | 180 +++++++++++- .../impl/temp/CoreseValueConverterTest.java | 269 ++++++++++++++++++ 2 files changed, 441 insertions(+), 8 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverterTest.java diff --git a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseModelTest.java b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseModelTest.java index ebcded5d4..98acda01e 100644 --- a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseModelTest.java +++ b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseModelTest.java @@ -3,12 +3,11 @@ import fr.inria.corese.core.Graph; import fr.inria.corese.core.kgram.api.core.Edge; import fr.inria.corese.core.kgram.api.core.Node; -import fr.inria.corese.core.next.api.IRI; -import fr.inria.corese.core.next.api.Model; -import fr.inria.corese.core.next.api.Namespace; -import fr.inria.corese.core.next.api.Resource; -import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.api.*; import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.vocabulary.XMLSchema; +import org.eclipse.rdf4j.rio.Rio; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,6 +16,14 @@ import org.mockito.quality.Strictness; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.util.Models; +import org.eclipse.rdf4j.model.impl.LinkedHashModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; import java.util.Arrays; import java.util.HashSet; @@ -25,14 +32,21 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; - +import java.io.ByteArrayOutputStream; +import java.io.IOException; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class CoreseModelTest { + private static final Logger logger = LoggerFactory.getLogger(CoreseModelTest.class); + + private CoreseModel coreseModel; + @Mock + private CoreseAdaptedValueFactory valueFactory; + @Mock private Model model; @Mock @@ -62,12 +76,14 @@ class CoreseModelTest { private Statement statement0; @Mock private Statement statement1; - + @Mock + private CoreseValueConverter coreseValueConverter; @BeforeEach void setUp() { // Initializes the CoreseModel instance for testing coreseModel = new CoreseModel(mockCoreseGraph, new HashSet<>()); + valueFactory = new CoreseAdaptedValueFactory(); when(mockValueConverter.toCoreseNode(mockSubjectIRI)).thenReturn(mockSubjectNode); when(mockValueConverter.toCoreseNode(mockPredicateIRI)).thenReturn(mockPredicateNode); @@ -338,7 +354,7 @@ void testContainsExistingStatementWithoutContext() { when(mockCoreseGraph.getEdgeFactory()).thenReturn(mock(fr.inria.corese.core.EdgeFactory.class)); when(mockCoreseGraph.getEdgeFactory().copy(any(fr.inria.corese.core.kgram.api.core.Edge.class))).thenReturn(mockReturnedEdge); - // Call the contains method + boolean contains = coreseModel.contains(realSubject, realPredicate, realObject); assertTrue(contains, "Le modèle devrait contenir la déclaration sans contexte."); @@ -417,4 +433,152 @@ void testContainsNonExistingTriple() { assertFalse(coreseModel.contains(georgeBrassens, rdfType, singer), "Le modèle ne devrait pas contenir un triple non existant."); verify(mockCoreseGraph, times(0)).getEdgesRDF4J(any(Node.class), any(Node.class), any(Node.class), eq(null)); } + + + + /** + * Tests that the `serializeToRdf4jFormat` method throws a `NullPointerException` + * when a `null` `OutputStream` is provided. + */ + @Test + void testSerializeThrowsOnNullOutputStream() { + assertThrows(NullPointerException.class, () -> { + coreseModel.serializeToRdf4jFormat(null, "turtle"); + }); + } + /** + * Tests that the `serializeToRdf4jFormat` method throws an `IllegalArgumentException` + * when an unknown or unsupported RDF format string is provided. + */ + @Test + void testSerializeThrowsOnUnknownFormat() { + assertThrows(IllegalArgumentException.class, () -> { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + coreseModel.serializeToRdf4jFormat(os, "unsupported-format"); + }); + } + + /** + * Tests the serialization of a CoreseModel to Turtle format. + * It adds a statement to a concrete CoreseModel, serializes it to Turtle, + * and then parses the output back into an RDF4J Model to verify isomorphism + * with the expected RDF4J Model. It also checks for specific string content + * in the Turtle output. + * + * @throws IOException if an I/O error occurs during serialization or parsing. + */ + @Test + void testSerializeToTurtleFormat() throws IOException { + Resource subject = valueFactory.createIRI("http://example.org/subject"); + IRI predicate = valueFactory.createIRI("http://example.org/predicate"); + + + fr.inria.corese.core.next.api.Value object = valueFactory.createLiteral("Test literal"); + + org.eclipse.rdf4j.model.ValueFactory rdf4jFactory = SimpleValueFactory.getInstance(); + + CoreseModel concreteModel = new CoreseModel(); + concreteModel.add(subject, predicate, object); + + Model expectedRdf4jModel = new LinkedHashModel(); + expectedRdf4jModel.add( + rdf4jFactory.createIRI(subject.stringValue()), + rdf4jFactory.createIRI(predicate.stringValue()), + rdf4jFactory.createLiteral(object.stringValue()) + ); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + concreteModel.serializeToRdf4jFormat(outputStream, "turtle"); + + String output = outputStream.toString("UTF-8"); + assertNotNull(output); + logger.info("testSerializeToTurtleFormat {} ", output); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(output.getBytes("UTF-8")); + Model parsedModel = Rio.parse(inputStream, "", RDFFormat.TURTLE); + + assertTrue(Models.isomorphic(expectedRdf4jModel, parsedModel), "Le modèle Turtle sérialisé et re-parsé doit être isomorphe à l'original."); + assertTrue(output.contains("subject"), "La sortie devrait contenir 'subject'"); + assertTrue(output.contains("predicate"), "La sortie devrait contenir 'predicate'"); + + assertTrue(output.contains("\"Test literal\""), "La sortie devrait contenir le littéral simple correctement."); + assertFalse(output.contains("@"), "La sortie ne devrait pas contenir de balise de langue"); + assertFalse(output.contains("^^"), "La sortie ne devrait pas contenir de type de données explicite"); + } + + /** + * Tests the serialization of a CoreseModel to JSON-LD format. + * It adds a statement to a concrete CoreseModel, serializes it to JSON-LD, + * and then parses the output back into an RDF4J Model to verify isomorphism + * with the expected RDF4J Model. It also checks for specific string content + * in the JSON-LD output. + * + * @throws IOException if an I/O error occurs during serialization or parsing. + */ + @Test + void testSerializeToJsonLdFormat() throws IOException { + Resource subject = valueFactory.createIRI("http://example.org/subject"); + IRI predicate = valueFactory.createIRI("http://example.org/predicate"); + fr.inria.corese.core.next.api.Value object = valueFactory.createLiteral("Test literal"); + org.eclipse.rdf4j.model.ValueFactory rdf4jFactory = SimpleValueFactory.getInstance(); + + + CoreseModel concreteModel = new CoreseModel(); + concreteModel.add(subject, predicate, object); + + Model expectedRdf4jModel = new LinkedHashModel(); + expectedRdf4jModel.add( + rdf4jFactory.createIRI(subject.stringValue()), + rdf4jFactory.createIRI(predicate.stringValue()), + rdf4jFactory.createLiteral(object.stringValue(), rdf4jFactory.createIRI(XMLSchema.STRING.stringValue())) + ); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + + concreteModel.serializeToRdf4jFormat(outputStream, "jsonld"); + + String output = outputStream.toString("UTF-8"); + assertNotNull(output); + + logger.info("testSerializeToJsonLdFormat {}" , output); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(output.getBytes("UTF-8")); + + Model parsedModel = Rio.parse(inputStream, "", RDFFormat.JSONLD); + + assertTrue(Models.isomorphic(expectedRdf4jModel, parsedModel), "Le modèle JSON-LD sérialisé et re-parsé doit être isomorphe à l'original."); + assertTrue(output.contains("http://example.org/subject"), "La sortie JSON-LD devrait contenir 'http://example.org/subject'"); + assertTrue(output.contains("http://example.org/predicate"), "La sortie JSON-LD devrait contenir 'http://example.org/predicate'"); + assertTrue(output.contains("Test literal"), "La sortie JSON-LD devrait contenir 'Test literal'"); + } + /** + * Tests the serialization of a simple CoreseModel to RDF/XML format. + * It adds a basic statement to the model and then attempts to serialize it, + * checking for the presence of key RDF/XML elements in the output. + * + * @throws IOException if an I/O error occurs during serialization. + */ + @Test + void testSerializeToRDFXMLFormat() throws IOException { + + coreseModel.add( + valueFactory.createIRI("http://example.org/a"), + valueFactory.createIRI("http://example.org/b"), + valueFactory.createLiteral("c") + ); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + + coreseModel.serializeToRdf4jFormat(outputStream, "rdfxml"); + + String output = outputStream.toString("UTF-8"); + assertNotNull(output); + logger.info("testSerializeToRDFXMLFormat {}" , output); + assertTrue(output.contains("rdf:RDF") || output.contains("rdf:Description")); + + + } } diff --git a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverterTest.java b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverterTest.java new file mode 100644 index 000000000..b86becdfe --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseValueConverterTest.java @@ -0,0 +1,269 @@ +package fr.inria.corese.core.next.impl.temp; // Adjust package as necessary + +import fr.inria.corese.core.sparql.api.IDatatype; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.BNode; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.XMLSchema; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the {@link CoreseValueConverter} class. + * This class focuses on testing the conversion logic from Corese's internal + * {@link IDatatype} representation to RDF4J's {@link Value} objects, + * especially for URI, Blank Node, and various Literal types. + */ +@DisplayName("CoreseValueConverter: Test de la conversion IDatatype Corese vers RDF4J Value") +class CoreseValueConverterTest { + + private CoreseValueConverter converter; + private IDatatype mockIDatatype; + + /** + * Sets up the test environment before each test method is executed. + * Initializes a new {@link CoreseValueConverter} instance and a mock + * {@link IDatatype} object. + */ + @BeforeEach + void setUp() { + converter = new CoreseValueConverter(); + mockIDatatype = Mockito.mock(IDatatype.class); + } + + /** + * Tests the conversion of a Corese URI {@link IDatatype} to an RDF4J {@link IRI}. + * Verifies that the resulting value is an IRI and its string representation matches the expected URI. + */ + @Test + @DisplayName("Conversion URI: IDatatype URI vers RDF4J IRI") + void testConvertUriToRdf4jValue() { + String uriString = "http://example.org/resource"; + when(mockIDatatype.isURI()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(uriString); + + Value result = converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertTrue(result instanceof IRI); + assertEquals(uriString, result.stringValue()); + } + + /** + * Tests the conversion of a Corese Blank Node {@link IDatatype} to an RDF4J {@link BNode}. + * Verifies that the resulting value is a BNode and its ID matches the expected blank node ID. + */ + @Test + @DisplayName("Conversion BNode: IDatatype Blank Node vers RDF4J BNode") + void testConvertBNodeToRdf4jValue() { + String bnodeId = "b123"; + when(mockIDatatype.isBlank()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(bnodeId); + + Value result = converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertTrue(result instanceof BNode); + assertEquals(bnodeId, ((BNode) result).getID()); + } + + + /** + * Tests the conversion of a Corese plain literal (no language, no datatype) + * to an RDF4J {@link Literal}. + * Verifies that the literal's label is correct and that it has neither a language tag + * nor an explicit datatype URI (i.e., it's a plain literal). + */ + @Test + @DisplayName("Literal Simple: Conversion d'un littéral sans langue ni datatype") + void testConvertPlainLiteral() { + String label = "Hello world"; + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(null); + when(mockIDatatype.getDatatypeURI()).thenReturn(null); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + } + + /** + * Tests the conversion of a Corese language-tagged literal to an RDF4J {@link Literal}. + * Verifies the label, the presence and value of the language tag, and that its datatype + * is correctly identified as `rdf:langString`. + */ + @Test + @DisplayName("Literal avec Langue: Conversion d'un littéral avec tag de langue valide") + void testConvertLanguageTaggedLiteral() { + String label = "Bonjour"; + String lang = "fr"; + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(lang); + when(mockIDatatype.getDatatypeURI()).thenReturn(RDF.LANGSTRING.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + assertEquals(RDF.LANGSTRING, result.getDatatype()); + } + + /** + * Tests the conversion of a Corese typed literal with `xsd:string` datatype + * to an RDF4J {@link Literal}. + * Verifies the label, absence of language tag, and correct `xsd:string` datatype. + */ + @Test + void testConvertTypedLiteral_XSDString() { + String label = "Some text"; + + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(null); + when(mockIDatatype.getDatatypeURI()).thenReturn(XMLSchema.STRING.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + assertEquals(XMLSchema.STRING, result.getDatatype()); + } + + /** + * Tests the conversion of a Corese typed literal with `xsd:integer` datatype + * to an RDF4J {@link Literal}. + * Verifies the label, absence of language tag, and correct `xsd:integer` datatype. + */ + @Test + void testConvertTypedLiteral_XSDInteger() { + String label = "123"; + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(null); + when(mockIDatatype.getDatatypeURI()).thenReturn(XMLSchema.INTEGER.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + assertEquals(XMLSchema.INTEGER, result.getDatatype()); + } + + /** + * Tests a problematic scenario where Corese might incorrectly put a URI + * (like `xsd:string`) into the language field and/or set `rdf:langString` + * as datatype for a non-language-tagged literal. + * Verifies that the converter handles this by defaulting to a plain literal. + */ + @Test + void testConvertProblematicLiteral_LangFieldContainsURI() { + String label = "Problematic literal"; + // This is the scenario we encountered: xsd:string URI in the lang field + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(XMLSchema.STRING.stringValue()); + when(mockIDatatype.getDatatypeURI()).thenReturn(RDF.LANGSTRING.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage(), "Should not have a language tag if it's a URI"); + assertNotNull(result.getDatatype(), "Should be a plain literal after correction"); + } + + /** + * Tests a scenario where Corese implicitly treats a literal as `xsd:string` + * but provides a null language tag. + * Verifies that the converter correctly handles this, typically resulting in a plain literal + * or a literal typed as `xsd:string` if the logic explicitly handles it. + */ + @Test + void testConvertProblematicLiteral_ImplicitXSDStringWithNullLang() { + String label = "Implicit XSD string"; + + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(null); + when(mockIDatatype.getDatatypeURI()).thenReturn(XMLSchema.STRING.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + } + + /** + * Tests a scenario where `rdf:langString` is indicated, but the provided + * language tag is invalid (e.g., empty string or contains a colon). + * Verifies that the converter falls back to a plain literal in such cases. + */ + @Test + @DisplayName("Literal Problématique: rdf:langString avec tag de langue invalide (vide ou colon)") + void testConvertProblematicLiteral_RdfLangStringWithoutValidLang() { + String label = "Literal with rdf:langString but no valid lang"; + when(mockIDatatype.isLiteral()).thenReturn(true); + when(mockIDatatype.getLabel()).thenReturn(label); + when(mockIDatatype.getLang()).thenReturn(""); + when(mockIDatatype.getDatatypeURI()).thenReturn(RDF.LANGSTRING.stringValue()); + + Literal result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage(), "Should not have a language tag if invalid"); + assertNotNull(result.getDatatype(), "Should be a plain literal fallback"); + + + when(mockIDatatype.getLang()).thenReturn("en:US"); + result = (Literal) converter.valuetoRdf4jValue(mockIDatatype); + assertNotNull(result); + assertEquals(label, result.getLabel()); + assertNotNull(result.getLanguage()); + assertNotNull(result.getDatatype()); + } + /** + * Tests that an {@link IllegalArgumentException} is thrown when attempting + * to convert an unsupported Corese {@link IDatatype} type (e.g., not URI, Blank Node, or Literal). + */ + @Test + @DisplayName("Gestion des Erreurs: Type d'IDatatype non supporté") + void testConvertUnsupportedDatatype() { + + when(mockIDatatype.isURI()).thenReturn(false); + when(mockIDatatype.isBlank()).thenReturn(false); + when(mockIDatatype.isLiteral()).thenReturn(false); + + assertThrows(IllegalArgumentException.class, () -> { + converter.valuetoRdf4jValue(mockIDatatype); + }); + } + /** + * Tests that `null` input to {@link CoreseValueConverter#valuetoRdf4jValue(IDatatype)} + * correctly returns `null`, ensuring graceful handling of null values. + */ + @Test + @DisplayName("Gestion des Nulls: IDatatype null en entrée") + void testConvertNullIDatatype() { + assertNull(converter.valuetoRdf4jValue(null)); + } +} \ No newline at end of file