diff --git a/src/main/java/fr/inria/corese/core/next/api/AbstractBNode.java b/src/main/java/fr/inria/corese/core/next/api/AbstractBNode.java
deleted file mode 100644
index 961ce814a..000000000
--- a/src/main/java/fr/inria/corese/core/next/api/AbstractBNode.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package fr.inria.corese.core.next.api;
-
-import java.util.Objects;
-
-/**
- * Abstract base class for blank nodes in an RDF graph.
- *
- * Provides default implementations for {@link BNode#getID()},
- * {@link Object#equals(Object)}, and {@link Object#hashCode()}.
- *
- */
-public abstract class AbstractBNode implements BNode {
-
- private static final String BLANK_NODE_PREFIX = "_:";
-
- /** Internal identifier for the blank node (unique within a model) */
- private final String id;
-
- /** Serial version UID for serialization */
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a blank node with a specific identifier.
- *
- * Warning: This bypasses the automatic ID generation and should only be
- * used when restoring an existing RDF graph (e.g. during parsing or tests).
- *
- */
- protected AbstractBNode(String id) {
- this.id = Objects.requireNonNull(id, "Blank node ID must not be null");
- }
-
- @Override
- public String getID() {
- return id;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o)
- return true;
- if (!(o instanceof BNode))
- return false;
- BNode other = (BNode) o;
- return id.equals(other.getID());
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public String stringValue() {
- return BLANK_NODE_PREFIX + id;
- }
-
- @Override
- public String toString() {
- return BLANK_NODE_PREFIX + id;
- }
-}
diff --git a/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java b/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java
new file mode 100644
index 000000000..4741a9712
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java
@@ -0,0 +1,22 @@
+package fr.inria.corese.core.next.api;
+
+import java.io.Writer;
+
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+
+public interface FormatSerializer {
+
+ /**
+ * A serializer that converts a {@link Model} instance
+ * into a specific output format and writes it to a character stream.
+ *
+ * Implementations may follow standard RDF serialization formats
+ * (e.g., Turtle, N-Triples, JSON-LD), or define custom formats.
+ *
+ * @param writer the destination {@link Writer} for the serialized
+ * output
+ * @throws SerializationException if an error occurs during the serialization
+ * process
+ */
+ void write(Writer writer) throws SerializationException;
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/api/Resource.java b/src/main/java/fr/inria/corese/core/next/api/Resource.java
index 153907344..151a0f099 100644
--- a/src/main/java/fr/inria/corese/core/next/api/Resource.java
+++ b/src/main/java/fr/inria/corese/core/next/api/Resource.java
@@ -1,7 +1,8 @@
package fr.inria.corese.core.next.api;
/**
- * Super interface of all resources of an RDF graph (statements, IRI, blank nodes) as defined for RDF 1.2.
+ * Super interface of all resources of an RDF graph (statements, IRI, blank
+ * nodes) as defined for RDF 1.2.
*/
public interface Resource extends Value {
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FileFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FileFormat.java
new file mode 100644
index 000000000..8242799b9
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FileFormat.java
@@ -0,0 +1,103 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a general file format, including its name, associated file
+ * extensions, and MIME types.
+ */
+public class FileFormat {
+
+ private final String name;
+ private final List extensions;
+ private final List mimeTypes;
+
+ /**
+ * Constructs a new FileFormat instance.
+ *
+ * @param name The human-readable name of the format.
+ * @param extensions The list of file extensions.
+ * @param mimeTypes The list of MIME types.
+ * @throws NullPointerException if name, extensions or mimeTypes is null or
+ * empty.
+ */
+ public FileFormat(String name, List extensions, List mimeTypes) {
+ this.name = Objects.requireNonNull(name, "Format name cannot be null");
+ this.extensions = List.copyOf(Objects.requireNonNull(extensions, "Extensions list cannot be null"));
+ this.mimeTypes = List.copyOf(Objects.requireNonNull(mimeTypes, "MIME types list cannot be null"));
+
+ if (extensions.isEmpty()) {
+ throw new IllegalArgumentException("At least one file extension must be provided");
+ }
+ if (mimeTypes.isEmpty()) {
+ throw new IllegalArgumentException("At least one MIME type must be provided");
+ }
+ }
+
+ /**
+ * Returns the name of the format.
+ *
+ * @return The format name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the list of known file extensions.
+ *
+ * @return A list of extensions.
+ */
+ public List getExtensions() {
+ return extensions;
+ }
+
+ /**
+ * Returns the list of associated MIME types.
+ *
+ * @return A list of MIME types.
+ */
+ public List getMimeTypes() {
+ return mimeTypes;
+ }
+
+ /**
+ * Returns the default (primary) file extension.
+ *
+ * @return The first extension in the list.
+ */
+ public String getDefaultExtension() {
+ return extensions.get(0);
+ }
+
+ /**
+ * Returns the default (primary) MIME type.
+ *
+ * @return The first MIME type in the list.
+ */
+ public String getDefaultMimeType() {
+ return mimeTypes.get(0);
+ }
+
+ @Override
+ public String toString() {
+ return "FileFormat{name='%s', extensions=%s, mimeTypes=%s}".formatted(name, extensions, mimeTypes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof FileFormat other))
+ return false;
+ return name.equalsIgnoreCase(other.name)
+ && extensions.equals(other.extensions)
+ && mimeTypes.equals(other.mimeTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name.toLowerCase(), extensions, mimeTypes);
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java
new file mode 100644
index 000000000..0e85acff7
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java
@@ -0,0 +1,69 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import java.util.Objects;
+
+/**
+ * Configuration options for the {@link NTriplesFormat} serializer.
+ * Use {@link FormatConfig # Builder} to create instances.
+ */
+public class FormatConfig {
+
+ private final String blankNodePrefix;
+
+
+ /**
+ * Private constructor to enforce usage of the Builder.
+ *
+ * @param builder The builder instance.
+ */
+ private FormatConfig(Builder builder) {
+ this.blankNodePrefix = builder.blankNodePrefix;
+
+ }
+
+ /**
+ * Returns the prefix to use for blank nodes.
+ *
+ * @return The blank node prefix.
+ */
+ public String getBlankNodePrefix() {
+ return blankNodePrefix;
+ }
+
+ /**
+ * Builder class for {@link FormatConfig}.
+ */
+ public static class Builder {
+
+ private String blankNodePrefix = "_:";
+
+ /**
+ * Default constructor for the Builder.
+ * Initializes fields with default values.
+ */
+ Builder() {
+
+ }
+
+ /**
+ * Sets the prefix to use for blank nodes. Default is "_:".
+ *
+ * @param blankNodePrefix The desired blank node prefix.
+ * @return The builder instance.
+ */
+ public Builder blankNodePrefix(String blankNodePrefix) {
+ this.blankNodePrefix = Objects.requireNonNull(blankNodePrefix, "Blank node prefix cannot be null");
+ return this;
+ }
+
+
+ /**
+ * Builds a new {@link FormatConfig} instance.
+ *
+ * @return A new NFormatConfig instance.
+ */
+ public FormatConfig build() {
+ return new FormatConfig(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java
new file mode 100644
index 000000000..daa921cf0
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java
@@ -0,0 +1,304 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.util.SerializationConstants;
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.util.Objects;
+
+/**
+ * Serializes a Corese {@link Model} into N-Quads format.
+ * This class provides a method to write the statements (quads) of a model to a given {@link Writer}
+ * according to the N-Quads specification, including support for named graphs (contexts).
+ */
+public class NQuadsFormat implements FormatSerializer {
+
+ /**
+ * Logger for this class, used for logging potential issues or information during serialization.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(NQuadsFormat.class);
+
+ private final Model model;
+ private final FormatConfig config;
+
+ /**
+ * Constructs a new {@code NQuadsFormat} instance with the specified model and default configuration.
+ *
+ * @param model the {@link Model} to be serialized. Must not be null.
+ * @throws NullPointerException if the provided model is null.
+ */
+ public NQuadsFormat(Model model) {
+ this(model, new FormatConfig.Builder().build());
+ }
+
+ /**
+ * Constructs a new {@code NQuadsFormat} instance with the specified model and custom configuration.
+ *
+ * @param model the {@link Model} to be serialized. Must not be null.
+ * @param config the {@link FormatConfig} to use for serialization. Must not be null.
+ * @throws NullPointerException if the provided model or config is null.
+ */
+ public NQuadsFormat(Model model, FormatConfig config) {
+ this.model = Objects.requireNonNull(model, "Model cannot be null");
+ this.config = Objects.requireNonNull(config, "Configuration cannot be null");
+ }
+
+ /**
+ * Writes the model to the given writer in N-Quads format.
+ * Each statement (quad) in the model is written on a new line, terminated by a dot and a newline character.
+ *
+ * @param writer the {@link Writer} to which the N-Quads output will be written.
+ * @throws SerializationException if an I/O error occurs during writing or if invalid data is encountered.
+ */
+ @Override
+ public void write(Writer writer) throws SerializationException {
+ try {
+ for (Statement stmt : model) {
+ writeStatement(writer, stmt);
+ }
+ writer.flush();
+ } catch (IOException e) {
+ throw new SerializationException("Failed to write", "NQuads", e);
+ } catch (IllegalArgumentException e) {
+ throw new SerializationException("Invalid data: " + e.getMessage(), "NQuads", e);
+ }
+ }
+
+ /**
+ * Writes a single {@link Statement} (quad) to the writer in N-Quads format.
+ * The statement is written as "$subject $predicate $object $context ." if a context is present,
+ * or "$subject $predicate $object ." if no context is present (default graph).
+ *
+ * @param writer the {@link Writer} to which the statement will be written.
+ * @param stmt the {@link Statement} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeStatement(Writer writer, Statement stmt) throws IOException {
+ writeValue(writer, stmt.getSubject());
+ writer.write(SerializationConstants.SPACE);
+ writeValue(writer, stmt.getPredicate());
+ writer.write(SerializationConstants.SPACE);
+ writeValue(writer, stmt.getObject());
+
+ Resource context = stmt.getContext();
+ if (context != null) {
+ writer.write(SerializationConstants.SPACE);
+ writeValue(writer, context);
+ }
+
+ writer.write(SerializationConstants.SPACE_POINT);
+ }
+
+ /**
+ * Writes a single {@link Value} to the writer.
+ * Handles literals, blank nodes, and IRIs.
+ *
+ * @param writer the {@link Writer} to which the value will be written.
+ * @param value the {@link Value} to write.
+ * @throws IOException if an I/O error occurs.
+ * @throws IllegalArgumentException if the provided value is null or an unsupported type.
+ */
+ private void writeValue(Writer writer, Value value) throws IOException {
+ validateValue(value);
+
+ if (value.isLiteral()) {
+ writeLiteral(writer, (Literal) value);
+ } else if (value.isResource()) {
+ if (value.isIRI()) {
+ writeIRI(writer, (IRI) value);
+ } else if (value.isBNode()) {
+ writeBlankNode(writer, (Resource) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported resource type for N-Quads serialization: " + value.getClass().getName());
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported value type for N-Quads serialization: " + value.getClass().getName());
+ }
+ }
+
+ /**
+ * Writes a {@link Literal} to the writer in N-Quads format.
+ * Handles plain literals, language-tagged literals, and typed literals.
+ *
+ * @param writer the {@link Writer} to which the literal will be written.
+ * @param literal the {@link Literal} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeLiteral(Writer writer, Literal literal) throws IOException {
+ writer.write(SerializationConstants.QUOTE);
+ writer.write(escapeLiteral(literal.stringValue()));
+ writer.write(SerializationConstants.QUOTE);
+
+ // Gestion du langage
+ literal.getLanguage().ifPresent(lang -> {
+ try {
+ writer.write(SerializationConstants.AT_SIGN + lang);
+ } catch (IOException e) {
+ throw new UncheckedIOException("Error writing language tag", e);
+ }
+ });
+
+ if (!literal.getLanguage().isPresent()) {
+ IRI datatype = literal.getDatatype();
+ if (datatype != null && !datatype.stringValue().equals(SerializationConstants.XSD_STRING)) {
+ writer.write(SerializationConstants.DATATYPE_SEPARATOR);
+ writeIRI(writer, datatype);
+ }
+ }
+ }
+
+ /**
+ * Writes an {@link IRI} to the writer.
+ * The IRI's string representation must be enclosed in angle brackets for N-Quads.
+ *
+ * @param writer the {@link Writer} to which the IRI will be written.
+ * @param iri the {@link IRI} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeIRI(Writer writer, IRI iri) throws IOException {
+ writer.write(SerializationConstants.LT);
+ writer.write(escapeIRI(iri.stringValue()));
+ writer.write(SerializationConstants.GT);
+ }
+
+ /**
+ * Writes a blank node to the writer.
+ * Blank nodes are prefixed with "_:", and the identifier is appended.
+ *
+ * @param writer the {@link Writer} to which the blank node will be written.
+ * @param blankNode the {@link Resource} representing the blank node.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeBlankNode(Writer writer, Resource blankNode) throws IOException {
+ writer.write(config.getBlankNodePrefix());
+ writer.write(blankNode.stringValue());
+ }
+
+ /**
+ * Validates and potentially escapes an IRI string.
+ * Throws an {@link IllegalArgumentException} if the IRI contains characters
+ * that are not allowed in N-Quads unescaped form (like spaces, quotes, angle brackets).
+ *
+ * @param iri The string value of the IRI to validate and escape.
+ * @return The validated and potentially escaped IRI string.
+ * @throws IllegalArgumentException if the IRI string is invalid.
+ */
+ private String escapeIRI(String iri) {
+
+ if (iri.contains(SerializationConstants.SPACE) || iri.contains(SerializationConstants.QUOTE) ||
+ iri.contains(SerializationConstants.LT) || iri.contains(SerializationConstants.GT)) {
+ throw new IllegalArgumentException("Invalid IRI: contains illegal characters for N-Quads unescaped form: " + iri);
+ }
+ return iri;
+ }
+
+ /**
+ * Escape special characters in N-Quads string literals.
+ * Handles backslash, double quote, and common control characters.
+ * Unicode escape sequences are used for unprintable characters.
+ *
+ * @param value The string value of the literal to escape.
+ * @return The escaped string suitable for N-Quads literal.
+ */
+ private String escapeLiteral(String value) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '\n':
+ sb.append(SerializationConstants.BACK_SLASH).append('n');
+ break;
+ case '\r':
+ sb.append(SerializationConstants.BACK_SLASH).append('r');
+ break;
+ case '\t':
+ sb.append(SerializationConstants.BACK_SLASH).append('t');
+ break;
+ case '\b':
+ sb.append(SerializationConstants.BACK_SLASH).append('b');
+ break;
+ case '\f':
+ sb.append(SerializationConstants.BACK_SLASH).append('f');
+ break;
+ case '"':
+ sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.QUOTE);
+ break;
+ case '\\':
+ sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.BACK_SLASH);
+ break;
+ default:
+ if (c <= 0x1F || c == 0x7F) {
+ sb.append(String.format("\\u%04X", (int) c));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Validates RDF values before serialization to ensure they conform to N-Quads rules.
+ *
+ * @param value The {@link Value} to validate.
+ * @throws IllegalArgumentException if the value is null or invalid.
+ */
+ private void validateValue(Value value) {
+ if (value == null) {
+ logger.warn("Encountered a null value where a non-null value was expected for N-Quads serialization.");
+ throw new IllegalArgumentException("Value cannot be null in N-Quads format");
+ }
+
+ if (value.isLiteral()) {
+ validateLiteral((Literal) value);
+ } else if (value.isIRI()) {
+ validateIRI((IRI) value);
+ }
+ }
+
+ /**
+ * Validates a {@link Literal} to ensure it conforms to RDF/N-Quads rules.
+ * Specifically checks for consistency between language tags and the rdf:langString datatype.
+ *
+ * @param literal The {@link Literal} to validate.
+ * @throws IllegalArgumentException if the literal is invalid (e.g., language tag with wrong datatype,
+ * or rdf:langString literal missing a language tag).
+ */
+ private void validateLiteral(Literal literal) {
+ IRI datatype = literal.getDatatype();
+
+
+ if (literal.getLanguage().isPresent()) {
+
+ if (datatype == null || !datatype.stringValue().equals(SerializationConstants.RDF_LANGSTRING)) {
+ throw new IllegalArgumentException(
+ "Literal with language tag must use rdf:langString datatype. Found: " + (datatype != null ? datatype.stringValue() : "null"));
+ }
+ } else {
+
+ if (datatype != null && datatype.stringValue().equals(SerializationConstants.RDF_LANGSTRING)) {
+ throw new IllegalArgumentException(
+ "rdf:langString literal must have a language tag.");
+ }
+ }
+ }
+
+ /**
+ * Validates an {@link IRI} to ensure it conforms to N-Quads rules.
+ * Checks if the IRI string contains characters that are not allowed in N-Quads
+ * unescaped form, such as spaces.
+ *
+ * @param iri The {@link IRI} to validate.
+ * @throws IllegalArgumentException if the IRI contains spaces or is otherwise invalid.
+ */
+ private void validateIRI(IRI iri) {
+ if (iri.stringValue().contains(SerializationConstants.SPACE)) {
+ throw new IllegalArgumentException("IRI contains spaces, which is not allowed in N-Quads unescaped form: " + iri.stringValue());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java
new file mode 100644
index 000000000..fc20c0e92
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java
@@ -0,0 +1,305 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.util.SerializationConstants;
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.util.Objects;
+
+/**
+ * Serializes a Corese {@link Model} into N-Triples format.
+ * This class provides a method to write the statements of a model to a given {@link Writer}
+ * according to the N-Triples specification.
+ */
+public class NTriplesFormat implements FormatSerializer {
+
+ /**
+ * Logger for this class, used for logging potential issues or information during serialization.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(NTriplesFormat.class);
+
+ private final Model model;
+ private final FormatConfig config;
+
+ /**
+ * Constructs a new {@code NTriplesFormat} instance with the specified model and default configuration.
+ *
+ * @param model the {@link Model} to be serialized. Must not be null.
+ * @throws NullPointerException if the provided model is null.
+ */
+ public NTriplesFormat(Model model) {
+ this(model, new FormatConfig.Builder().build());
+ }
+
+ /**
+ * Constructs a new {@code NTriplesFormat} instance with the specified model and custom configuration.
+ *
+ * @param model the {@link Model} to be serialized. Must not be null.
+ * @param config the {@link FormatConfig} to use for serialization. Must not be null.
+ * @throws NullPointerException if the provided model or config is null.
+ */
+ public NTriplesFormat(Model model, FormatConfig config) {
+ this.model = Objects.requireNonNull(model, "Model cannot be null");
+ this.config = Objects.requireNonNull(config, "Configuration cannot be null");
+ }
+
+ /**
+ * Writes the model to the given writer in N-Triples format.
+ * Each statement in the model is written on a new line, terminated by a dot and a newline character.
+ *
+ * @param writer the {@link Writer} to which the N-Triples output will be written.
+ * @throws SerializationException if an I/O error occurs during writing or if invalid data is encountered.
+ */
+ @Override
+ public void write(Writer writer) throws SerializationException {
+ try {
+ for (Statement stmt : model) {
+ writeStatement(writer, stmt);
+ }
+ writer.flush();
+ } catch (IOException e) {
+ throw new SerializationException("Failed to write", "NTriples", e);
+ } catch (IllegalArgumentException e) {
+ throw new SerializationException("Invalid data: " + e.getMessage(), "NTriples", e);
+ }
+ }
+
+ /**
+ * Writes a single {@link Statement} to the writer in N-Triples format.
+ * The statement is written as "$subject $predicate $object ."
+ * N-Triples does not support contexts (named graphs). If a context is present, it's ignored and a warning is logged.
+ *
+ * @param writer the {@link Writer} to which the statement will be written.
+ * @param stmt the {@link Statement} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeStatement(Writer writer, Statement stmt) throws IOException {
+ writeValue(writer, stmt.getSubject());
+ writer.write(SerializationConstants.SPACE);
+ writeValue(writer, stmt.getPredicate());
+ writer.write(SerializationConstants.SPACE);
+ writeValue(writer, stmt.getObject());
+
+ Resource context = stmt.getContext();
+ if (context != null && logger.isWarnEnabled()) {
+ logger.warn("N-Triples format does not support named graphs. Context '{}' will be ignored for statement: {}",
+ context.stringValue(), stmt);
+
+ }
+
+ writer.write(SerializationConstants.SPACE_POINT);
+ }
+
+ /**
+ * Writes a single {@link Value} to the writer.
+ * Handles literals, blank nodes, and IRIs.
+ *
+ * @param writer the {@link Writer} to which the value will be written.
+ * @param value the {@link Value} to write.
+ * @throws IOException if an I/O error occurs.
+ * @throws IllegalArgumentException if the provided value is null or an unsupported type.
+ */
+ private void writeValue(Writer writer, Value value) throws IOException {
+ validateValue(value);
+
+ if (value.isLiteral()) {
+ writeLiteral(writer, (Literal) value);
+ } else if (value.isResource()) {
+ if (value.isIRI()) {
+ writeIRI(writer, (IRI) value);
+ } else if (value.isBNode()) {
+ writeBlankNode(writer, (Resource) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported resource type for N-Triples serialization: " + value.getClass().getName());
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported value type for N-Triples serialization: " + value.getClass().getName());
+ }
+ }
+
+ /**
+ * Writes a {@link Literal} to the writer in N-Triples format.
+ * Handles plain literals, language-tagged literals, and typed literals.
+ *
+ * @param writer the {@link Writer} to which the literal will be written.
+ * @param literal the {@link Literal} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeLiteral(Writer writer, Literal literal) throws IOException {
+ writer.write(SerializationConstants.QUOTE);
+ writer.write(escapeLiteral(literal.stringValue()));
+ writer.write(SerializationConstants.QUOTE);
+
+ literal.getLanguage().ifPresent(lang -> {
+ try {
+ writer.write(SerializationConstants.AT_SIGN + lang);
+ } catch (IOException e) {
+
+ throw new UncheckedIOException("Error writing language tag to stream", e);
+ }
+ });
+
+ if (!literal.getLanguage().isPresent()) {
+ IRI datatype = literal.getDatatype();
+ if (datatype != null && !datatype.stringValue().equals(SerializationConstants.XSD_STRING)) {
+ writer.write(SerializationConstants.DATATYPE_SEPARATOR);
+ writeIRI(writer, datatype);
+ }
+ }
+ }
+
+ /**
+ * Writes an {@link IRI} to the writer.
+ * The IRI's string representation must be enclosed in angle brackets for N-Triples.
+ *
+ * @param writer the {@link Writer} to which the IRI will be written.
+ * @param iri the {@link IRI} to write.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeIRI(Writer writer, IRI iri) throws IOException {
+ writer.write(SerializationConstants.LT);
+ writer.write(escapeIRI(iri.stringValue()));
+ writer.write(SerializationConstants.GT);
+ }
+
+ /**
+ * Writes a blank node to the writer.
+ * Blank nodes are prefixed with "_:", and the identifier is appended.
+ *
+ * @param writer the {@link Writer} to which the blank node will be written.
+ * @param blankNode the {@link Resource} representing the blank node.
+ * @throws IOException if an I/O error occurs.
+ */
+ private void writeBlankNode(Writer writer, Resource blankNode) throws IOException {
+ writer.write(config.getBlankNodePrefix());
+ writer.write(blankNode.stringValue());
+ }
+
+ /**
+ * Validates and potentially escapes an IRI string.
+ * Throws an {@link IllegalArgumentException} if the IRI contains characters
+ * that are not allowed in N-Triples unescaped form (like spaces, quotes, angle brackets).
+ *
+ * @param iri The string value of the IRI to validate and escape.
+ * @return The validated and potentially escaped IRI string.
+ * @throws IllegalArgumentException if the IRI string is invalid.
+ */
+ private String escapeIRI(String iri) {
+
+ if (iri.contains(SerializationConstants.SPACE) || iri.contains(SerializationConstants.QUOTE) || iri.contains(SerializationConstants.LT) || iri.contains(SerializationConstants.GT)) {
+ throw new IllegalArgumentException("Invalid IRI: contains illegal characters for N-Triples unescaped form: " + iri);
+ }
+ return iri;
+ }
+
+ /**
+ * Escape special characters in N-Triples string literals.
+ * Handles backslash, double quote, and common control characters.
+ * Unicode escape sequences are used for unprintable characters.
+ *
+ * @param value The string value of the literal to escape.
+ * @return The escaped string suitable for N-Triples literal.
+ */
+ private String escapeLiteral(String value) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '\n':
+ sb.append(SerializationConstants.BACK_SLASH).append('n');
+ break;
+ case '\r':
+ sb.append(SerializationConstants.BACK_SLASH).append('r');
+ break;
+ case '\t':
+ sb.append(SerializationConstants.BACK_SLASH).append('t');
+ break;
+ case '\b': // backspace
+ sb.append(SerializationConstants.BACK_SLASH).append('b');
+ break;
+ case '\f': // form feed
+ sb.append(SerializationConstants.BACK_SLASH).append('f');
+ break;
+ case '"':
+ sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.QUOTE);
+ break;
+ case '\\':
+ sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.BACK_SLASH);
+ break;
+ default:
+ if (c <= 0x1F || c == 0x7F) {
+ sb.append(String.format("\\u%04X", (int) c));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Validates RDF values before serialization to ensure they conform to N-Triples rules.
+ *
+ * @param value The {@link Value} to validate.
+ * @throws IllegalArgumentException if the value is null or invalid.
+ */
+ private void validateValue(Value value) {
+ if (value == null) {
+ logger.warn("Encountered a null value where a non-null value was expected for N-Triples serialization.");
+ throw new IllegalArgumentException("Value cannot be null in N-Triples format");
+ }
+
+ if (value.isLiteral()) {
+ validateLiteral((Literal) value);
+ } else if (value.isIRI()) {
+ validateIRI((IRI) value);
+ }
+
+ }
+
+ /**
+ * Validates a {@link Literal} to ensure it conforms to RDF/N-Triples rules.
+ * Specifically checks for consistency between language tags and the rdf:langString datatype.
+ *
+ * @param literal The {@link Literal} to validate.
+ * @throws IllegalArgumentException if the literal is invalid (e.g., language tag with wrong datatype,
+ * or rdf:langString literal missing a language tag).
+ */
+ private void validateLiteral(Literal literal) {
+ IRI datatype = literal.getDatatype();
+
+
+ if (literal.getLanguage().isPresent()) {
+
+ if (datatype == null || !datatype.stringValue().equals(SerializationConstants.RDF_LANGSTRING)) {
+ throw new IllegalArgumentException(
+ "Literal with language tag must use rdf:langString datatype. Found: " + (datatype != null ? datatype.stringValue() : "null"));
+ }
+ } else {
+
+ if (datatype != null && datatype.stringValue().equals(SerializationConstants.RDF_LANGSTRING)) {
+ throw new IllegalArgumentException(
+ "rdf:langString literal must have a language tag.");
+ }
+ }
+ }
+
+ /**
+ * Validates an {@link IRI} to ensure it conforms to N-Triples rules.
+ * Checks if the IRI string contains characters that are not allowed in N-Triples
+ * unescaped form, such as spaces.
+ *
+ * @param iri The {@link IRI} to validate.
+ * @throws IllegalArgumentException if the IRI contains spaces or is otherwise invalid.
+ */
+ private void validateIRI(IRI iri) {
+ if (iri.stringValue().contains(SerializationConstants.SPACE)) {
+ throw new IllegalArgumentException("IRI contains spaces, which is not allowed in N-Triples unescaped form: " + iri.stringValue());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java
new file mode 100644
index 000000000..fbea74d93
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java
@@ -0,0 +1,176 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Describes a semantic RDF serialization format, extending file-level metadata
+ * with RDF-specific capabilities.
+ * This class also acts as a central registry for all known RDF formats,
+ * providing static constants for common formats and utility methods for lookup.
+ */
+public class RdfFormat extends FileFormat {
+
+ private final boolean supportsNamespaces;
+ private final boolean supportsNamedGraphs;
+
+ public static final RdfFormat TURTLE = new RdfFormat(
+ "Turtle",
+ List.of("ttl"),
+ List.of("text/turtle"),
+ true,
+ false);
+
+
+ public static final RdfFormat NTRIPLES = new RdfFormat(
+ "N-Triples",
+ List.of("nt"),
+ List.of("application/n-triples", "text/plain"),
+ false,
+ false);
+
+ public static final RdfFormat NQUADS = new RdfFormat(
+ "N-Quads",
+ List.of("nq"),
+ List.of("application/n-quads"),
+ false,
+ true);
+
+ public static final RdfFormat JSONLD = new RdfFormat(
+ "JSON-LD",
+ List.of("jsonld"),
+ List.of("application/ld+json", "application/json"),
+ true,
+ true);
+
+ public static final RdfFormat RDFXML = new RdfFormat(
+ "RDF/XML",
+ List.of("rdf", "xml"),
+ List.of("application/rdf+xml"),
+ true,
+ false);
+
+ /**
+ * Constructs a new RDF format.
+ *
+ * @param name The name of the format (e.g., "Turtle").
+ * @param extensions File extensions for this format (e.g., ["ttl"]).
+ * @param mimeTypes MIME types for this format (e.g.,
+ * ["text/turtle"]).
+ * @param supportsNamespaces Whether the format supports prefixes/namespaces.
+ * @param supportsNamedGraphs Whether the format supports named graphs.
+ * serialization.
+ */
+ public RdfFormat(
+ String name,
+ List extensions,
+ List mimeTypes,
+ boolean supportsNamespaces,
+ boolean supportsNamedGraphs) {
+ super(name, extensions, mimeTypes);
+ this.supportsNamespaces = supportsNamespaces;
+ this.supportsNamedGraphs = supportsNamedGraphs;
+ }
+
+ /**
+ * Whether the format supports RDF prefixes (e.g., Turtle's @prefix declarations).
+ *
+ * @return true if the format supports explicit namespace declarations, false otherwise.
+ */
+ public boolean supportsNamespaces() {
+ return supportsNamespaces;
+ }
+
+ /**
+ * Whether the format supports named graphs (e.g., TriG, N-Quads).
+ */
+ public boolean supportsNamedGraphs() {
+ return supportsNamedGraphs;
+ }
+
+
+ /**
+ * Finds a known RDF format by its name (case-insensitive).
+ *
+ * @param name The name of the format (e.g., "Turtle").
+ * @return An Optional containing the matching RdfFormat if found.
+ */
+ public static Optional byName(String name) {
+ String n = name.toLowerCase(Locale.ROOT);
+ return all().stream()
+ .filter(format -> format.getName().equalsIgnoreCase(n))
+ .findFirst();
+ }
+
+ /**
+ * Finds a known RDF format by file extension (case-insensitive).
+ *
+ * @param extension The file extension (e.g., "ttl").
+ * @return An Optional containing the matching RdfFormat if found.
+ */
+ public static Optional byExtension(String extension) {
+ String ext = extension.toLowerCase(Locale.ROOT);
+ return all().stream()
+ .filter(format -> format.getExtensions().stream()
+ .anyMatch(e -> e.equalsIgnoreCase(ext)))
+ .findFirst();
+ }
+
+ /**
+ * Finds a known RDF format by MIME type (case-insensitive).
+ *
+ * @param mimeType The MIME type (e.g., "text/turtle").
+ * @return An Optional containing the matching RdfFormat if found.
+ */
+ public static Optional byMimeType(String mimeType) {
+ String mime = mimeType.toLowerCase(Locale.ROOT);
+ return all().stream()
+ .filter(format -> format.getMimeTypes().stream()
+ .anyMatch(m -> m.equalsIgnoreCase(mime)))
+ .findFirst();
+ }
+
+ /**
+ * Returns a list of all known RDF formats.
+ *
+ * @return An unmodifiable List of all RdfFormat constants.
+ */
+ public static List all() {
+ return List.of(TURTLE, NTRIPLES, NQUADS, JSONLD, RDFXML);
+ }
+
+ @Override
+ public String toString() {
+ return "%s [extensions: %s, mimeTypes: %s, prefixes: %s, namedGraphs: %s]".formatted(
+ getName(),
+ String.join(", ", getExtensions()),
+ String.join(", ", getMimeTypes()),
+ supportsNamespaces(),
+ supportsNamedGraphs());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (!(obj instanceof RdfFormat other))
+ return false;
+ return getName().equalsIgnoreCase(other.getName())
+ && getExtensions().equals(other.getExtensions())
+ && getMimeTypes().equals(other.getMimeTypes())
+ && supportsNamespaces == other.supportsNamespaces
+ && supportsNamedGraphs == other.supportsNamedGraphs;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getName().toLowerCase(),
+ getExtensions(),
+ getMimeTypes(),
+ supportsNamespaces,
+ supportsNamedGraphs);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/Serializer.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/Serializer.java
new file mode 100644
index 000000000..37427b832
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/Serializer.java
@@ -0,0 +1,55 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import fr.inria.corese.core.next.api.FormatSerializer;
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+
+import java.io.Writer;
+import java.util.Objects;
+
+public class Serializer {
+
+ private final Model model;
+ private final FormatConfig config;
+
+ public Serializer(Model model) {
+ this(model, new FormatConfig.Builder().build());
+ }
+
+ public Serializer(Model model, FormatConfig config) {
+ this.model = Objects.requireNonNull(model, "Model cannot be null for serialization");
+ this.config = Objects.requireNonNull(config, "FormatConfig cannot be null for serialization");
+ }
+
+ /**
+ * Serializes the RDF model to the given writer in the specified {@link RdfFormat}.
+ *
+ * @param writer the Writer to write the serialized data to.
+ * @param format the {@link RdfFormat} describing the desired serialization format.
+ * @throws SerializationException if an error occurs during serialization or if the format is not currently supported by an implementation.
+ */
+ public void serialize(Writer writer, RdfFormat format) throws SerializationException {
+ Objects.requireNonNull(writer, "Writer cannot be null");
+ Objects.requireNonNull(format, "RdfFormat cannot be null");
+
+ FormatSerializer formatSerializer;
+
+
+ if (format.equals(RdfFormat.NTRIPLES)) {
+ formatSerializer = new NTriplesFormat(model, config);
+ } else if (format.equals( RdfFormat.NQUADS)) {
+ formatSerializer = new NQuadsFormat(model, config);
+ } else if (format.equals( RdfFormat.TURTLE)) {
+
+ throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented.");
+ } else if (format.equals( RdfFormat.JSONLD)) {
+ throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented.");
+ } else if (format.equals( RdfFormat.RDFXML)) {
+ throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented.");
+ } else {
+ throw new IllegalArgumentException("Unknown or unsupported RdfFormat: " + format.getName());
+ }
+
+ formatSerializer.write(writer);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java b/src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java
new file mode 100644
index 000000000..54dfdca54
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java
@@ -0,0 +1,83 @@
+package fr.inria.corese.core.next.impl.common.util;
+
+/**
+ * Provides a collection of constant strings used during the serialization process
+ * for various RDF formats. These constants aim to
+ * centralize common literal values to ensure consistency and maintainability
+ * across different serialization utilities.
+ */
+public class SerializationConstants {
+
+ /**
+ * Represents a single space character (" ").
+ * Used for separating elements within an RDF triple or quad.
+ */
+ public static final String SPACE = " ";
+
+ /**
+ * Represents the termination sequence for an RDF statement format:
+ * a space, a dot, and a newline character (" .\n").
+ */
+ public static final String SPACE_POINT = " .\n";
+
+ /**
+ * Represents a single dot character (".").
+ * Used as a terminator for RDF statements.
+ */
+ public static final String POINT = ".";
+
+ /**
+ * Represents the less-than sign ("<").
+ * Used to enclose URIs in N-Triples and N-Quads.
+ */
+ public static final String LT = "<";
+
+ /**
+ * Represents the greater-than sign (">").
+ * Used to enclose URIs.
+ */
+ public static final String GT = ">";
+
+ /**
+ * Represents the standard prefix for blank nodes ("_:").
+ * Used to identify blank nodes.
+ */
+ public static final String BNODE_PREFIX = "_:";
+
+ /**
+ * Represents a double-quote character ("\"").
+ * Used to enclose literal values.
+ */
+ public static final String QUOTE = "\"";
+ /**
+ * Represents a backslash character ("\\").
+ * Used for escaping special characters within string literals.
+ */
+ public static final String BACK_SLASH = "\\";
+
+ /**
+ * The URI string for the XML Schema `xsd:string` datatype:
+ * {@code http://www.w3.org/2001/XMLSchema#string}.
+ */
+ public static final String XSD_STRING = "http://www.w3.org/2001/XMLSchema#string";
+
+ /**
+ * The URI string for the RDF `rdf:langString` datatype:
+ * {@code http://www.w3.org/1999/02/22-rdf-syntax-ns#langString}.
+ */
+ public static final String RDF_LANGSTRING = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
+
+ // Nouvelle constante pour le préfixe de la balise de langue
+ public static final String AT_SIGN = "@";
+
+ // Nouvelle constante pour le séparateur de datatype
+ public static final String DATATYPE_SEPARATOR = "^^";
+
+
+ /**
+ * Private constructor to prevent instantiation of this utility class.
+ * All members are static.
+ */
+ private SerializationConstants() {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java b/src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java
new file mode 100644
index 000000000..710ba0c93
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java
@@ -0,0 +1,50 @@
+package fr.inria.corese.core.next.impl.exception;
+
+/**
+ * Exception levée lors d'échecs de sérialisation/désérialisation RDF.
+ * Peut contenir des détails spécifiques au format (NTriples, JSON-LD, etc.).
+ */
+public class SerializationException extends Exception {
+ private final String formatName;
+ private final int lineNumber;
+ private final int columnNumber;
+
+ public SerializationException(String message, String formatName, Throwable cause) {
+ this(message, formatName, -1, -1, cause);
+ }
+
+
+ public SerializationException(String message, String formatName, int lineNumber, int columnNumber, Throwable cause) {
+ super(buildMessage(message, formatName, lineNumber, columnNumber), cause);
+ this.formatName = formatName;
+ this.lineNumber = lineNumber;
+ this.columnNumber = columnNumber;
+ }
+
+ private static String buildMessage(String base, String format, int line, int col) {
+ StringBuilder sb = new StringBuilder(base);
+ if (!"unknown".equals(format)) {
+ sb.append(" [Format: ").append(format).append("]");
+ }
+ if (line > 0) {
+ sb.append(" at line ").append(line);
+ if (col > 0) {
+ sb.append(":").append(col);
+ }
+ }
+ return sb.toString();
+ }
+
+
+ public String getFormatName() {
+ return formatName;
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ public int getColumnNumber() {
+ return columnNumber;
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FileFormatTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FileFormatTest.java
new file mode 100644
index 000000000..2e3308e18
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FileFormatTest.java
@@ -0,0 +1,209 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Unit tests for {@link FileFormat}.
+ *
+ *
+ * Coverage :
+ *
+ *
+ * - Valid constructor & getters (parametrised)
+ * - Null / empty argument validation
+ * - Immutability of internal collections
+ * - equals / hashCode full contract (symmetry, transitivity, null & type
+ * safety)
+ * - Meaningful toString()
+ *
+ */
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+class FileFormatTest {
+
+ /*
+ * ---------------------------------------------------------- *
+ * Test data (re-usable across parameterised tests) *
+ * ----------------------------------------------------------
+ */
+ private static Stream validFormats() {
+ return Stream.of(
+ Arguments.of("Turtle", List.of("ttl"), List.of("text/turtle")),
+ Arguments.of("N-Triples", List.of("nt"), List.of("application/n-triples")),
+ Arguments.of("JSON-LD", List.of("jsonld"), List.of("application/ld+json")),
+ Arguments.of("RDF/XML", List.of("rdf", "xml"), List.of("application/rdf+xml")),
+ Arguments.of("TriG", List.of("trig"), List.of("application/trig")));
+ }
+
+ /*
+ * ---------------------------------------------------------- *
+ * Constructor & getters *
+ * ----------------------------------------------------------
+ */
+ @ParameterizedTest(name = "Create FileFormat: {0}")
+ @MethodSource("validFormats")
+ @DisplayName("Constructor populates fields correctly")
+ void constructor_sets_all_fields(String name,
+ List extensions,
+ List mimeTypes) {
+
+ FileFormat format = new FileFormat(name, extensions, mimeTypes);
+
+ assertAll("All getters reflect constructor arguments",
+ () -> assertEquals(name, format.getName(), "name"),
+ () -> assertEquals(extensions, format.getExtensions(), "extensions"),
+ () -> assertEquals(mimeTypes, format.getMimeTypes(), "mimeTypes"),
+ () -> assertEquals(extensions.get(0), format.getDefaultExtension(), "defaultExtension"),
+ () -> assertEquals(mimeTypes.get(0), format.getDefaultMimeType(), "defaultMimeType"));
+ }
+
+ /*
+ * ---------------------------------------------------------- *
+ * Validation of constructor arguments *
+ * ----------------------------------------------------------
+ */
+ @Nested
+ class Constructor_argument_validation {
+
+ private final List EXT = List.of("ttl");
+ private final List MIME = List.of("text/turtle");
+
+ @Test
+ void null_name_throws_NPE() {
+ assertThrows(NullPointerException.class,
+ () -> new FileFormat(null, EXT, MIME));
+ }
+
+ @Test
+ void null_extensions_throws_NPE() {
+ assertThrows(NullPointerException.class,
+ () -> new FileFormat("Turtle", null, MIME));
+ }
+
+ @Test
+ void null_mimeTypes_throws_NPE() {
+ assertThrows(NullPointerException.class,
+ () -> new FileFormat("Turtle", EXT, null));
+ }
+
+ @Test
+ void empty_extensions_throws_IAE() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new FileFormat("Turtle", List.of(), MIME));
+ }
+
+ @Test
+ void empty_mimeTypes_throws_IAE() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new FileFormat("Turtle", EXT, List.of()));
+ }
+ }
+
+ /*
+ * ---------------------------------------------------------- *
+ * Immutability & defensive copies *
+ * ----------------------------------------------------------
+ */
+ @Test
+ void internal_lists_are_immutable_and_defensively_copied() {
+ // Build mutable lists
+ List ext = new ArrayList<>(List.of("ttl"));
+ List mime = new ArrayList<>(List.of("text/turtle"));
+ FileFormat format = new FileFormat("Turtle", ext, mime);
+
+ // Mutate originals AFTER construction
+ ext.add("bad");
+ mime.add("bad/type");
+
+ // Verify defensive copy & immutability
+ assertEquals(List.of("ttl"), format.getExtensions(), "defensive copy for extensions");
+ assertEquals(List.of("text/turtle"), format.getMimeTypes(), "defensive copy for mimeTypes");
+
+ // Returned lists must be unmodifiable
+ assertAll("Returned lists are unmodifiable",
+ () -> assertThrows(UnsupportedOperationException.class,
+ () -> format.getExtensions().add("new")),
+ () -> assertThrows(UnsupportedOperationException.class,
+ () -> format.getMimeTypes().add("new/type")));
+ }
+
+ /*
+ * ---------------------------------------------------------- *
+ * equals & hashCode contract *
+ * ----------------------------------------------------------
+ */
+ @Nested
+ class Equals_and_hashCode_contract {
+
+ private final FileFormat base = new FileFormat("Turtle", List.of("ttl"), List.of("text/turtle"));
+
+ @Test
+ void symmetry_and_case_insensitivity() {
+ FileFormat sameDifferentCase = new FileFormat("tUrTlE", List.of("ttl"), List.of("text/turtle"));
+
+ assertEquals(base, sameDifferentCase);
+ assertEquals(sameDifferentCase, base);
+ assertEquals(base.hashCode(), sameDifferentCase.hashCode());
+ }
+
+ @Test
+ void transitivity() {
+ FileFormat a = new FileFormat("Turtle", List.of("ttl"), List.of("text/turtle"));
+ FileFormat b = new FileFormat("TURTLE", List.of("ttl"), List.of("text/turtle"));
+ FileFormat c = new FileFormat("turtle", List.of("ttl"), List.of("text/turtle"));
+
+ assertAll(
+ () -> assertEquals(a, b),
+ () -> assertEquals(b, c),
+ () -> assertEquals(a, c));
+ }
+
+ @Test
+ void inequality_when_any_field_differs() {
+ FileFormat diffName = new FileFormat("N-Triples", List.of("ttl"), List.of("text/turtle"));
+ FileFormat diffExt = new FileFormat("Turtle", List.of("nt"), List.of("text/turtle"));
+ FileFormat diffMime = new FileFormat("Turtle", List.of("ttl"), List.of("application/n-triples"));
+
+ assertAll(
+ () -> assertNotEquals(base, diffName),
+ () -> assertNotEquals(base, diffExt),
+ () -> assertNotEquals(base, diffMime),
+ () -> assertNotEquals(base, null),
+ () -> assertNotEquals(base, "some string"));
+ }
+ }
+
+ /*
+ * ---------------------------------------------------------- *
+ * toString() *
+ * ----------------------------------------------------------
+ */
+ @Test
+ void toString_contains_all_relevant_information() {
+ FileFormat format = new FileFormat("Turtle", List.of("ttl"), List.of("text/turtle"));
+
+ String out = format.toString();
+ assertTrue(
+ Pattern.compile("Turtle", Pattern.CASE_INSENSITIVE).matcher(out).find(),
+ "name is present");
+ assertTrue(out.contains("ttl"), "extension is present");
+ assertTrue(out.contains("text/turtle"), "mime type is present");
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java
new file mode 100644
index 000000000..c1ee05f95
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java
@@ -0,0 +1,56 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class FormatConfigTest {
+
+ @Test
+ @DisplayName("Builder should create FormatConfig with default blank node prefix")
+ void builderShouldCreateWithDefaultBlankNodePrefix() {
+
+ FormatConfig config = new FormatConfig.Builder().build();
+
+
+ assertNotNull(config, "FormatConfig should not be null");
+ assertEquals("_:", config.getBlankNodePrefix(), "Default blank node prefix should be '_:'");
+ }
+
+ @Test
+ @DisplayName("Builder should create FormatConfig with custom blank node prefix")
+ void builderShouldCreateWithCustomBlankNodePrefix() {
+ String customPrefix = "genid-";
+
+
+ FormatConfig config = new FormatConfig.Builder()
+ .blankNodePrefix(customPrefix)
+ .build();
+
+
+ assertNotNull(config, "FormatConfig should not be null");
+ assertEquals(customPrefix, config.getBlankNodePrefix(), "Blank node prefix should match the custom value");
+ }
+
+ @Test
+ @DisplayName("blankNodePrefix method in Builder should throw NullPointerException for null prefix")
+ void blankNodePrefixShouldThrowForNull() {
+
+ FormatConfig.Builder builder = new FormatConfig.Builder();
+
+
+ assertThrows(NullPointerException.class, () -> builder.blankNodePrefix(null),
+ "Setting a null blank node prefix should throw NullPointerException");
+ }
+
+ @Test
+ @DisplayName("FormatConfig constructor should be private and only accessible via builder")
+ void constructorIsPrivateAndAccessibleViaBuilder() {
+
+ FormatConfig config = new FormatConfig.Builder().build();
+ assertNotNull(config);
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormatTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormatTest.java
new file mode 100644
index 000000000..5d1a3636d
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormatTest.java
@@ -0,0 +1,457 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+class NQuadsFormatTest {
+
+ private Model model;
+ private FormatConfig config;
+ private NQuadsFormat nQuadsFormat;
+
+ private Resource mockExPerson;
+ private IRI mockExName;
+
+ private IRI mockExKnows;
+ private IRI mockExContext;
+
+
+ private final String lexJohn = "John Doe";
+
+ private final String hello = "Hello";
+
+ private Literal mockLiteralJohn;
+ private Literal mockLiteralHelloEn;
+ private Resource mockBNode1;
+ private Resource mockBNode2;
+
+ @BeforeEach
+ void setUp() {
+ model = mock(Model.class);
+ config = new FormatConfig.Builder().build();
+ nQuadsFormat = new NQuadsFormat(model, config);
+
+ mockExPerson = createIRI("http://example.org/Person");
+ mockExName = createIRI("http://example.org/name");
+
+ mockExKnows = createIRI("http://example.org/knows");
+
+
+ mockLiteralJohn = createLiteral(lexJohn, null, null);
+ mockLiteralHelloEn = createLiteral(hello, null, "en");
+
+ mockBNode1 = createBlankNode("b1");
+ mockBNode2 = createBlankNode("b2");
+ mockExContext = createIRI("http://example.org/myGraph");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null model")
+ void constructorShouldThrowForNullModel() {
+ assertThrows(NullPointerException.class, () -> new NQuadsFormat(null), "Model cannot be null");
+ assertThrows(NullPointerException.class, () -> new NQuadsFormat(null, config), "Model cannot be null");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null config")
+ void constructorShouldThrowForNullConfig() {
+ assertThrows(NullPointerException.class, () -> new NQuadsFormat(model, null), "Configuration cannot be null");
+ }
+
+ @Test
+ @DisplayName("Write should serialize simple statement correctly (default graph)")
+ void writeShouldSerializeSimpleStatement() throws SerializationException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+
+ String expected = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNQuadsString(lexJohn)) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should serialize statement with context (named graph)")
+ void writeShouldSerializeStatementWithContext() throws SerializationException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn,
+ mockExContext
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+
+ String expected = String.format("<%s> <%s> \"%s\" <%s>",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNQuadsString(lexJohn),
+ mockExContext.stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle blank nodes with default prefix")
+ void writeShouldHandleBlankNodes() throws SerializationException {
+ Statement stmt = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockBNode2
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+ String expected = String.format("_:%s <%s> _:%s",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockBNode2.stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle blank nodes in context with default prefix")
+ void writeShouldHandleBlankNodesInContext() throws SerializationException {
+ Resource blankNodeContext = createBlankNode("b3");
+ Statement stmt = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockExPerson,
+ blankNodeContext
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+ String expected = String.format("_:%s <%s> <%s> _:%s",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockExPerson.stringValue(),
+ blankNodeContext.stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle blank nodes with custom prefix")
+ void writeShouldHandleBlankNodesWithCustomPrefix() throws SerializationException {
+ FormatConfig customConfig = new FormatConfig.Builder().blankNodePrefix("genid-").build();
+ NQuadsFormat customSerializer = new NQuadsFormat(model, customConfig);
+
+ Statement stmt = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockBNode2,
+ createBlankNode("b3")
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ customSerializer.write(writer);
+
+ String expected = String.format("genid-%s <%s> genid-%s genid-%s",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockBNode2.stringValue(),
+ createBlankNode("b3").stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on IO error")
+ void writeShouldThrowOnIOException() throws IOException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ Writer faultyWriter = mock(Writer.class);
+ doThrow(new IOException("Simulated IO error")).when(faultyWriter).write(anyString());
+
+ assertThrows(SerializationException.class, () -> nQuadsFormat.write(faultyWriter));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null subject value from Statement")
+ void writeShouldThrowOnNullSubjectValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(null);
+ when(stmt.getPredicate()).thenReturn(mockExName);
+ when(stmt.getObject()).thenReturn(mockLiteralJohn);
+ when(stmt.getContext()).thenReturn(null);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nQuadsFormat.write(writer));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null predicate value from Statement")
+ void writeShouldThrowOnNullPredicateValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(mockExPerson);
+ when(stmt.getPredicate()).thenReturn(null);
+ when(stmt.getObject()).thenReturn(mockLiteralJohn);
+ when(stmt.getContext()).thenReturn(null);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nQuadsFormat.write(writer));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null object value from Statement")
+ void writeShouldThrowOnNullObjectValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(mockExPerson);
+ when(stmt.getPredicate()).thenReturn(mockExName);
+ when(stmt.getObject()).thenReturn(null);
+ when(stmt.getContext()).thenReturn(null);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nQuadsFormat.write(writer));
+ }
+
+ @Test
+ @DisplayName("Write should correctly handle null context (default graph)")
+ void writeShouldHandleNullContext() throws SerializationException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn,
+ null
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+
+ String expected = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNQuadsString(lexJohn)) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "simple literal",
+ "literal with \"quotes\"",
+ "literal with \\ backslash",
+ "literal with \n newline",
+ "literal with \t tab",
+ "literal with \r carriage return",
+ "literal with \u0001 (SOH)",
+ "literal with \u007F (DEL)"
+ })
+ @DisplayName("Write should handle various literal values with proper escaping")
+ void writeShouldHandleVariousLiterals(String literalValue) throws SerializationException {
+
+ Literal literalMock = createLiteral(literalValue, null, null);
+
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ literalMock
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nQuadsFormat.write(writer);
+
+
+ String expectedEscapedLiteral = escapeNQuadsString(literalValue);
+ String expectedOutput = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ expectedEscapedLiteral) + " .\n";
+
+ assertEquals(expectedOutput, writer.toString());
+ }
+
+
+ @Test
+ @DisplayName("Should handle literals with language tags")
+ void shouldHandleLiteralsWithLanguageTags() throws SerializationException {
+ Statement stmt = createStatement(mockExPerson, createIRI("http://example.org/greeting"), mockLiteralHelloEn);
+
+ Model currentTestModel = mock(Model.class);
+ when(currentTestModel.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ Writer writer = new StringWriter();
+ NQuadsFormat serializer = new NQuadsFormat(currentTestModel);
+ serializer.write(writer);
+
+ String expectedOutput = String.format("<%s> <%s> \"%s\"@%s",
+ mockExPerson.stringValue(),
+ createIRI("http://example.org/greeting").stringValue(),
+ escapeNQuadsString(hello),
+ mockLiteralHelloEn.getLanguage().get()) + " .\n";
+
+ assertEquals(expectedOutput, writer.toString());
+ }
+
+
+ /**
+ * Creates a mocked Literal object.
+ * Important: The `lexicalForm` is the *raw string value* of the literal,
+ * without N-Quads specific quotes, lang tags, or datatype URIs.
+ * The `NQuadsFormat` class is responsible for adding those.
+ *
+ * @param lexicalForm The raw string value of the literal (e.g., "hello", "123").
+ * @param dataTypeIRI The IRI of the literal's datatype (e.g., XSD.INTEGER.getIRI()), or null for plain/lang-tagged.
+ * @param langTag The language tag (e.g., "en"), or null if not language-tagged.
+ * @return A mocked Literal instance.
+ */
+ private Literal createLiteral(String lexicalForm, IRI dataTypeIRI, String langTag) {
+ Literal literal = mock(Literal.class);
+ when(literal.isLiteral()).thenReturn(true);
+ when(literal.isResource()).thenReturn(false);
+ when(literal.stringValue()).thenReturn(lexicalForm);
+
+ if (langTag != null && !langTag.isEmpty()) {
+ when(literal.getLanguage()).thenReturn(Optional.of(langTag));
+
+ when(literal.getDatatype()).thenReturn(RDF.langString.getIRI());
+ } else {
+ when(literal.getLanguage()).thenReturn(Optional.empty());
+ when(literal.getDatatype()).thenReturn(dataTypeIRI);
+ }
+ return literal;
+ }
+
+ /**
+ * Escapes a string according to N-Quads literal escaping rules.
+ * This helper is used in tests to construct the *expected* output strings.
+ * It mimics the behavior of NQuadsFormat's internal escapeLiteral method.
+ *
+ * @param s The string to escape.
+ * @return The escaped string.
+ */
+ private String escapeNQuadsString(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '\b': // backspace
+ sb.append("\\b");
+ break;
+ case '\f': // form feed
+ sb.append("\\f");
+ break;
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ default:
+ if (c >= '\u0000' && c <= '\u001F' || c == '\u007F') {
+ sb.append(String.format("\\u%04X", (int) c));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+
+ private static class MockStatementIterator implements Iterator {
+ private final Statement[] statements;
+ private int index = 0;
+
+ MockStatementIterator(Statement... statements) {
+ this.statements = statements;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < statements.length;
+ }
+
+ @Override
+ public Statement next() {
+ return statements[index++];
+ }
+ }
+
+ private Statement createStatement(Resource subject, IRI predicate, Value object) {
+ return createStatement(subject, predicate, object, null);
+ }
+
+ private Statement createStatement(Resource subject, IRI predicate, Value object, Resource context) {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(subject);
+ when(stmt.getPredicate()).thenReturn(predicate);
+ when(stmt.getObject()).thenReturn(object);
+ when(stmt.getContext()).thenReturn(context);
+ return stmt;
+ }
+
+ private Resource createBlankNode(String id) {
+ Resource blankNode = mock(Resource.class);
+ when(blankNode.isResource()).thenReturn(true);
+ when(blankNode.isBNode()).thenReturn(true);
+ when(blankNode.isIRI()).thenReturn(false);
+ when(blankNode.stringValue()).thenReturn(id);
+ return blankNode;
+ }
+
+ private IRI createIRI(String uri) {
+ IRI iri = mock(IRI.class);
+ when(iri.isResource()).thenReturn(true);
+ when(iri.isIRI()).thenReturn(true);
+ when(iri.isBNode()).thenReturn(false);
+ when(iri.stringValue()).thenReturn(uri);
+ return iri;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormatTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormatTest.java
new file mode 100644
index 000000000..e00d89d41
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormatTest.java
@@ -0,0 +1,434 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+class NTriplesFormatTest {
+
+ private Model model;
+ private FormatConfig config;
+ private NTriplesFormat nTriplesFormat;
+
+ private Resource mockExPerson;
+ private IRI mockExName;
+ private IRI mockExKnows;
+
+ private final String lexJohn = "John Doe";
+ private final String hello = "Hello";
+
+ private Literal mockLiteralJohn;
+
+ private Literal mockLiteralHelloEn;
+ private Resource mockBNode1;
+ private Resource mockBNode2;
+
+ @BeforeEach
+ void setUp() {
+ model = mock(Model.class);
+ config = new FormatConfig.Builder().build();
+ nTriplesFormat = new NTriplesFormat(model, config);
+
+
+ mockExPerson = createIRI("http://example.org/Person");
+ mockExName = createIRI("http://example.org/name");
+
+ mockExKnows = createIRI("http://example.org/knows");
+
+
+ mockLiteralJohn = createLiteral(lexJohn, null, null);
+
+ mockLiteralHelloEn = createLiteral(hello, null, "en");
+
+ mockBNode1 = createBlankNode("b1");
+ mockBNode2 = createBlankNode("b2");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null model")
+ void constructorShouldThrowForNullModel() {
+ assertThrows(NullPointerException.class, () -> new NTriplesFormat(null), "Model cannot be null");
+ assertThrows(NullPointerException.class, () -> new NTriplesFormat(null, config), "Model cannot be null");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null config")
+ void constructorShouldThrowForNullConfig() {
+ assertThrows(NullPointerException.class, () -> new NTriplesFormat(model, null), "Configuration cannot be null");
+ }
+
+ @Test
+ @DisplayName("Write should serialize simple statement correctly")
+ void writeShouldSerializeSimpleStatement() throws SerializationException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nTriplesFormat.write(writer);
+
+
+ String expected = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNTriplesString(lexJohn)) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should serialize statement with context but ignore it (N-Triples)")
+ void writeShouldSerializeStatementWithContext() throws SerializationException {
+ IRI mockContext = createIRI("http://example.org/ctx");
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn,
+ mockContext
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nTriplesFormat.write(writer);
+
+ String expected = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNTriplesString(lexJohn)) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle blank nodes with default prefix")
+ void writeShouldHandleBlankNodes() throws SerializationException {
+ Statement stmt = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockBNode2
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nTriplesFormat.write(writer);
+
+ String expected = String.format("_:%s <%s> _:%s",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockBNode2.stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle blank nodes with custom prefix")
+ void writeShouldHandleBlankNodesWithCustomPrefix() throws SerializationException {
+ FormatConfig customConfig = new FormatConfig.Builder().blankNodePrefix("genid-").build();
+ NTriplesFormat customSerializer = new NTriplesFormat(model, customConfig);
+
+ Statement stmt = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockBNode2
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ customSerializer.write(writer);
+
+ String expected = String.format("genid-%s <%s> genid-%s",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockBNode2.stringValue()) + " .\n";
+
+ assertEquals(expected, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on IO error")
+ void writeShouldThrowOnIOException() throws IOException {
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ mockLiteralJohn
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ Writer faultyWriter = mock(Writer.class);
+
+ doThrow(new IOException("Simulated IO error")).when(faultyWriter).write(anyString());
+
+ assertThrows(SerializationException.class, () -> nTriplesFormat.write(faultyWriter));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null subject value from Statement")
+ void writeShouldThrowOnNullSubjectValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(null);
+ when(stmt.getPredicate()).thenReturn(mockExName);
+ when(stmt.getObject()).thenReturn(mockLiteralJohn);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nTriplesFormat.write(writer));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null predicate value from Statement")
+ void writeShouldThrowOnNullPredicateValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(mockExPerson);
+ when(stmt.getPredicate()).thenReturn(null);
+ when(stmt.getObject()).thenReturn(mockLiteralJohn);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nTriplesFormat.write(writer));
+ }
+
+ @Test
+ @DisplayName("Write should throw SerializationException on null object value from Statement")
+ void writeShouldThrowOnNullObjectValue() {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(mockExPerson);
+ when(stmt.getPredicate()).thenReturn(mockExName);
+ when(stmt.getObject()).thenReturn(null);
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ assertThrows(SerializationException.class, () -> nTriplesFormat.write(writer));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "simple literal",
+ "literal with \"quotes\"",
+ "literal with \\ backslash",
+ "literal with \n newline",
+ "literal with \t tab",
+ "literal with \r carriage return",
+ "literal with \u0001 (SOH)",
+ "literal with \u007F (DEL)"
+ })
+ @DisplayName("Write should handle various literal values with proper escaping")
+ void writeShouldHandleVariousLiterals(String literalValue) throws SerializationException {
+ Literal literalMock = createLiteral(literalValue, null, null);
+
+ Statement stmt = createStatement(
+ mockExPerson,
+ mockExName,
+ literalMock
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ StringWriter writer = new StringWriter();
+ nTriplesFormat.write(writer);
+
+ String expectedEscapedLiteral = escapeNTriplesString(literalValue);
+ String expectedOutput = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ expectedEscapedLiteral) + " .\n";
+
+ assertEquals(expectedOutput, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Write should handle multiple statements")
+ void writeShouldHandleMultipleStatements() throws SerializationException {
+ Statement stmt1 = createStatement(
+ mockExPerson,
+ mockExName,
+ createLiteral("o1", null, null)
+ );
+ Statement stmt2 = createStatement(
+ mockBNode1,
+ mockExKnows,
+ mockExPerson,
+ createIRI("http://example.org/ctx")
+ );
+ when(model.iterator()).thenReturn(new MockStatementIterator(stmt1, stmt2));
+
+ StringWriter writer = new StringWriter();
+ nTriplesFormat.write(writer);
+
+ String expectedOutput = String.format("<%s> <%s> \"%s\"",
+ mockExPerson.stringValue(),
+ mockExName.stringValue(),
+ escapeNTriplesString("o1")) + " .\n" +
+ String.format("_:%s <%s> <%s>",
+ mockBNode1.stringValue(),
+ mockExKnows.stringValue(),
+ mockExPerson.stringValue()) + " .\n";
+
+ assertEquals(expectedOutput, writer.toString());
+ }
+
+ @Test
+ @DisplayName("Should handle literals with language tags")
+ void shouldHandleLiteralsWithLanguageTags() throws SerializationException {
+ Statement stmt = createStatement(mockExPerson, createIRI("http://example.org/greeting"), mockLiteralHelloEn);
+
+ Model currentTestModel = mock(Model.class);
+ when(currentTestModel.iterator()).thenReturn(new MockStatementIterator(stmt));
+
+ Writer writer = new StringWriter();
+ NTriplesFormat serializer = new NTriplesFormat(currentTestModel);
+ serializer.write(writer);
+
+ String expectedOutput = String.format("<%s> <%s> \"%s\"@%s",
+ mockExPerson.stringValue(),
+ createIRI("http://example.org/greeting").stringValue(),
+ escapeNTriplesString(hello),
+ mockLiteralHelloEn.getLanguage().get()) + " .\n";
+
+ assertEquals(expectedOutput, writer.toString());
+ }
+
+
+ /**
+ * Escapes a string according to N-Triples literal escaping rules.
+ * This helper is used in tests to construct the *expected* output strings.
+ * It mimics the behavior of NTriplesFormat's internal escapeLiteral method.
+ *
+ * @param s The string to escape.
+ * @return The escaped string.
+ */
+ private String escapeNTriplesString(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ default:
+ if (c >= '\u0000' && c <= '\u001F' || c == '\u007F') {
+ sb.append(String.format("\\u%04X", (int) c));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+
+ private static class MockStatementIterator implements Iterator {
+ private final Statement[] statements;
+ private int index = 0;
+
+ MockStatementIterator(Statement... statements) {
+ this.statements = statements;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < statements.length;
+ }
+
+ @Override
+ public Statement next() {
+ return statements[index++];
+ }
+ }
+
+
+ /**
+ * Creates a mocked Literal object.
+ * Important: The `lexicalForm` is the *raw string value* of the literal,
+ * without N-Triples specific quotes, lang tags, or datatype URIs.
+ * The `NTriplesFormat` class is responsible for adding those.
+ *
+ * @param lexicalForm The raw string value of the literal (e.g., "hello", "123").
+ * @param dataTypeIRI The IRI of the literal's datatype (e.g., XSD.INTEGER.getIRI()), or null for plain/lang-tagged.
+ * @param langTag The language tag (e.g., "en"), or null if not language-tagged.
+ * @return A mocked Literal instance.
+ */
+ private Literal createLiteral(String lexicalForm, IRI dataTypeIRI, String langTag) {
+ Literal literal = mock(Literal.class);
+ when(literal.isLiteral()).thenReturn(true);
+ when(literal.isResource()).thenReturn(false);
+ when(literal.stringValue()).thenReturn(lexicalForm);
+
+ if (langTag != null && !langTag.isEmpty()) {
+ when(literal.getLanguage()).thenReturn(Optional.of(langTag));
+
+
+ when(literal.getDatatype()).thenReturn(RDF.langString.getIRI());
+ } else {
+ when(literal.getLanguage()).thenReturn(Optional.empty());
+ when(literal.getDatatype()).thenReturn(dataTypeIRI);
+ }
+ return literal;
+ }
+
+ private Statement createStatement(Resource subject, IRI predicate, Value object) {
+ return createStatement(subject, predicate, object, null);
+ }
+
+ private Statement createStatement(Resource subject, IRI predicate, Value object, Resource context) {
+ Statement stmt = mock(Statement.class);
+ when(stmt.getSubject()).thenReturn(subject);
+ when(stmt.getPredicate()).thenReturn(predicate);
+ when(stmt.getObject()).thenReturn(object);
+ when(stmt.getContext()).thenReturn(context);
+ return stmt;
+ }
+
+ private Resource createBlankNode(String id) {
+ Resource blankNode = mock(Resource.class);
+ when(blankNode.isResource()).thenReturn(true);
+ when(blankNode.isBNode()).thenReturn(true);
+ when(blankNode.isIRI()).thenReturn(false);
+ when(blankNode.stringValue()).thenReturn(id);
+ return blankNode;
+ }
+
+ private IRI createIRI(String uri) {
+ IRI iri = mock(IRI.class);
+ when(iri.isResource()).thenReturn(true);
+ when(iri.isIRI()).thenReturn(true);
+ when(iri.isBNode()).thenReturn(false);
+ when(iri.stringValue()).thenReturn(uri);
+ return iri;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormatTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormatTest.java
new file mode 100644
index 000000000..8f0df66d0
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormatTest.java
@@ -0,0 +1,300 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RdfFormatTest {
+
+
+ @Test
+ @DisplayName("Constructor should correctly initialize fields and getters should return correct values")
+ void constructorAndGetters() {
+ String name = "TestFormat";
+ List extensions = List.of("tf", "testf");
+ List mimeTypes = List.of("application/x-test", "text/test");
+ boolean supportsNamespaces = true;
+ boolean supportsNamedGraphs = false;
+
+ RdfFormat format = new RdfFormat(name, extensions, mimeTypes, supportsNamespaces, supportsNamedGraphs);
+
+ assertEquals(name, format.getName(), "Name should match constructor argument");
+ assertEquals(extensions, format.getExtensions(), "Extensions list should match constructor argument");
+ assertEquals(mimeTypes, format.getMimeTypes(), "MIME types list should match constructor argument");
+ assertEquals(supportsNamespaces, format.supportsNamespaces(), "supportsNamespaces should match constructor argument");
+ assertEquals(supportsNamedGraphs, format.supportsNamedGraphs(), "supportsNamedGraphs should match constructor argument");
+
+ assertEquals("tf", format.getDefaultExtension(), "Default extension should be the first in list");
+ assertEquals("application/x-test", format.getDefaultMimeType(), "Default MIME type should be the first in list");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null name")
+ void constructorThrowsForNullName() {
+ List extensions = List.of("tf");
+ List mimeTypes = List.of("text/plain");
+ assertThrows(NullPointerException.class,
+ () -> new RdfFormat(null, extensions, mimeTypes, true, false),
+ "Constructor should throw NPE for null name");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null extensions list")
+ void constructorThrowsForNullExtensions() {
+ List mimeTypes = List.of("text/plain");
+ assertThrows(NullPointerException.class,
+ () -> new RdfFormat("Test", null, mimeTypes, true, false),
+ "Constructor should throw NPE for null extensions list");
+ }
+
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null mimeTypes list")
+ void constructorThrowsForNullMimeTypes() {
+ List extensions = List.of("tf");
+ assertThrows(NullPointerException.class,
+ () -> new RdfFormat("Test", extensions, null, true, false),
+ "Constructor should throw NPE for null mimeTypes list");
+ }
+
+
+ @Test
+ @DisplayName("toString() should return a meaningful string representation")
+ void testToString() {
+ RdfFormat format = RdfFormat.TURTLE;
+ String expected = "Turtle [extensions: ttl, mimeTypes: text/turtle, prefixes: true, namedGraphs: false]";
+
+ assertEquals(expected, format.toString(), "toString() should match expected format");
+ }
+
+ @Test
+ @DisplayName("equals() should return true for identical objects")
+ void equalsIdenticalObjects() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, false);
+
+ assertEquals(format1, format1, "Object should be equal to itself");
+ }
+
+ @Test
+ @DisplayName("equals() should return true for logically equal objects")
+ void equalsLogicallyEqualObjects() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("test", List.of("t"), List.of("text/t"), true, false); // Name case-insensitive
+
+ assertEquals(format1, format2, "Objects with same properties (case-insensitive name) should be equal");
+ }
+
+ @Test
+ @DisplayName("equals() should return false for objects with different names")
+ void equalsDifferentNames() {
+ RdfFormat format1 = new RdfFormat("Test1", List.of("t"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("Test2", List.of("t"), List.of("text/t"), true, false);
+
+ assertNotEquals(format1, format2, "Objects with different names should not be equal");
+ }
+
+ @Test
+ @DisplayName("equals() should return false for objects with different extensions")
+ void equalsDifferentExtensions() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t1"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("Test", List.of("t2"), List.of("text/t"), true, false);
+
+ assertNotEquals(format1, format2, "Objects with different extensions should not be equal");
+ }
+
+ @Test
+ @DisplayName("equals() should return false for objects with different mimeTypes")
+ void equalsDifferentMimeTypes() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t1"), true, false);
+ RdfFormat format2 = new RdfFormat("Test", List.of("t"), List.of("text/t2"), true, false);
+
+ assertNotEquals(format1, format2, "Objects with different mimeTypes should not be equal");
+ }
+
+ @Test
+ @DisplayName("equals() should return false for objects with different supportsNamespaces")
+ void equalsDifferentSupportsNamespaces() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("Test", List.of("t"), List.of("text/t"), false, false);
+
+ assertNotEquals(format1, format2, "Objects with different supportsNamespaces should not be equal");
+ }
+
+ @Test
+ @DisplayName("equals() should return false for objects with different supportsNamedGraphs")
+ void equalsDifferentSupportsNamedGraphs() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, true);
+ RdfFormat format2 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, false);
+
+ assertNotEquals(format1, format2, "Objects with different supportsNamedGraphs should not be equal");
+ }
+
+ @Test
+ @DisplayName("hashCode() should be consistent with equals()")
+ void hashCodeConsistency() {
+ RdfFormat format1 = new RdfFormat("Test", List.of("t"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("test", List.of("t"), List.of("text/t"), true, false);
+
+ assertEquals(format1, format2, "Objects should be equal");
+ assertEquals(format1.hashCode(), format2.hashCode(), "Hash codes must be equal for equal objects");
+ }
+
+ @Test
+ @DisplayName("hashCode() should return different values for unequal objects (high probability)")
+ void hashCodeDifference() {
+ RdfFormat format1 = new RdfFormat("Test1", List.of("t"), List.of("text/t"), true, false);
+ RdfFormat format2 = new RdfFormat("Test2", List.of("t"), List.of("text/t"), true, false);
+
+ assertNotEquals(format1.hashCode(), format2.hashCode(), "Hash codes should ideally be different for unequal objects");
+ }
+
+
+ @Test
+ @DisplayName("TURTLE constant should be correctly defined")
+ void turtleConstant() {
+ RdfFormat turtle = RdfFormat.TURTLE;
+
+ assertNotNull(turtle, "TURTLE constant should not be null");
+ assertEquals("Turtle", turtle.getName());
+ assertTrue(turtle.getExtensions().contains("ttl"));
+ assertTrue(turtle.getMimeTypes().contains("text/turtle"));
+ assertTrue(turtle.supportsNamespaces());
+ assertFalse(turtle.supportsNamedGraphs());
+ }
+
+ @Test
+ @DisplayName("NTRIPLES constant should be correctly defined")
+ void nTriplesConstant() {
+ RdfFormat ntriples = RdfFormat.NTRIPLES;
+
+ assertNotNull(ntriples, "NTRIPLES constant should not be null");
+ assertEquals("N-Triples", ntriples.getName());
+ assertTrue(ntriples.getExtensions().contains("nt"));
+ assertTrue(ntriples.getMimeTypes().contains("application/n-triples"));
+ assertFalse(ntriples.supportsNamespaces());
+ assertFalse(ntriples.supportsNamedGraphs());
+ }
+
+ @Test
+ @DisplayName("NQUADS constant should be correctly defined")
+ void nQuadsConstant() {
+ RdfFormat nquads = RdfFormat.NQUADS;
+
+ assertNotNull(nquads, "NQUADS constant should not be null");
+ assertEquals("N-Quads", nquads.getName());
+ assertTrue(nquads.getExtensions().contains("nq"));
+ assertTrue(nquads.getMimeTypes().contains("application/n-quads"));
+ assertFalse(nquads.supportsNamespaces());
+ assertTrue(nquads.supportsNamedGraphs());
+ }
+
+ @Test
+ @DisplayName("JSONLD constant should be correctly defined")
+ void jsonLdConstant() {
+ RdfFormat jsonld = RdfFormat.JSONLD;
+
+ assertNotNull(jsonld, "JSONLD constant should not be null");
+ assertEquals("JSON-LD", jsonld.getName());
+ assertTrue(jsonld.getExtensions().contains("jsonld"));
+ assertTrue(jsonld.getMimeTypes().contains("application/ld+json"));
+ assertTrue(jsonld.supportsNamespaces());
+ assertTrue(jsonld.supportsNamedGraphs());
+ }
+
+ @Test
+ @DisplayName("RDFXML constant should be correctly defined")
+ void rdfXmlConstant() {
+ RdfFormat rdfxml = RdfFormat.RDFXML;
+
+ assertNotNull(rdfxml, "RDFXML constant should not be null");
+ assertEquals("RDF/XML", rdfxml.getName());
+ assertTrue(rdfxml.getExtensions().contains("rdf"));
+ assertTrue(rdfxml.getExtensions().contains("xml"));
+ assertTrue(rdfxml.getMimeTypes().contains("application/rdf+xml"));
+ assertTrue(rdfxml.supportsNamespaces());
+ assertFalse(rdfxml.supportsNamedGraphs());
+ }
+
+
+ @Test
+ @DisplayName("byName() should find existing format by name (case-insensitive)")
+ void byNameFound() {
+ Optional format = RdfFormat.byName("TuRtLe");
+
+ assertTrue(format.isPresent(), "Turtle format should be found by name");
+ assertEquals(RdfFormat.TURTLE, format.get(), "Found format should be the TURTLE constant");
+ }
+
+ @Test
+ @DisplayName("byName() should return empty Optional for non-existent name")
+ void byNameNotFound() {
+ Optional format = RdfFormat.byName("NonExistentFormat");
+
+ assertFalse(format.isPresent(), "Non-existent format should not be found");
+ }
+
+ @Test
+ @DisplayName("byExtension() should find existing format by extension (case-insensitive)")
+ void byExtensionFound() {
+ Optional format = RdfFormat.byExtension("TTL");
+
+ assertTrue(format.isPresent(), "Turtle format should be found by extension");
+ assertEquals(RdfFormat.TURTLE, format.get(), "Found format should be the TURTLE constant");
+
+ Optional rdfXmlFormat = RdfFormat.byExtension("XML");
+ assertTrue(rdfXmlFormat.isPresent());
+ assertEquals(RdfFormat.RDFXML, rdfXmlFormat.get());
+ }
+
+ @Test
+ @DisplayName("byExtension() should return empty Optional for non-existent extension")
+ void byExtensionNotFound() {
+ Optional format = RdfFormat.byExtension("xyz");
+
+ assertFalse(format.isPresent(), "Non-existent extension should not find a format");
+ }
+
+ @Test
+ @DisplayName("byMimeType() should find existing format by MIME type (case-insensitive)")
+ void byMimeTypeFound() {
+ Optional format = RdfFormat.byMimeType("text/TuRtLe");
+
+ assertTrue(format.isPresent(), "Turtle format should be found by MIME type");
+ assertEquals(RdfFormat.TURTLE, format.get(), "Found format should be the TURTLE constant");
+
+ Optional nTriplesFormat = RdfFormat.byMimeType("text/plain");
+
+ assertTrue(nTriplesFormat.isPresent());
+ assertEquals(RdfFormat.NTRIPLES, nTriplesFormat.get());
+ }
+
+ @Test
+ @DisplayName("byMimeType() should return empty Optional for non-existent MIME type")
+ void byMimeTypeNotFound() {
+ Optional format = RdfFormat.byMimeType("application/x-unknown");
+
+ assertFalse(format.isPresent(), "Non-existent MIME type should not find a format");
+ }
+
+ @Test
+ @DisplayName("all() should return a list containing all predefined formats")
+ void allFormats() {
+ List allFormats = RdfFormat.all();
+
+ assertNotNull(allFormats, "List of all formats should not be null");
+ assertEquals(5, allFormats.size(), "List should contain 5 predefined formats"); // TURTLE, NTRIPLES, NQUADS, JSONLD, RDFXML
+
+ assertTrue(allFormats.contains(RdfFormat.TURTLE));
+ assertTrue(allFormats.contains(RdfFormat.NTRIPLES));
+ assertTrue(allFormats.contains(RdfFormat.NQUADS));
+ assertTrue(allFormats.contains(RdfFormat.JSONLD));
+ assertTrue(allFormats.contains(RdfFormat.RDFXML));
+
+ assertThrows(UnsupportedOperationException.class, () -> allFormats.add(RdfFormat.TURTLE),
+ "The list returned by all() should be unmodifiable");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/common/serialization/SerializerTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/SerializerTest.java
new file mode 100644
index 000000000..0e8700810
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/SerializerTest.java
@@ -0,0 +1,98 @@
+package fr.inria.corese.core.next.impl.common.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+
+import java.io.Writer;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockedConstruction;
+import org.mockito.MockitoAnnotations;
+
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.impl.exception.SerializationException;
+
+class SerializerTest {
+
+ private Serializer serializer;
+
+ @Mock
+ private Model mockModel;
+ @Mock
+ private FormatConfig mockConfig;
+ @Mock
+ private Writer mockWriter;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ serializer = new Serializer(mockModel, mockConfig);
+ }
+
+ // --- Constructor tests ---
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null model")
+ void constructorShouldThrowForNullModel() {
+ assertThrows(NullPointerException.class, () -> new Serializer(null), "Model cannot be null");
+ assertThrows(NullPointerException.class, () -> new Serializer(null, mockConfig), "Model cannot be null");
+ }
+
+ @Test
+ @DisplayName("Constructor should throw NullPointerException for null config")
+ void constructorShouldThrowForNullConfig() {
+ assertThrows(NullPointerException.class, () -> new Serializer(mockModel, null), "FormatConfig cannot be null");
+ }
+
+ // --- Tests for the arguments of the serialize method ---
+
+ @Test
+ @DisplayName("serialize should throw NullPointerException for null writer")
+ void serializeShouldThrowForNullWriter() {
+ assertThrows(NullPointerException.class, () -> serializer.serialize(null, RdfFormat.NTRIPLES),
+ "Writer cannot be null");
+ }
+
+ @Test
+ @DisplayName("serialize should throw NullPointerException for null format")
+ void serializeShouldThrowForNullFormat() {
+ assertThrows(NullPointerException.class, () -> serializer.serialize(mockWriter, null),
+ "RdfFormat cannot be null");
+ }
+
+ // --- Serialization delegation tests ---
+
+ @Test
+ @DisplayName("serialize should delegate to NTriplesFormat for NTRIPLES format")
+ void serializeShouldDelegateToNTriplesFormat() throws SerializationException {
+ try (MockedConstruction mockedNtConstructor = mockConstruction(NTriplesFormat.class)) {
+ serializer.serialize(mockWriter, RdfFormat.NTRIPLES);
+
+ assertEquals(1, mockedNtConstructor.constructed().size(),
+ "NTriplesFormat constructor should be called once");
+
+ NTriplesFormat createdNtSerializer = mockedNtConstructor.constructed().get(0);
+
+ verify(createdNtSerializer).write(mockWriter);
+ }
+ }
+
+ @Test
+ @DisplayName("serialize should delegate to NQuadsFormat for NQUADS format")
+ void serializeShouldDelegateToNQuadsFormat() throws SerializationException {
+ try (MockedConstruction mockedNqConstructor = mockConstruction(NQuadsFormat.class)) {
+ serializer.serialize(mockWriter, RdfFormat.NQUADS);
+
+ assertEquals(1, mockedNqConstructor.constructed().size(), "NQuadsFormat constructor should be called once");
+ NQuadsFormat createdNqSerializer = mockedNqConstructor.constructed().get(0);
+
+ verify(createdNqSerializer).write(mockWriter);
+ }
+ }
+
+}
\ No newline at end of file