From e9046d998232e63db99172ad6745880bae7ac3e3 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 11 Jun 2025 14:37:01 +0200 Subject: [PATCH 01/19] Create NTriples Serializer --- .../inria/corese/core/next/api/Resource.java | 11 ++ .../exception/SerializationException.java | 50 ++++++ .../model/serialization/NFormatConfig.java | 62 +++++++ .../model/serialization/NTriplesFormat.java | 159 ++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java create mode 100644 src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java create mode 100644 src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java 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..0b286bceb 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 @@ -12,5 +12,16 @@ public interface Resource extends Value { default boolean isResource() { return true; } + /** + * @return true if this resource is a blank node, false otherwise (i.e., if it is an IRI). + */ + boolean isBlank(); + + /** + * Retrieves the unique identifier of this resource. + * For IRIs, this is the IRI string. For blank nodes, this is the blank node identifier. + */ + String getID(); + } diff --git a/src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java b/src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java new file mode 100644 index 000000000..b0800811b --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java @@ -0,0 +1,50 @@ +package fr.inria.corese.core.next.api.base.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/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java new file mode 100644 index 000000000..5b41ee14b --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java @@ -0,0 +1,62 @@ +package fr.inria.corese.core.next.api.base.model.serialization; + +import java.util.Objects; + +/** + * Configuration options for the {@link NTriplesFormat} serializer. + * Use {@link NFormatConfig# Builder} to create instances. + */ +public class NFormatConfig { + + private final String blankNodePrefix; + + + /** + * Private constructor to enforce usage of the Builder. + * @param builder The builder instance. + */ + private NFormatConfig(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 NFormatConfig}. + */ + public static class Builder { + + private String blankNodePrefix = "_:"; + + + public 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 NFormatConfig} instance. + * @return A new NFormatConfig instance. + */ + public NFormatConfig build() { + return new NFormatConfig(this); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java new file mode 100644 index 000000000..e04593d30 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java @@ -0,0 +1,159 @@ +package fr.inria.corese.core.next.api.base.model.serialization; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.base.exception.SerializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +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 { + + /** + * Logger for this class, used for logging potential issues or information during serialization. + */ + private static final Logger logger = LoggerFactory.getLogger(NTriplesFormat.class); + + /** + * A constant string representing a single space character, used for separating elements in N-Triples output. + */ + private static final String SPACE = " "; + + /** + * A constant string representing the prefix for blank nodes in N-Triples format. + */ + private static final String SPACE_POINT = " ."; + + private final Model model; + private NFormatConfig 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 NFormatConfig.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 NFormatConfig} to use for serialization. Must not be null. + * @throws NullPointerException if the provided model or config is null. + */ + public NTriplesFormat(Model model, NFormatConfig 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. + */ + public void write(Writer writer) throws SerializationException { + try { + for (Statement stmt : model) { + writeStatement(writer, stmt); + writer.write("\n"); + } + } catch (IOException e) { + logger.error("An I/O error occurred during N-Triples serialization: {}", e.getMessage(), e); + throw new SerializationException("Failed to write", "NTriples", e); + } catch (IllegalArgumentException e) { + logger.error("Invalid data encountered during N-Triples serialization: {}", e.getMessage(), 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 ." + * If the statement has a context, it is written as "$subject $predicate $object $context ." + * + * @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(SPACE); + writeValue(writer, stmt.getPredicate()); + writer.write(SPACE); + writeValue(writer, stmt.getObject()); + + Resource context = stmt.getContext(); + if (context != null) { + writer.write(SPACE); + writeValue(writer, context); + } + + writer.write(SPACE_POINT); + } + + /** + * Writes a single {@link Value} to the writer. + * Handles literals, resources (blank nodes and IRIs), and other value types by calling their {@code stringValue()} method. + * + * @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. + */ + private void writeValue(Writer writer, Value value) throws IOException { + 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()) { + writer.write(value.stringValue()); + } else if (value.isResource()) { + writeResource(writer, (Resource) value); + } else { + writer.write(value.stringValue()); + } + } + + /** + * Writes a {@link Resource} (either a blank node or an IRI) to the writer. + * Blank nodes are prefixed with "_:", and IRIs are written directly. + * + * @param writer the {@link Writer} to which the resource will be written. + * @param resource the {@link Resource} to write. + * @throws IOException if an I/O error occurs. + */ + private void writeResource(Writer writer, Resource resource) throws IOException { + if (resource.isBlank()) { + writer.write(config.getBlankNodePrefix()); + writer.write(resource.getID()); + } else { + writeIRI(writer, (IRI) resource); + } + } + + /** + * Writes an {@link IRI} to the writer. + * The IRI's string representation (including angle brackets if necessary) is written directly. + * + * @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(iri.stringValue()); + } +} \ No newline at end of file From 692693d8e1c14b70deb1eb37ce55dcda21b23980 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 11 Jun 2025 14:42:49 +0200 Subject: [PATCH 02/19] Create NQuads Serializer --- .../model/serialization/NQuadsFormat.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java new file mode 100644 index 000000000..ad1689378 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java @@ -0,0 +1,160 @@ +package fr.inria.corese.core.next.api.base.model.serialization; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.base.exception.SerializationException; + +import java.io.IOException; +import java.io.Writer; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Serializes a Corese {@link Model} into N-Quads format. + * This class provides a method to write the quads of a model to a given {@link Writer} + * according to the N-Quads specification. It reuses the underlying serialization + * logic for N-Triples but explicitly includes named graph information when present. + * It can be configured using {@link NFormatConfig}. + */ +public class NQuadsFormat { + + /** + * Logger for this class, used for logging potential issues or information during serialization. + */ + private static final Logger logger = LoggerFactory.getLogger(NQuadsFormat.class); + + /** + * A constant string representing a single space character, used for separating elements in N-Quads output. + */ + private static final String SPACE = " "; + /** + * A constant string representing the prefix for blank nodes in N-Triples format. + */ + private static final String SPACE_POINT = " ."; + + private final Model model; + private final NFormatConfig 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 NFormatConfig.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 NFormatConfig} to use for serialization. Must not be null. + * @throws NullPointerException if the provided model or config is null. + */ + public NQuadsFormat(Model model, NFormatConfig 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 quad in the model is written on a new line, terminated by a dot and a newline character. + * If a statement has no context, it is treated as belonging to the default graph. + * + * @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. + */ + public void write(Writer writer) throws SerializationException { + try { + for (Statement stmt : model) { + writeQuad(writer, stmt); // Renamed for clarity in NQuads context + writer.write("\n"); + } + } catch (IOException e) { + logger.error("An I/O error occurred during N-Quads serialization: {}", e.getMessage(), e); + throw new SerializationException("Failed to write", "NQuads", e); + } catch (IllegalArgumentException e) { + logger.error("Invalid data encountered during N-Quads serialization: {}", e.getMessage(), e); + throw new SerializationException("Invalid data: " + e.getMessage(), "NQuads", e); + } + } + + /** + * Writes a single {@link Statement} (treated as a quad) to the writer in N-Quads format. + * The quad is written as "$subject $predicate $object $context ." + * If the statement has no context, it is written as "$subject $predicate $object ." + * + * @param writer the {@link Writer} to which the quad will be written. + * @param stmt the {@link Statement} to write. + * @throws IOException if an I/O error occurs. + */ + private void writeQuad(Writer writer, Statement stmt) throws IOException { + writeValue(writer, stmt.getSubject()); + writer.write(SPACE); + writeValue(writer, stmt.getPredicate()); + writer.write(SPACE); + writeValue(writer, stmt.getObject()); + + Resource context = stmt.getContext(); + if (context != null) { + writer.write(SPACE); + writeValue(writer, context); + } + + writer.write(SPACE_POINT); + } + + /** + * Writes a single {@link Value} to the writer. + * Handles literals, resources (blank nodes and IRIs), and other value types by calling their {@code stringValue()} method. + * + * @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. + */ + private void writeValue(Writer writer, Value value) throws IOException { + 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()) { + writer.write(value.stringValue()); + } else if (value.isResource()) { + writeResource(writer, (Resource) value); + } else { + writer.write(value.stringValue()); + } + } + + /** + * Writes a {@link Resource} (either a blank node or an IRI) to the writer. + * Blank nodes are prefixed with the configured blank node prefix, and IRIs are written directly. + * + * @param writer the {@link Writer} to which the resource will be written. + * @param resource the {@link Resource} to write. + * @throws IOException if an I/O error occurs. + */ + private void writeResource(Writer writer, Resource resource) throws IOException { + if (resource.isBlank()) { + writer.write(config.getBlankNodePrefix()); + writer.write(resource.getID()); + } else { + writeIRI(writer, (IRI) resource); + } + } + + /** + * Writes an {@link IRI} to the writer. + * The IRI's string representation (including angle brackets if necessary) is written directly. + * + * @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(iri.stringValue()); + } +} \ No newline at end of file From 0400c31bc2081e908eee83e94f36b57ed63686a2 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 11 Jun 2025 15:08:25 +0200 Subject: [PATCH 03/19] Create NQuads Serializer --- .../java/fr/inria/corese/core/next/api/Resource.java | 9 --------- .../next/api/base/model/serialization/NFormatConfig.java | 2 +- .../next/api/base/model/serialization/NQuadsFormat.java | 3 +-- .../api/base/model/serialization/NTriplesFormat.java | 3 +-- .../next/api/model/serialization/NFormatConfigTest.java | 4 ++++ 5 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java 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 0b286bceb..920bf285b 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 @@ -12,16 +12,7 @@ public interface Resource extends Value { default boolean isResource() { return true; } - /** - * @return true if this resource is a blank node, false otherwise (i.e., if it is an IRI). - */ - boolean isBlank(); - /** - * Retrieves the unique identifier of this resource. - * For IRIs, this is the IRI string. For blank nodes, this is the blank node identifier. - */ - String getID(); } diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java index 5b41ee14b..da392ba4b 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java @@ -4,7 +4,7 @@ /** * Configuration options for the {@link NTriplesFormat} serializer. - * Use {@link NFormatConfig# Builder} to create instances. + * Use {@link NFormatConfig # Builder} to create instances. */ public class NFormatConfig { diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java index ad1689378..867e4ecd2 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java @@ -138,9 +138,8 @@ private void writeValue(Writer writer, Value value) throws IOException { * @throws IOException if an I/O error occurs. */ private void writeResource(Writer writer, Resource resource) throws IOException { - if (resource.isBlank()) { + if (resource.isResource()) { writer.write(config.getBlankNodePrefix()); - writer.write(resource.getID()); } else { writeIRI(writer, (IRI) resource); } diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java index e04593d30..bdb7140da 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java @@ -137,9 +137,8 @@ private void writeValue(Writer writer, Value value) throws IOException { * @throws IOException if an I/O error occurs. */ private void writeResource(Writer writer, Resource resource) throws IOException { - if (resource.isBlank()) { + if (resource.isResource()) { writer.write(config.getBlankNodePrefix()); - writer.write(resource.getID()); } else { writeIRI(writer, (IRI) resource); } diff --git a/src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java b/src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java new file mode 100644 index 000000000..b36c74356 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java @@ -0,0 +1,4 @@ +package fr.inria.corese.core.next.api.model.serialization; + +public class NFormatConfigTest { +} From 7328345fe1a0de17923cc2b568a0232d72ead6b2 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Wed, 11 Jun 2025 16:54:20 +0200 Subject: [PATCH 04/19] Create NTriples/NQuads Serializer --- .../{NFormatConfig.java => FormatConfig.java} | 14 +-- .../model/serialization/NQuadsFormat.java | 87 ++++++++++--------- .../model/serialization/NTriplesFormat.java | 68 ++++++++------- ...tConfigTest.java => FormatConfigTest.java} | 2 +- 4 files changed, 90 insertions(+), 81 deletions(-) rename src/main/java/fr/inria/corese/core/next/api/base/model/serialization/{NFormatConfig.java => FormatConfig.java} (79%) rename src/test/java/fr/inria/corese/core/next/api/model/serialization/{NFormatConfigTest.java => FormatConfigTest.java} (65%) diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java similarity index 79% rename from src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java rename to src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java index da392ba4b..f9208aaba 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NFormatConfig.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java @@ -4,9 +4,9 @@ /** * Configuration options for the {@link NTriplesFormat} serializer. - * Use {@link NFormatConfig # Builder} to create instances. + * Use {@link FormatConfig # Builder} to create instances. */ -public class NFormatConfig { +public class FormatConfig { private final String blankNodePrefix; @@ -15,7 +15,7 @@ public class NFormatConfig { * Private constructor to enforce usage of the Builder. * @param builder The builder instance. */ - private NFormatConfig(Builder builder) { + private FormatConfig(Builder builder) { this.blankNodePrefix = builder.blankNodePrefix; } @@ -29,7 +29,7 @@ public String getBlankNodePrefix() { } /** - * Builder class for {@link NFormatConfig}. + * Builder class for {@link FormatConfig}. */ public static class Builder { @@ -52,11 +52,11 @@ public Builder blankNodePrefix(String blankNodePrefix) { /** - * Builds a new {@link NFormatConfig} instance. + * Builds a new {@link FormatConfig} instance. * @return A new NFormatConfig instance. */ - public NFormatConfig build() { - return new NFormatConfig(this); + public FormatConfig build() { + return new FormatConfig(this); } } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java index 867e4ecd2..a4b19a74f 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java @@ -2,19 +2,17 @@ import fr.inria.corese.core.next.api.*; import fr.inria.corese.core.next.api.base.exception.SerializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.Writer; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Serializes a Corese {@link Model} into N-Quads format. - * This class provides a method to write the quads of a model to a given {@link Writer} - * according to the N-Quads specification. It reuses the underlying serialization - * logic for N-Triples but explicitly includes named graph information when present. - * It can be configured using {@link NFormatConfig}. + * 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 { @@ -27,13 +25,14 @@ public class NQuadsFormat { * A constant string representing a single space character, used for separating elements in N-Quads output. */ private static final String SPACE = " "; + /** - * A constant string representing the prefix for blank nodes in N-Triples format. + * A constant string representing the end of a statement in N-Quads format (space, dot, newline). */ - private static final String SPACE_POINT = " ."; + private static final String SPACE_POINT = " .\n"; private final Model model; - private final NFormatConfig config; + private final FormatConfig config; /** * Constructs a new {@code NQuadsFormat} instance with the specified model and default configuration. @@ -42,25 +41,24 @@ public class NQuadsFormat { * @throws NullPointerException if the provided model is null. */ public NQuadsFormat(Model model) { - this(model, new NFormatConfig.Builder().build()); + 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 NFormatConfig} to use for serialization. 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, NFormatConfig config) { + 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 quad in the model is written on a new line, terminated by a dot and a newline character. - * If a statement has no context, it is treated as belonging to the default graph. + * 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. @@ -68,9 +66,9 @@ public NQuadsFormat(Model model, NFormatConfig config) { public void write(Writer writer) throws SerializationException { try { for (Statement stmt : model) { - writeQuad(writer, stmt); // Renamed for clarity in NQuads context - writer.write("\n"); + writeStatement(writer, stmt); } + writer.flush(); } catch (IOException e) { logger.error("An I/O error occurred during N-Quads serialization: {}", e.getMessage(), e); throw new SerializationException("Failed to write", "NQuads", e); @@ -81,15 +79,15 @@ public void write(Writer writer) throws SerializationException { } /** - * Writes a single {@link Statement} (treated as a quad) to the writer in N-Quads format. - * The quad is written as "$subject $predicate $object $context ." - * If the statement has no context, it is written as "$subject $predicate $object ." + * 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 quad will be written. + * @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 writeQuad(Writer writer, Statement stmt) throws IOException { + private void writeStatement(Writer writer, Statement stmt) throws IOException { writeValue(writer, stmt.getSubject()); writer.write(SPACE); writeValue(writer, stmt.getPredicate()); @@ -107,12 +105,12 @@ private void writeQuad(Writer writer, Statement stmt) throws IOException { /** * Writes a single {@link Value} to the writer. - * Handles literals, resources (blank nodes and IRIs), and other value types by calling their {@code stringValue()} method. + * 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. + * @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 { if (value == null) { @@ -123,37 +121,42 @@ private void writeValue(Writer writer, Value value) throws IOException { if (value.isLiteral()) { writer.write(value.stringValue()); } else if (value.isResource()) { - writeResource(writer, (Resource) value); + 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 { - writer.write(value.stringValue()); + throw new IllegalArgumentException("Unsupported value type for N-Quads serialization: " + value.getClass().getName()); } } /** - * Writes a {@link Resource} (either a blank node or an IRI) to the writer. - * Blank nodes are prefixed with the configured blank node prefix, and IRIs are written directly. + * 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 resource will be written. - * @param resource the {@link Resource} to write. + * @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 writeResource(Writer writer, Resource resource) throws IOException { - if (resource.isResource()) { - writer.write(config.getBlankNodePrefix()); - } else { - writeIRI(writer, (IRI) resource); - } + private void writeIRI(Writer writer, IRI iri) throws IOException { + writer.write("<"); + writer.write(iri.stringValue()); + writer.write(">"); } /** - * Writes an {@link IRI} to the writer. - * The IRI's string representation (including angle brackets if necessary) is written directly. + * 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 IRI will be written. - * @param iri the {@link IRI} to write. + * @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 writeIRI(Writer writer, IRI iri) throws IOException { - writer.write(iri.stringValue()); + private void writeBlankNode(Writer writer, Resource blankNode) throws IOException { + writer.write(config.getBlankNodePrefix()); + writer.write(blankNode.stringValue()); } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java index bdb7140da..0e115474f 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java @@ -27,12 +27,12 @@ public class NTriplesFormat { private static final String SPACE = " "; /** - * A constant string representing the prefix for blank nodes in N-Triples format. + * A constant string representing the end of a statement in N-Triples format (space, dot, newline). */ - private static final String SPACE_POINT = " ."; + private static final String SPACE_POINT = " .\n"; private final Model model; - private NFormatConfig config; + private final FormatConfig config; /** * Constructs a new {@code NTriplesFormat} instance with the specified model and default configuration. @@ -42,17 +42,17 @@ public class NTriplesFormat { */ public NTriplesFormat(Model model) { - this(model, new NFormatConfig.Builder().build()); + 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 NFormatConfig} to use for serialization. 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, NFormatConfig config) { + public NTriplesFormat(Model model, FormatConfig config) { this.model = Objects.requireNonNull(model, "Model cannot be null"); this.config = Objects.requireNonNull(config, "Configuration cannot be null"); } @@ -68,8 +68,9 @@ public void write(Writer writer) throws SerializationException { try { for (Statement stmt : model) { writeStatement(writer, stmt); - writer.write("\n"); + } + writer.flush(); } catch (IOException e) { logger.error("An I/O error occurred during N-Triples serialization: {}", e.getMessage(), e); throw new SerializationException("Failed to write", "NTriples", e); @@ -82,7 +83,7 @@ public void write(Writer writer) throws SerializationException { /** * Writes a single {@link Statement} to the writer in N-Triples format. * The statement is written as "$subject $predicate $object ." - * If the statement has a context, it is written as "$subject $predicate $object $context ." + * 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. @@ -97,8 +98,7 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { Resource context = stmt.getContext(); if (context != null) { - writer.write(SPACE); - writeValue(writer, context); + logger.warn("N-Triples format does not support named graphs. Context '{}' will be ignored for statement: {}", context.stringValue(), stmt); } writer.write(SPACE_POINT); @@ -106,12 +106,12 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { /** * Writes a single {@link Value} to the writer. - * Handles literals, resources (blank nodes and IRIs), and other value types by calling their {@code stringValue()} method. + * 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. + * @throws IllegalArgumentException if the provided value is null or an unsupported type. */ private void writeValue(Writer writer, Value value) throws IOException { if (value == null) { @@ -122,37 +122,43 @@ private void writeValue(Writer writer, Value value) throws IOException { if (value.isLiteral()) { writer.write(value.stringValue()); } else if (value.isResource()) { - writeResource(writer, (Resource) value); + 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 { - writer.write(value.stringValue()); - } - } - /** - * Writes a {@link Resource} (either a blank node or an IRI) to the writer. - * Blank nodes are prefixed with "_:", and IRIs are written directly. - * - * @param writer the {@link Writer} to which the resource will be written. - * @param resource the {@link Resource} to write. - * @throws IOException if an I/O error occurs. - */ - private void writeResource(Writer writer, Resource resource) throws IOException { - if (resource.isResource()) { - writer.write(config.getBlankNodePrefix()); - } else { - writeIRI(writer, (IRI) resource); + throw new IllegalArgumentException("Unsupported value type for N-Triples serialization: " + value.getClass().getName()); } } /** * Writes an {@link IRI} to the writer. - * The IRI's string representation (including angle brackets if necessary) is written directly. + * 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("<"); writer.write(iri.stringValue()); + writer.write(">"); + } + + /** + * 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()); } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java b/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java similarity index 65% rename from src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java rename to src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java index b36c74356..2b3775ac3 100644 --- a/src/test/java/fr/inria/corese/core/next/api/model/serialization/NFormatConfigTest.java +++ b/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java @@ -1,4 +1,4 @@ package fr.inria.corese.core.next.api.model.serialization; -public class NFormatConfigTest { +public class FormatConfigTest { } From 245aaf7fe2f2f044ba721e942c02dd651572625b Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 09:05:27 +0200 Subject: [PATCH 05/19] ajouter test unitaire sur formtConfig --- .../model/serialization/FormatConfig.java | 4 ++ .../model/serialization/FormatConfigTest.java | 55 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java index f9208aaba..7ed7f7c5a 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java @@ -13,6 +13,7 @@ public class FormatConfig { /** * Private constructor to enforce usage of the Builder. + * * @param builder The builder instance. */ private FormatConfig(Builder builder) { @@ -22,6 +23,7 @@ private FormatConfig(Builder builder) { /** * Returns the prefix to use for blank nodes. + * * @return The blank node prefix. */ public String getBlankNodePrefix() { @@ -42,6 +44,7 @@ public Builder() { /** * Sets the prefix to use for blank nodes. Default is "_:". + * * @param blankNodePrefix The desired blank node prefix. * @return The builder instance. */ @@ -53,6 +56,7 @@ public Builder blankNodePrefix(String blankNodePrefix) { /** * Builds a new {@link FormatConfig} instance. + * * @return A new NFormatConfig instance. */ public FormatConfig build() { diff --git a/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java b/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java index 2b3775ac3..a51ba42f7 100644 --- a/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java +++ b/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java @@ -1,4 +1,57 @@ package fr.inria.corese.core.next.api.model.serialization; -public class FormatConfigTest { +import fr.inria.corese.core.next.api.base.model.serialization.FormatConfig; +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); + } } From cf051482f78690d5cc63ecf92ceaaa1a4df5b193 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 09:29:30 +0200 Subject: [PATCH 06/19] Create NTriples/NQuads Serializer --- .../model => impl/common}/serialization/FormatConfig.java | 2 +- .../model => impl/common}/serialization/NQuadsFormat.java | 4 ++-- .../model => impl/common}/serialization/NTriplesFormat.java | 4 ++-- .../{api/base => impl}/exception/SerializationException.java | 2 +- .../model => impl/common}/serialization/FormatConfigTest.java | 3 +-- 5 files changed, 7 insertions(+), 8 deletions(-) rename src/main/java/fr/inria/corese/core/next/{api/base/model => impl/common}/serialization/FormatConfig.java (95%) rename src/main/java/fr/inria/corese/core/next/{api/base/model => impl/common}/serialization/NQuadsFormat.java (97%) rename src/main/java/fr/inria/corese/core/next/{api/base/model => impl/common}/serialization/NTriplesFormat.java (97%) rename src/main/java/fr/inria/corese/core/next/{api/base => impl}/exception/SerializationException.java (96%) rename src/test/java/fr/inria/corese/core/next/{api/model => impl/common}/serialization/FormatConfigTest.java (93%) diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java similarity index 95% rename from src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java rename to src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java index 7ed7f7c5a..5c6df301c 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/FormatConfig.java +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfig.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.api.base.model.serialization; +package fr.inria.corese.core.next.impl.common.serialization; import java.util.Objects; diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java similarity index 97% rename from src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java rename to src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java index a4b19a74f..2bec6b147 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NQuadsFormat.java +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormat.java @@ -1,7 +1,7 @@ -package fr.inria.corese.core.next.api.base.model.serialization; +package fr.inria.corese.core.next.impl.common.serialization; import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.base.exception.SerializationException; +import fr.inria.corese.core.next.impl.exception.SerializationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java similarity index 97% rename from src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java rename to src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java index 0e115474f..1b3acf069 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/serialization/NTriplesFormat.java +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormat.java @@ -1,7 +1,7 @@ -package fr.inria.corese.core.next.api.base.model.serialization; +package fr.inria.corese.core.next.impl.common.serialization; import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.base.exception.SerializationException; +import fr.inria.corese.core.next.impl.exception.SerializationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java b/src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java similarity index 96% rename from src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java rename to src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java index b0800811b..710ba0c93 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/exception/SerializationException.java +++ b/src/main/java/fr/inria/corese/core/next/impl/exception/SerializationException.java @@ -1,4 +1,4 @@ -package fr.inria.corese.core.next.api.base.exception; +package fr.inria.corese.core.next.impl.exception; /** * Exception levée lors d'échecs de sérialisation/désérialisation RDF. diff --git a/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java similarity index 93% rename from src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java rename to src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java index a51ba42f7..c1ee05f95 100644 --- a/src/test/java/fr/inria/corese/core/next/api/model/serialization/FormatConfigTest.java +++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/FormatConfigTest.java @@ -1,6 +1,5 @@ -package fr.inria.corese.core.next.api.model.serialization; +package fr.inria.corese.core.next.impl.common.serialization; -import fr.inria.corese.core.next.api.base.model.serialization.FormatConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 4e1fc0bbc01fadac9efad37096ec0f42301131ea Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 09:51:10 +0200 Subject: [PATCH 07/19] ajouter une classe constants --- .../common/serialization/NQuadsFormat.java | 22 +++---- .../common/serialization/NTriplesFormat.java | 21 ++----- .../common/util/SerializationConstants.java | 59 +++++++++++++++++++ 3 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java 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 index 2bec6b147..3180b4f74 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -21,15 +22,6 @@ public class NQuadsFormat { */ private static final Logger logger = LoggerFactory.getLogger(NQuadsFormat.class); - /** - * A constant string representing a single space character, used for separating elements in N-Quads output. - */ - private static final String SPACE = " "; - - /** - * A constant string representing the end of a statement in N-Quads format (space, dot, newline). - */ - private static final String SPACE_POINT = " .\n"; private final Model model; private final FormatConfig config; @@ -89,18 +81,18 @@ public void write(Writer writer) throws SerializationException { */ private void writeStatement(Writer writer, Statement stmt) throws IOException { writeValue(writer, stmt.getSubject()); - writer.write(SPACE); + writer.write(SerializationConstants.SPACE); writeValue(writer, stmt.getPredicate()); - writer.write(SPACE); + writer.write(SerializationConstants.SPACE); writeValue(writer, stmt.getObject()); Resource context = stmt.getContext(); if (context != null) { - writer.write(SPACE); + writer.write(SerializationConstants.SPACE); writeValue(writer, context); } - writer.write(SPACE_POINT); + writer.write(SerializationConstants.SPACE_POINT); } /** @@ -142,9 +134,9 @@ private void writeValue(Writer writer, Value value) throws IOException { * @throws IOException if an I/O error occurs. */ private void writeIRI(Writer writer, IRI iri) throws IOException { - writer.write("<"); + writer.write(SerializationConstants.LT); writer.write(iri.stringValue()); - writer.write(">"); + writer.write(SerializationConstants.GT); } /** 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 index 1b3acf069..c5fa45291 100644 --- 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 @@ -4,7 +4,7 @@ import fr.inria.corese.core.next.impl.exception.SerializationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import fr.inria.corese.core.next.impl.common.util.SerializationConstants; import java.io.IOException; import java.io.Writer; import java.util.Objects; @@ -21,15 +21,6 @@ public class NTriplesFormat { */ private static final Logger logger = LoggerFactory.getLogger(NTriplesFormat.class); - /** - * A constant string representing a single space character, used for separating elements in N-Triples output. - */ - private static final String SPACE = " "; - - /** - * A constant string representing the end of a statement in N-Triples format (space, dot, newline). - */ - private static final String SPACE_POINT = " .\n"; private final Model model; private final FormatConfig config; @@ -91,9 +82,9 @@ public void write(Writer writer) throws SerializationException { */ private void writeStatement(Writer writer, Statement stmt) throws IOException { writeValue(writer, stmt.getSubject()); - writer.write(SPACE); + writer.write(SerializationConstants.SPACE); writeValue(writer, stmt.getPredicate()); - writer.write(SPACE); + writer.write(SerializationConstants.SPACE); writeValue(writer, stmt.getObject()); Resource context = stmt.getContext(); @@ -101,7 +92,7 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { logger.warn("N-Triples format does not support named graphs. Context '{}' will be ignored for statement: {}", context.stringValue(), stmt); } - writer.write(SPACE_POINT); + writer.write(SerializationConstants.SPACE_POINT); } /** @@ -144,9 +135,9 @@ private void writeValue(Writer writer, Value value) throws IOException { * @throws IOException if an I/O error occurs. */ private void writeIRI(Writer writer, IRI iri) throws IOException { - writer.write("<"); + writer.write(SerializationConstants.LT); writer.write(iri.stringValue()); - writer.write(">"); + writer.write(SerializationConstants.GT); } /** 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..d19bc3bb9 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java @@ -0,0 +1,59 @@ +package fr.inria.corese.core.next.impl.common.util; + +/** + * Provides a collection of constant strings used during the serialization process + * for various RDF formats like N-Triples and N-Quads. 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 in N-Triples or N-Quads 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 in N-Triples and N-Quads. + */ + public static final String GT = ">"; + + /** + * Represents the standard prefix for blank nodes ("_:"). + * Used to identify blank nodes in N-Triples and N-Quads. + */ + public static final String BNODE_PREFIX = "_:"; + + /** + * Represents a double-quote character ("\""). + * Used to enclose literal values in N-Triples and N-Quads. + */ + public static final String QUOTE = "\""; + + + /** + * Private constructor to prevent instantiation of this utility class. + * All members are static. + */ + private SerializationConstants() {} +} \ No newline at end of file From 3d7c2fdbda86d002e024ba4e0f196174da944f14 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 10:29:00 +0200 Subject: [PATCH 08/19] correction java Doc et ajouter n triples format --- .../common/util/SerializationConstants.java | 14 +- .../serialization/NTriplesFormatTest.java | 402 ++++++++++++++++++ 2 files changed, 409 insertions(+), 7 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormatTest.java 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 index d19bc3bb9..376a22bf9 100644 --- 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 @@ -2,7 +2,7 @@ /** * Provides a collection of constant strings used during the serialization process - * for various RDF formats like N-Triples and N-Quads. These constants aim to + * for various RDF formats. These constants aim to * centralize common literal values to ensure consistency and maintainability * across different serialization utilities. */ @@ -15,7 +15,7 @@ public class SerializationConstants { public static final String SPACE = " "; /** - * Represents the termination sequence for an RDF statement in N-Triples or N-Quads format: + * 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"; @@ -27,26 +27,26 @@ public class SerializationConstants { public static final String POINT = "."; /** - * Represents the less-than sign ("<"). + * 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 in N-Triples and N-Quads. + * 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 in N-Triples and N-Quads. + * Used to identify blank nodes. */ public static final String BNODE_PREFIX = "_:"; /** * Represents a double-quote character ("\""). - * Used to enclose literal values in N-Triples and N-Quads. + * Used to enclose literal values. */ public static final String QUOTE = "\""; 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..f6e71edd7 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/NTriplesFormatTest.java @@ -0,0 +1,402 @@ +package fr.inria.corese.core.next.impl.common.serialization; + +import fr.inria.corese.core.next.api.*; + +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 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 mockExAge; + private IRI mockExKnows; + private Literal mockLiteralJohn; + private Literal mockLiteral30; + private Literal mockLiteralHelloEn; + private Literal mockLiteralTrue; + 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"); + mockExAge = createIRI("http://example.org/age"); + mockExKnows = createIRI("http://example.org/knows"); + + mockLiteralJohn = createLiteral("John Doe"); + mockLiteral30 = createLiteral("30", "http://www.w3.org/2001/XMLSchema#integer", null); + mockLiteralHelloEn = createLiteral("Hello", null, "en"); + mockLiteralTrue = createLiteral("true", "http://www.w3.org/2001/XMLSchema#boolean", null); + + mockBNode1 = createBlankNode("b1"); + mockBNode2 = createBlankNode("b2"); + } + + @Test + @DisplayName("Constructor should throw NullPointerException for null model") + void constructorShouldThrowForNullModel() { + assertThrows(NullPointerException.class, () -> new NTriplesFormat(null)); + assertThrows(NullPointerException.class, () -> new NTriplesFormat(null, config)); + } + + @Test + @DisplayName("Constructor should throw NullPointerException for null config") + void constructorShouldThrowForNullConfig() { + assertThrows(NullPointerException.class, () -> new NTriplesFormat(model, 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); + + + assertEquals(" \"John Doe\" .\n", 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); + + + assertEquals(" \"John Doe\" .\n", 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); + + + assertEquals("_:b1 _:b2 .\n", 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); + + assertEquals("genid-b1 genid-b2 .\n", 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") + 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") + 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") + 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" + }) + @DisplayName("Write should handle various literal values with proper escaping") + void writeShouldHandleVariousLiterals(String literalValue) throws SerializationException { + + + Literal literalMock = mock(Literal.class); + when(literalMock.isLiteral()).thenReturn(true); + when(literalMock.isResource()).thenReturn(false); + + StringBuilder sb = new StringBuilder(); + sb.append("\"").append(escapeNTriplesString(literalValue)).append("\""); + when(literalMock.stringValue()).thenReturn(sb.toString()); + + Statement stmt = createStatement( + mockExPerson, + mockExName, + literalMock + ); + when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); + + StringWriter writer = new StringWriter(); + + nTriplesFormat.write(writer); + + String expectedEscapedLiteral = escapeNTriplesString(literalValue); + assertEquals(String.format(" \"%s\" .\n", expectedEscapedLiteral), writer.toString()); + } + + @Test + @DisplayName("Write should handle multiple statements") + void writeShouldHandleMultipleStatements() throws SerializationException { + + Statement stmt1 = createStatement( + mockExPerson, + mockExName, + createLiteral("o1") + ); + 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); + + assertEquals(" \"o1\" .\n" + + "_:b1 .\n", 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 = " \"Hello\"@en .\n"; + + assertEquals(expectedOutput, writer.toString()); + } + + @Test + @DisplayName("Should handle literals with datatype IRIs") + void shouldHandleLiteralsWithDatatypeIRIs() throws SerializationException { + + Statement stmt = createStatement(mockExPerson, mockExAge, mockLiteral30); + + 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 = " \"30\"^^ .\n"; + + assertEquals(expectedOutput, writer.toString()); + } + + + 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; + } + + + private Literal createLiteral(String lexicalForm) { + return createLiteral(lexicalForm, null, null); + } + + + private Literal createLiteral(String lexicalForm, String dataType, String langTag) { + Literal literal = mock(Literal.class); + when(literal.isLiteral()).thenReturn(true); + when(literal.isResource()).thenReturn(false); + + + StringBuilder sb = new StringBuilder(); + sb.append("\"").append(escapeNTriplesString(lexicalForm)).append("\""); + if (langTag != null && !langTag.isEmpty()) { + sb.append("@").append(langTag); + } else if (dataType != null && !dataType.isEmpty()) { + sb.append("^^<").append(dataType).append(">"); + } + when(literal.stringValue()).thenReturn(sb.toString()); + return literal; + } + + + private String escapeNTriplesString(String s) { + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + + 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++]; + } + } +} From 6c2a1a955f551e816c66da6c8e6757e369cfe60f Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 14:38:20 +0200 Subject: [PATCH 09/19] Correction de la JavaDoc et ajout des tests unitaires pour NTriplesFormat --- .../common/serialization/NQuadsFormat.java | 168 ++++++++++++- .../common/serialization/NTriplesFormat.java | 171 ++++++++++++-- .../common/util/SerializationConstants.java | 26 ++- .../serialization/NTriplesFormatTest.java | 221 ++++++++++-------- 4 files changed, 465 insertions(+), 121 deletions(-) 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 index 3180b4f74..da3f76ca6 100644 --- 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 @@ -22,7 +22,6 @@ public class NQuadsFormat { */ private static final Logger logger = LoggerFactory.getLogger(NQuadsFormat.class); - private final Model model; private final FormatConfig config; @@ -39,7 +38,7 @@ public NQuadsFormat(Model model) { /** * 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 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. */ @@ -76,7 +75,7 @@ public void write(Writer writer) throws SerializationException { * 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. + * @param stmt the {@link Statement} to write. * @throws IOException if an I/O error occurs. */ private void writeStatement(Writer writer, Statement stmt) throws IOException { @@ -100,18 +99,15 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { * 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. + * @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 { - 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"); - } + validateValue(value); if (value.isLiteral()) { - writer.write(value.stringValue()); + writeLiteral(writer, (Literal) value); } else if (value.isResource()) { if (value.isIRI()) { writeIRI(writer, (IRI) value); @@ -125,17 +121,42 @@ private void writeValue(Writer writer, Value value) throws IOException { } } + /** + * 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); + + + if (literal.getLanguage().isPresent()) { + writer.write(SerializationConstants.AT_SIGN + literal.getLanguage().get()); + } else { + 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. + * @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(iri.stringValue()); + writer.write(escapeIRI(iri.stringValue())); writer.write(SerializationConstants.GT); } @@ -143,7 +164,7 @@ private void writeIRI(Writer writer, IRI iri) throws IOException { * 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 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. */ @@ -151,4 +172,127 @@ private void writeBlankNode(Writer writer, Resource blankNode) throws IOExceptio 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 index c5fa45291..4aadb3b84 100644 --- 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 @@ -1,10 +1,11 @@ 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 fr.inria.corese.core.next.impl.common.util.SerializationConstants; + import java.io.IOException; import java.io.Writer; import java.util.Objects; @@ -21,7 +22,6 @@ public class NTriplesFormat { */ private static final Logger logger = LoggerFactory.getLogger(NTriplesFormat.class); - private final Model model; private final FormatConfig config; @@ -32,7 +32,6 @@ public class NTriplesFormat { * @throws NullPointerException if the provided model is null. */ public NTriplesFormat(Model model) { - this(model, new FormatConfig.Builder().build()); } @@ -59,7 +58,6 @@ public void write(Writer writer) throws SerializationException { try { for (Statement stmt : model) { writeStatement(writer, stmt); - } writer.flush(); } catch (IOException e) { @@ -89,7 +87,8 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { Resource context = stmt.getContext(); if (context != null) { - logger.warn("N-Triples format does not support named graphs. Context '{}' will be ignored for statement: {}", context.stringValue(), stmt); + logger.warn("N-Triples format does not support named graphs. Context '{}' will be ignored for statement: {}", + context.stringValue(), stmt); } writer.write(SerializationConstants.SPACE_POINT); @@ -105,13 +104,10 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { * @throws IllegalArgumentException if the provided value is null or an unsupported type. */ private void writeValue(Writer writer, Value value) throws IOException { - 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"); - } + validateValue(value); if (value.isLiteral()) { - writer.write(value.stringValue()); + writeLiteral(writer, (Literal) value); } else if (value.isResource()) { if (value.isIRI()) { writeIRI(writer, (IRI) value); @@ -121,11 +117,35 @@ private void writeValue(Writer writer, Value value) throws IOException { 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); + + + if (literal.getLanguage().isPresent()) { + writer.write(SerializationConstants.AT_SIGN + literal.getLanguage().get()); + } else { + 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. @@ -136,7 +156,7 @@ private void writeValue(Writer writer, Value value) throws IOException { */ private void writeIRI(Writer writer, IRI iri) throws IOException { writer.write(SerializationConstants.LT); - writer.write(iri.stringValue()); + writer.write(escapeIRI(iri.stringValue())); writer.write(SerializationConstants.GT); } @@ -144,7 +164,7 @@ private void writeIRI(Writer writer, IRI iri) throws IOException { * 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 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. */ @@ -152,4 +172,127 @@ private void writeBlankNode(Writer writer, Resource blankNode) throws IOExceptio 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/util/SerializationConstants.java b/src/main/java/fr/inria/corese/core/next/impl/common/util/SerializationConstants.java index 376a22bf9..54dfdca54 100644 --- 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 @@ -49,11 +49,35 @@ public class SerializationConstants { * 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() {} + private SerializationConstants() { + } } \ 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 index f6e71edd7..26a455484 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -13,6 +14,7 @@ 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; @@ -28,6 +30,12 @@ class NTriplesFormatTest { private IRI mockExName; private IRI mockExAge; private IRI mockExKnows; + + private final String LEX_JOHN = "John Doe"; + private final String LEX_30 = "30"; + private final String LEX_HELLO = "Hello"; + private final String LEX_TRUE = "true"; + private Literal mockLiteralJohn; private Literal mockLiteral30; private Literal mockLiteralHelloEn; @@ -47,10 +55,12 @@ void setUp() { mockExAge = createIRI("http://example.org/age"); mockExKnows = createIRI("http://example.org/knows"); - mockLiteralJohn = createLiteral("John Doe"); - mockLiteral30 = createLiteral("30", "http://www.w3.org/2001/XMLSchema#integer", null); - mockLiteralHelloEn = createLiteral("Hello", null, "en"); - mockLiteralTrue = createLiteral("true", "http://www.w3.org/2001/XMLSchema#boolean", null); + + mockLiteralJohn = createLiteral(LEX_JOHN, null, null); + + mockLiteralHelloEn = createLiteral(LEX_HELLO, null, "en"); + + mockBNode1 = createBlankNode("b1"); mockBNode2 = createBlankNode("b2"); @@ -59,20 +69,19 @@ void setUp() { @Test @DisplayName("Constructor should throw NullPointerException for null model") void constructorShouldThrowForNullModel() { - assertThrows(NullPointerException.class, () -> new NTriplesFormat(null)); - assertThrows(NullPointerException.class, () -> new NTriplesFormat(null, config)); + 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)); + 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, @@ -81,18 +90,20 @@ void writeShouldSerializeSimpleStatement() throws SerializationException { when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - - nTriplesFormat.write(writer); - assertEquals(" \"John Doe\" .\n", writer.toString()); + String expected = String.format("<%s> <%s> \"%s\" .\n", + mockExPerson.stringValue(), + mockExName.stringValue(), + escapeNTriplesString(LEX_JOHN)); + + 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, @@ -103,17 +114,19 @@ void writeShouldSerializeStatementWithContext() throws SerializationException { when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - nTriplesFormat.write(writer); + String expected = String.format("<%s> <%s> \"%s\" .\n", + mockExPerson.stringValue(), + mockExName.stringValue(), + escapeNTriplesString(LEX_JOHN)); - assertEquals(" \"John Doe\" .\n", writer.toString()); + assertEquals(expected, writer.toString()); } @Test @DisplayName("Write should handle blank nodes with default prefix") void writeShouldHandleBlankNodes() throws SerializationException { - Statement stmt = createStatement( mockBNode1, mockExKnows, @@ -122,18 +135,19 @@ void writeShouldHandleBlankNodes() throws SerializationException { when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - - nTriplesFormat.write(writer); + String expected = String.format("_:%s <%s> _:%s .\n", + mockBNode1.stringValue(), + mockExKnows.stringValue(), + mockBNode2.stringValue()); - assertEquals("_:b1 _:b2 .\n", writer.toString()); + 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); @@ -145,17 +159,19 @@ void writeShouldHandleBlankNodesWithCustomPrefix() throws SerializationException when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - - customSerializer.write(writer); - assertEquals("genid-b1 genid-b2 .\n", writer.toString()); + String expected = String.format("genid-%s <%s> genid-%s .\n", + mockBNode1.stringValue(), + mockExKnows.stringValue(), + mockBNode2.stringValue()); + + assertEquals(expected, writer.toString()); } @Test @DisplayName("Write should throw SerializationException on IO error") void writeShouldThrowOnIOException() throws IOException { - Statement stmt = createStatement( mockExPerson, mockExName, @@ -171,9 +187,8 @@ void writeShouldThrowOnIOException() throws IOException { } @Test - @DisplayName("Write should throw SerializationException on null subject value") + @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); @@ -181,15 +196,12 @@ void writeShouldThrowOnNullSubjectValue() { 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") + @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); @@ -197,15 +209,12 @@ void writeShouldThrowOnNullPredicateValue() { 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") + @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); @@ -213,8 +222,6 @@ void writeShouldThrowOnNullObjectValue() { when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - - assertThrows(SerializationException.class, () -> nTriplesFormat.write(writer)); } @@ -225,19 +232,13 @@ void writeShouldThrowOnNullObjectValue() { "literal with \\ backslash", "literal with \n newline", "literal with \t tab", - "literal with \r carriage return" + "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 = mock(Literal.class); - when(literalMock.isLiteral()).thenReturn(true); - when(literalMock.isResource()).thenReturn(false); - - StringBuilder sb = new StringBuilder(); - sb.append("\"").append(escapeNTriplesString(literalValue)).append("\""); - when(literalMock.stringValue()).thenReturn(sb.toString()); + Literal literalMock = createLiteral(literalValue, null, null); Statement stmt = createStatement( mockExPerson, @@ -247,21 +248,24 @@ void writeShouldHandleVariousLiterals(String literalValue) throws SerializationE when(model.iterator()).thenReturn(new MockStatementIterator(stmt)); StringWriter writer = new StringWriter(); - nTriplesFormat.write(writer); String expectedEscapedLiteral = escapeNTriplesString(literalValue); - assertEquals(String.format(" \"%s\" .\n", expectedEscapedLiteral), writer.toString()); + String expectedOutput = String.format("<%s> <%s> \"%s\" .\n", + mockExPerson.stringValue(), + mockExName.stringValue(), + expectedEscapedLiteral); + + assertEquals(expectedOutput, writer.toString()); } @Test @DisplayName("Write should handle multiple statements") void writeShouldHandleMultipleStatements() throws SerializationException { - Statement stmt1 = createStatement( mockExPerson, mockExName, - createLiteral("o1") + createLiteral("o1", null, null) ); Statement stmt2 = createStatement( mockBNode1, @@ -272,17 +276,19 @@ void writeShouldHandleMultipleStatements() throws SerializationException { when(model.iterator()).thenReturn(new MockStatementIterator(stmt1, stmt2)); StringWriter writer = new StringWriter(); - nTriplesFormat.write(writer); - assertEquals(" \"o1\" .\n" + - "_:b1 .\n", writer.toString()); + String expectedOutput = String.format("<%s> <%s> \"%s\" .\n", + mockExPerson.stringValue(), mockExName.stringValue(), escapeNTriplesString("o1")) + + String.format("_:%s <%s> <%s> .\n", + mockBNode1.stringValue(), mockExKnows.stringValue(), mockExPerson.stringValue()); + + 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); @@ -292,25 +298,11 @@ void shouldHandleLiteralsWithLanguageTags() throws SerializationException { NTriplesFormat serializer = new NTriplesFormat(currentTestModel); serializer.write(writer); - String expectedOutput = " \"Hello\"@en .\n"; - - assertEquals(expectedOutput, writer.toString()); - } - - @Test - @DisplayName("Should handle literals with datatype IRIs") - void shouldHandleLiteralsWithDatatypeIRIs() throws SerializationException { - - Statement stmt = createStatement(mockExPerson, mockExAge, mockLiteral30); - - 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 = " \"30\"^^ .\n"; + String expectedOutput = String.format("<%s> <%s> \"%s\"@%s .\n", + mockExPerson.stringValue(), + createIRI("http://example.org/greeting").stringValue(), + escapeNTriplesString(LEX_HELLO), + mockLiteralHelloEn.getLanguage().get()); assertEquals(expectedOutput, writer.toString()); } @@ -338,7 +330,6 @@ private Resource createBlankNode(String id) { return blankNode; } - private IRI createIRI(String uri) { IRI iri = mock(IRI.class); when(iri.isResource()).thenReturn(true); @@ -348,36 +339,78 @@ private IRI createIRI(String uri) { return iri; } - - private Literal createLiteral(String lexicalForm) { - return createLiteral(lexicalForm, null, null); - } - - - private Literal createLiteral(String lexicalForm, String dataType, String langTag) { + /** + * 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); - - StringBuilder sb = new StringBuilder(); - sb.append("\"").append(escapeNTriplesString(lexicalForm)).append("\""); if (langTag != null && !langTag.isEmpty()) { - sb.append("@").append(langTag); - } else if (dataType != null && !dataType.isEmpty()) { - sb.append("^^<").append(dataType).append(">"); + 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); } - when(literal.stringValue()).thenReturn(sb.toString()); return literal; } - + /** + * 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) { - return s.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + 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(); } @@ -399,4 +432,4 @@ public Statement next() { return statements[index++]; } } -} +} \ No newline at end of file From 23b139549604921967d72a770af8277ba551cf50 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 15:24:43 +0200 Subject: [PATCH 10/19] correction les erreur de sonar sur TUs --- .../serialization/NTriplesFormatTest.java | 177 +++++++++--------- 1 file changed, 88 insertions(+), 89 deletions(-) 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 index 26a455484..e00d89d41 100644 --- 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 @@ -28,18 +28,14 @@ class NTriplesFormatTest { private Resource mockExPerson; private IRI mockExName; - private IRI mockExAge; private IRI mockExKnows; - private final String LEX_JOHN = "John Doe"; - private final String LEX_30 = "30"; - private final String LEX_HELLO = "Hello"; - private final String LEX_TRUE = "true"; + private final String lexJohn = "John Doe"; + private final String hello = "Hello"; private Literal mockLiteralJohn; - private Literal mockLiteral30; + private Literal mockLiteralHelloEn; - private Literal mockLiteralTrue; private Resource mockBNode1; private Resource mockBNode2; @@ -52,15 +48,13 @@ void setUp() { mockExPerson = createIRI("http://example.org/Person"); mockExName = createIRI("http://example.org/name"); - mockExAge = createIRI("http://example.org/age"); - mockExKnows = createIRI("http://example.org/knows"); + mockExKnows = createIRI("http://example.org/knows"); - mockLiteralJohn = createLiteral(LEX_JOHN, null, null); - - mockLiteralHelloEn = createLiteral(LEX_HELLO, null, "en"); + mockLiteralJohn = createLiteral(lexJohn, null, null); + mockLiteralHelloEn = createLiteral(hello, null, "en"); mockBNode1 = createBlankNode("b1"); mockBNode2 = createBlankNode("b2"); @@ -93,10 +87,10 @@ void writeShouldSerializeSimpleStatement() throws SerializationException { nTriplesFormat.write(writer); - String expected = String.format("<%s> <%s> \"%s\" .\n", + String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNTriplesString(LEX_JOHN)); + escapeNTriplesString(lexJohn)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -116,10 +110,10 @@ void writeShouldSerializeStatementWithContext() throws SerializationException { StringWriter writer = new StringWriter(); nTriplesFormat.write(writer); - String expected = String.format("<%s> <%s> \"%s\" .\n", + String expected = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - escapeNTriplesString(LEX_JOHN)); + escapeNTriplesString(lexJohn)) + " .\n"; assertEquals(expected, writer.toString()); } @@ -137,10 +131,10 @@ void writeShouldHandleBlankNodes() throws SerializationException { StringWriter writer = new StringWriter(); nTriplesFormat.write(writer); - String expected = String.format("_:%s <%s> _:%s .\n", + String expected = String.format("_:%s <%s> _:%s", mockBNode1.stringValue(), mockExKnows.stringValue(), - mockBNode2.stringValue()); + mockBNode2.stringValue()) + " .\n"; assertEquals(expected, writer.toString()); } @@ -161,10 +155,10 @@ void writeShouldHandleBlankNodesWithCustomPrefix() throws SerializationException StringWriter writer = new StringWriter(); customSerializer.write(writer); - String expected = String.format("genid-%s <%s> genid-%s .\n", + String expected = String.format("genid-%s <%s> genid-%s", mockBNode1.stringValue(), mockExKnows.stringValue(), - mockBNode2.stringValue()); + mockBNode2.stringValue()) + " .\n"; assertEquals(expected, writer.toString()); } @@ -251,10 +245,10 @@ void writeShouldHandleVariousLiterals(String literalValue) throws SerializationE nTriplesFormat.write(writer); String expectedEscapedLiteral = escapeNTriplesString(literalValue); - String expectedOutput = String.format("<%s> <%s> \"%s\" .\n", + String expectedOutput = String.format("<%s> <%s> \"%s\"", mockExPerson.stringValue(), mockExName.stringValue(), - expectedEscapedLiteral); + expectedEscapedLiteral) + " .\n"; assertEquals(expectedOutput, writer.toString()); } @@ -278,10 +272,14 @@ void writeShouldHandleMultipleStatements() throws SerializationException { StringWriter writer = new StringWriter(); nTriplesFormat.write(writer); - String expectedOutput = String.format("<%s> <%s> \"%s\" .\n", - mockExPerson.stringValue(), mockExName.stringValue(), escapeNTriplesString("o1")) + - String.format("_:%s <%s> <%s> .\n", - mockBNode1.stringValue(), mockExKnows.stringValue(), mockExPerson.stringValue()); + 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()); } @@ -298,76 +296,16 @@ void shouldHandleLiteralsWithLanguageTags() throws SerializationException { NTriplesFormat serializer = new NTriplesFormat(currentTestModel); serializer.write(writer); - String expectedOutput = String.format("<%s> <%s> \"%s\"@%s .\n", + String expectedOutput = String.format("<%s> <%s> \"%s\"@%s", mockExPerson.stringValue(), createIRI("http://example.org/greeting").stringValue(), - escapeNTriplesString(LEX_HELLO), - mockLiteralHelloEn.getLanguage().get()); + escapeNTriplesString(hello), + mockLiteralHelloEn.getLanguage().get()) + " .\n"; assertEquals(expectedOutput, writer.toString()); } - 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; - } - - /** - * 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; - } - /** * Escapes a string according to N-Triples literal escaping rules. * This helper is used in tests to construct the *expected* output strings. @@ -432,4 +370,65 @@ 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 From f5bbfcfad042dd08169aa5b7d428c59a8649d2ff Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 16:16:21 +0200 Subject: [PATCH 11/19] correction les erreur de sonar --- .../common/serialization/FormatConfig.java | 7 +- .../common/serialization/NQuadsFormat.java | 15 +- .../common/serialization/NTriplesFormat.java | 18 +- .../serialization/NQuadsFormatTest.java | 457 ++++++++++++++++++ 4 files changed, 484 insertions(+), 13 deletions(-) create mode 100644 src/test/java/fr/inria/corese/core/next/impl/common/serialization/NQuadsFormatTest.java 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 index 5c6df301c..0e85acff7 100644 --- 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 @@ -37,8 +37,11 @@ public static class Builder { private String blankNodePrefix = "_:"; - - public Builder() { + /** + * Default constructor for the Builder. + * Initializes fields with default values. + */ + Builder() { } 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 index da3f76ca6..598c8daa5 100644 --- 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 @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.io.Writer; import java.util.Objects; @@ -61,10 +62,8 @@ public void write(Writer writer) throws SerializationException { } writer.flush(); } catch (IOException e) { - logger.error("An I/O error occurred during N-Quads serialization: {}", e.getMessage(), e); throw new SerializationException("Failed to write", "NQuads", e); } catch (IllegalArgumentException e) { - logger.error("Invalid data encountered during N-Quads serialization: {}", e.getMessage(), e); throw new SerializationException("Invalid data: " + e.getMessage(), "NQuads", e); } } @@ -134,10 +133,16 @@ private void writeLiteral(Writer writer, Literal literal) throws IOException { 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()) { - writer.write(SerializationConstants.AT_SIGN + literal.getLanguage().get()); - } else { + if (!literal.getLanguage().isPresent()) { IRI datatype = literal.getDatatype(); if (datatype != null && !datatype.stringValue().equals(SerializationConstants.XSD_STRING)) { writer.write(SerializationConstants.DATATYPE_SEPARATOR); 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 index 4aadb3b84..81bf9bd4d 100644 --- 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 @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.io.Writer; import java.util.Objects; @@ -61,10 +62,8 @@ public void write(Writer writer) throws SerializationException { } writer.flush(); } catch (IOException e) { - logger.error("An I/O error occurred during N-Triples serialization: {}", e.getMessage(), e); throw new SerializationException("Failed to write", "NTriples", e); } catch (IllegalArgumentException e) { - logger.error("Invalid data encountered during N-Triples serialization: {}", e.getMessage(), e); throw new SerializationException("Invalid data: " + e.getMessage(), "NTriples", e); } } @@ -86,9 +85,10 @@ private void writeStatement(Writer writer, Statement stmt) throws IOException { writeValue(writer, stmt.getObject()); Resource context = stmt.getContext(); - if (context != null) { + 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); @@ -134,10 +134,16 @@ private void writeLiteral(Writer writer, Literal literal) throws IOException { writer.write(escapeLiteral(literal.stringValue())); writer.write(SerializationConstants.QUOTE); + literal.getLanguage().ifPresent(lang -> { + try { + writer.write(SerializationConstants.AT_SIGN + lang); + } catch (IOException e) { - if (literal.getLanguage().isPresent()) { - writer.write(SerializationConstants.AT_SIGN + literal.getLanguage().get()); - } else { + 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); 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 From 015d2466714dcf083287cbc60ee0e607475e3c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 12 Jun 2025 17:01:03 +0200 Subject: [PATCH 12/19] Remove AbstractBNode class as part of code cleanup --- .../corese/core/next/api/AbstractBNode.java | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 src/main/java/fr/inria/corese/core/next/api/AbstractBNode.java 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; - } -} From cdcee5c9d377e0165c24d9d9d3c0b66dac581a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 12 Jun 2025 17:04:53 +0200 Subject: [PATCH 13/19] Add FileFormat and RdfFormat classes for RDF serialization support --- .../impl/common/serialization/FileFormat.java | 112 ++++++++++++++++++ .../impl/common/serialization/RdfFormat.java | 84 +++++++++++++ .../impl/common/serialization/RdfFormats.java | 89 ++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/serialization/FileFormat.java create mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java create mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java 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..b10662eaa --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/FileFormat.java @@ -0,0 +1,112 @@ +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. + * + * This class is typically used to describe RDF serialization formats (e.g., + * Turtle, N-Triples), + * but can also apply to other text or binary formats. + * + * Example: + * name = "Turtle" + * extensions = ["ttl"] + * mimeTypes = ["text/turtle"] + */ +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 (e.g., "Turtle"). + * @param extensions The list of file extensions (e.g., ["ttl"]). + * @param mimeTypes The list of MIME types (e.g., ["text/turtle"]). + * @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 (e.g., "Turtle"). + */ + public String getName() { + return name; + } + + /** + * Returns the list of known file extensions. + * + * @return A list of extensions (e.g., ["ttl"]). + */ + public List getExtensions() { + return extensions; + } + + /** + * Returns the list of associated MIME types. + * + * @return A list of MIME types (e.g., ["text/turtle"]). + */ + 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/RdfFormat.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java new file mode 100644 index 000000000..72e98205f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormat.java @@ -0,0 +1,84 @@ +package fr.inria.corese.core.next.impl.common.serialization; + +import java.util.List; +import java.util.Objects; + +/** + * Describes a semantic RDF serialization format, extending file-level metadata + * with RDF-specific capabilities. + */ +public class RdfFormat extends FileFormat { + + private final boolean supportsNamespaces; + private final boolean supportsNamedGraphs; + + /** + * 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). + */ + public boolean supportsNamespaces() { + return supportsNamespaces; + } + + /** + * Whether the format supports named graphs (e.g., TriG, N-Quads). + */ + public boolean supportsNamedGraphs() { + return supportsNamedGraphs; + } + + @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); + } + +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java new file mode 100644 index 000000000..6304fda87 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java @@ -0,0 +1,89 @@ +package fr.inria.corese.core.next.impl.common.serialization; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +/** + * Provides a central registry of known RDF serialization formats, + * along with utility methods for lookup by extension or MIME type. + */ +public final class RdfFormats { + + public static final RdfFormat TURTLE = new RdfFormat( + "Turtle", + List.of("ttl"), + List.of("text/turtle"), + true, + true); + + 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 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, + true); + + /** + * 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 all known RDF formats. + */ + public static List all() { + return List.of(TURTLE, NTRIPLES, JSONLD, RDFXML); + } + +} From 0e61739da89e5f50e7b498216dd73dcbc9c9c279 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Thu, 12 Jun 2025 17:17:14 +0200 Subject: [PATCH 14/19] Create NTriples/NQuads Serializer --- .../core/next/api/FormatSerializer.java | 20 +++++++ .../common/serialization/NQuadsFormat.java | 3 +- .../common/serialization/NTriplesFormat.java | 3 +- .../impl/common/serialization/RdfFormats.java | 9 ++- .../impl/common/serialization/Serializer.java | 55 +++++++++++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java create mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/serialization/Serializer.java 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..cb9d1b46d --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java @@ -0,0 +1,20 @@ +package fr.inria.corese.core.next.api; + +import fr.inria.corese.core.next.impl.exception.SerializationException; + + +public interface FormatSerializer { + + /** + * Writes the RDF data (model/dataset) represented by this serializer instance + * to the given in its specific serialization format. + * The implementation must handle the formatting rules of the target RDF syntax. + * + * @param writer the which the serialized RDF data will be written. + * The writer will be flushed after writing. + * @throws SerializationException if an error occurs during the serialization process, + * such as an I/O error or if invalid data is encountered + * that cannot be serialized to the target format. + */ + void write(java.io.Writer writer) throws SerializationException; +} \ 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 index 598c8daa5..daa921cf0 100644 --- 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 @@ -16,7 +16,7 @@ * 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 { +public class NQuadsFormat implements FormatSerializer { /** * Logger for this class, used for logging potential issues or information during serialization. @@ -55,6 +55,7 @@ public NQuadsFormat(Model model, FormatConfig config) { * @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) { 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 index 81bf9bd4d..fc20c0e92 100644 --- 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 @@ -16,7 +16,7 @@ * 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 { +public class NTriplesFormat implements FormatSerializer { /** * Logger for this class, used for logging potential issues or information during serialization. @@ -55,6 +55,7 @@ public NTriplesFormat(Model model, FormatConfig config) { * @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) { diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java index 6304fda87..9eb5a0496 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java +++ b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java @@ -38,6 +38,13 @@ public final class RdfFormats { true, true); + public static final RdfFormat NQUADS = new RdfFormat( + "N-Quads", + List.of("nq"), + List.of("application/n-quads"), + false, + true); + /** * Finds a known RDF format by its name (case-insensitive). * @@ -83,7 +90,7 @@ public static Optional byMimeType(String mimeType) { * Returns all known RDF formats. */ public static List all() { - return List.of(TURTLE, NTRIPLES, JSONLD, RDFXML); + return List.of(TURTLE, NTRIPLES, JSONLD, RDFXML, NQUADS); } } 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..4b01b2956 --- /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(RdfFormats.NTRIPLES)) { + formatSerializer = new NTriplesFormat(model, config); + } else if (format.equals(RdfFormats.NQUADS)) { + formatSerializer = new NQuadsFormat(model, config); + } else if (format.equals(RdfFormats.TURTLE)) { + + throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented."); + } else if (format.equals(RdfFormats.JSONLD)) { + throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented."); + } else if (format.equals(RdfFormats.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 From 4a8fcb418ef77d1c57ce0d010be45e5759ac3ede Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 13 Jun 2025 10:38:41 +0200 Subject: [PATCH 15/19] ajouter test unitaire sur Serializer --- .../common/serialization/SerializerTest.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/test/java/fr/inria/corese/core/next/impl/common/serialization/SerializerTest.java 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..30c9df4ee --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/SerializerTest.java @@ -0,0 +1,95 @@ +package fr.inria.corese.core.next.impl.common.serialization; + +import fr.inria.corese.core.next.api.Model; +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.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockitoAnnotations; + +import java.io.Writer; + +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; + +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); + } + + // --- Tests des constructeurs --- + + @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 des arguments de la méthode serialize --- + + @Test + @DisplayName("serialize should throw NullPointerException for null writer") + void serializeShouldThrowForNullWriter() { + assertThrows(NullPointerException.class, () -> serializer.serialize(null, RdfFormats.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"); + } + + // --- Tests de délégation de sérialisation --- + + @Test + @DisplayName("serialize should delegate to NTriplesFormat for NTRIPLES format") + void serializeShouldDelegateToNTriplesFormat() throws SerializationException { + try (MockedConstruction mockedNtConstructor = mockConstruction(NTriplesFormat.class)) { + serializer.serialize(mockWriter, RdfFormats.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, RdfFormats.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 From dd48b2e11f103b3e670508f64b6d3b3a8d373715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Fri, 13 Jun 2025 11:33:27 +0200 Subject: [PATCH 16/19] Add unit tests for FileFormat class --- .../common/serialization/FileFormatTest.java | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/test/java/fr/inria/corese/core/next/impl/common/serialization/FileFormatTest.java 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"); + } +} From d273449703b5494acc949de20bfcfd93928e21d2 Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 13 Jun 2025 15:08:09 +0200 Subject: [PATCH 17/19] ajouter test unitaire sur Serializer et fusionner rdfFormet --- .../impl/common/serialization/RdfFormat.java | 100 +++++++++++++++++- .../impl/common/serialization/RdfFormats.java | 96 ----------------- .../impl/common/serialization/Serializer.java | 10 +- .../common/serialization/RdfFormatTest.java | 4 + .../common/serialization/SerializerTest.java | 6 +- 5 files changed, 108 insertions(+), 108 deletions(-) delete mode 100644 src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java create mode 100644 src/test/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormatTest.java 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 index 72e98205f..fbea74d93 100644 --- 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 @@ -1,17 +1,57 @@ 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. * @@ -35,7 +75,9 @@ public RdfFormat( } /** - * Whether the format supports RDF prefixes (e.g., Turtle). + * 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; @@ -48,6 +90,57 @@ 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( @@ -79,6 +172,5 @@ public int hashCode() { getMimeTypes(), supportsNamespaces, supportsNamedGraphs); - } - -} + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java b/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java deleted file mode 100644 index 9eb5a0496..000000000 --- a/src/main/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormats.java +++ /dev/null @@ -1,96 +0,0 @@ -package fr.inria.corese.core.next.impl.common.serialization; - -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -/** - * Provides a central registry of known RDF serialization formats, - * along with utility methods for lookup by extension or MIME type. - */ -public final class RdfFormats { - - public static final RdfFormat TURTLE = new RdfFormat( - "Turtle", - List.of("ttl"), - List.of("text/turtle"), - true, - true); - - 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 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, - true); - - public static final RdfFormat NQUADS = new RdfFormat( - "N-Quads", - List.of("nq"), - List.of("application/n-quads"), - false, - true); - - /** - * 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 all known RDF formats. - */ - public static List all() { - return List.of(TURTLE, NTRIPLES, JSONLD, RDFXML, NQUADS); - } - -} 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 index 4b01b2956..37427b832 100644 --- 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 @@ -35,16 +35,16 @@ public void serialize(Writer writer, RdfFormat format) throws SerializationExcep FormatSerializer formatSerializer; - if (format.equals(RdfFormats.NTRIPLES)) { + if (format.equals(RdfFormat.NTRIPLES)) { formatSerializer = new NTriplesFormat(model, config); - } else if (format.equals(RdfFormats.NQUADS)) { + } else if (format.equals( RdfFormat.NQUADS)) { formatSerializer = new NQuadsFormat(model, config); - } else if (format.equals(RdfFormats.TURTLE)) { + } else if (format.equals( RdfFormat.TURTLE)) { throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented."); - } else if (format.equals(RdfFormats.JSONLD)) { + } else if (format.equals( RdfFormat.JSONLD)) { throw new UnsupportedOperationException("Serialization to " + format.getName() + " format is not yet implemented."); - } else if (format.equals(RdfFormats.RDFXML)) { + } 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()); 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..6188f91da --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/common/serialization/RdfFormatTest.java @@ -0,0 +1,4 @@ +package fr.inria.corese.core.next.impl.common.serialization; + +public class RdfFormatTest { +} 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 index 30c9df4ee..f71318741 100644 --- 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 @@ -53,7 +53,7 @@ void constructorShouldThrowForNullConfig() { @Test @DisplayName("serialize should throw NullPointerException for null writer") void serializeShouldThrowForNullWriter() { - assertThrows(NullPointerException.class, () -> serializer.serialize(null, RdfFormats.NTRIPLES), "Writer cannot be null"); + assertThrows(NullPointerException.class, () -> serializer.serialize(null, RdfFormat.NTRIPLES), "Writer cannot be null"); } @Test @@ -68,7 +68,7 @@ void serializeShouldThrowForNullFormat() { @DisplayName("serialize should delegate to NTriplesFormat for NTRIPLES format") void serializeShouldDelegateToNTriplesFormat() throws SerializationException { try (MockedConstruction mockedNtConstructor = mockConstruction(NTriplesFormat.class)) { - serializer.serialize(mockWriter, RdfFormats.NTRIPLES); + serializer.serialize(mockWriter, RdfFormat.NTRIPLES); assertEquals(1, mockedNtConstructor.constructed().size(), "NTriplesFormat constructor should be called once"); @@ -82,7 +82,7 @@ void serializeShouldDelegateToNTriplesFormat() throws SerializationException { @DisplayName("serialize should delegate to NQuadsFormat for NQUADS format") void serializeShouldDelegateToNQuadsFormat() throws SerializationException { try (MockedConstruction mockedNqConstructor = mockConstruction(NQuadsFormat.class)) { - serializer.serialize(mockWriter, RdfFormats.NQUADS); + serializer.serialize(mockWriter, RdfFormat.NQUADS); assertEquals(1, mockedNqConstructor.constructed().size(), "NQuadsFormat constructor should be called once"); NQuadsFormat createdNqSerializer = mockedNqConstructor.constructed().get(0); From 896cd43d37a5e247fd72a2c5a6225af2d5ac404f Mon Sep 17 00:00:00 2001 From: "AD\\aabdoun" Date: Fri, 13 Jun 2025 15:58:35 +0200 Subject: [PATCH 18/19] ajouter test unitaire sur rdfFormet --- .../common/serialization/RdfFormatTest.java | 300 +++++++++++++++++- 1 file changed, 298 insertions(+), 2 deletions(-) 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 index 6188f91da..8f0df66d0 100644 --- 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 @@ -1,4 +1,300 @@ package fr.inria.corese.core.next.impl.common.serialization; -public class RdfFormatTest { -} +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 From 4a404ab4469ee76d40fbebcf9aeff9622bef3b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Thu, 19 Jun 2025 11:21:44 +0200 Subject: [PATCH 19/19] Refactor FormatSerializer and FileFormat documentation; update SerializerTest for clarity --- .../core/next/api/FormatSerializer.java | 22 +++++++------ .../inria/corese/core/next/api/Resource.java | 5 ++- .../impl/common/serialization/FileFormat.java | 25 +++++--------- .../common/serialization/SerializerTest.java | 33 ++++++++++--------- 4 files changed, 40 insertions(+), 45 deletions(-) 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 index cb9d1b46d..4741a9712 100644 --- a/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/api/FormatSerializer.java @@ -1,20 +1,22 @@ package fr.inria.corese.core.next.api; -import fr.inria.corese.core.next.impl.exception.SerializationException; +import java.io.Writer; +import fr.inria.corese.core.next.impl.exception.SerializationException; public interface FormatSerializer { /** - * Writes the RDF data (model/dataset) represented by this serializer instance - * to the given in its specific serialization format. - * The implementation must handle the formatting rules of the target RDF syntax. + * 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 which the serialized RDF data will be written. - * The writer will be flushed after writing. - * @throws SerializationException if an error occurs during the serialization process, - * such as an I/O error or if invalid data is encountered - * that cannot be serialized to the target format. + * @param writer the destination {@link Writer} for the serialized + * output + * @throws SerializationException if an error occurs during the serialization + * process */ - void write(java.io.Writer writer) throws SerializationException; + 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 920bf285b..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 { @@ -13,6 +14,4 @@ default boolean isResource() { return true; } - - } 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 index b10662eaa..8242799b9 100644 --- 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 @@ -4,17 +4,8 @@ import java.util.Objects; /** - * Represents a general file format, including its name, - * associated file extensions, and MIME types. - * - * This class is typically used to describe RDF serialization formats (e.g., - * Turtle, N-Triples), - * but can also apply to other text or binary formats. - * - * Example: - * name = "Turtle" - * extensions = ["ttl"] - * mimeTypes = ["text/turtle"] + * Represents a general file format, including its name, associated file + * extensions, and MIME types. */ public class FileFormat { @@ -25,9 +16,9 @@ public class FileFormat { /** * Constructs a new FileFormat instance. * - * @param name The human-readable name of the format (e.g., "Turtle"). - * @param extensions The list of file extensions (e.g., ["ttl"]). - * @param mimeTypes The list of MIME types (e.g., ["text/turtle"]). + * @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. */ @@ -47,7 +38,7 @@ public FileFormat(String name, List extensions, List mimeTypes) /** * Returns the name of the format. * - * @return The format name (e.g., "Turtle"). + * @return The format name. */ public String getName() { return name; @@ -56,7 +47,7 @@ public String getName() { /** * Returns the list of known file extensions. * - * @return A list of extensions (e.g., ["ttl"]). + * @return A list of extensions. */ public List getExtensions() { return extensions; @@ -65,7 +56,7 @@ public List getExtensions() { /** * Returns the list of associated MIME types. * - * @return A list of MIME types (e.g., ["text/turtle"]). + * @return A list of MIME types. */ public List getMimeTypes() { return mimeTypes; 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 index f71318741..0e8700810 100644 --- 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 @@ -1,7 +1,12 @@ package fr.inria.corese.core.next.impl.common.serialization; -import fr.inria.corese.core.next.api.Model; -import fr.inria.corese.core.next.impl.exception.SerializationException; +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; @@ -9,12 +14,8 @@ import org.mockito.MockedConstruction; import org.mockito.MockitoAnnotations; -import java.io.Writer; - -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 fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.impl.exception.SerializationException; class SerializerTest { @@ -33,7 +34,7 @@ void setUp() { serializer = new Serializer(mockModel, mockConfig); } - // --- Tests des constructeurs --- + // --- Constructor tests --- @Test @DisplayName("Constructor should throw NullPointerException for null model") @@ -48,21 +49,23 @@ void constructorShouldThrowForNullConfig() { assertThrows(NullPointerException.class, () -> new Serializer(mockModel, null), "FormatConfig cannot be null"); } - // --- Tests des arguments de la méthode serialize --- + // --- 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"); + 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"); + assertThrows(NullPointerException.class, () -> serializer.serialize(mockWriter, null), + "RdfFormat cannot be null"); } - // --- Tests de délégation de sérialisation --- + // --- Serialization delegation tests --- @Test @DisplayName("serialize should delegate to NTriplesFormat for NTRIPLES format") @@ -70,7 +73,8 @@ 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"); + assertEquals(1, mockedNtConstructor.constructed().size(), + "NTriplesFormat constructor should be called once"); NTriplesFormat createdNtSerializer = mockedNtConstructor.constructed().get(0); @@ -91,5 +95,4 @@ void serializeShouldDelegateToNQuadsFormat() throws SerializationException { } } - } \ No newline at end of file