([\\w\\-_]+))?)?$");
private static final Pattern STANDARD_IRI_PATTERN = Pattern.compile("^(([^:/?#\\s]+):)(\\/\\/([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(.*))?");
+
/**
* Prevent instantiation of the utility class.
*/
@@ -29,15 +30,24 @@ public static String guessNamespace(String iri) {
Matcher matcher = IRI_PATTERN.matcher(iri);
if(matcher.matches()) {
- if((matcher.group(8) == null) || (matcher.group(6) == null && matcher.group(9) == null) ) { // If the IRI has no fragment or ends with a slash
-
- return matcher.group(1);
- } else {
- // 1: Domain and path ending with a slash, 6: final path element without slash, 9: final # if there is a fragment
- return matcher.group(1) + matcher.group(6) + matcher.group(9);
+ if(matcher.group("protocol") != null && matcher.group("protocol").equals("_")) {
+ return "";
+ }
+ StringBuilder namespace = new StringBuilder();
+ namespace.append(matcher.group("protocol")).append(":");
+ if(matcher.group("dblSlashes") != null) {
+ namespace.append(matcher.group("dblSlashes"));
}
+ namespace.append(matcher.group("domain"));
+ if(matcher.group("path") != null) {
+ namespace.append(matcher.group("path"));
+ }
+ if(matcher.group("fragment") != null && matcher.group("finalPath") != null) {
+ namespace.append(matcher.group("finalPath")).append("#");
+ }
+ return namespace.toString();
} else {
- return "";
+ throw new IllegalStateException("No namespace found for the given IRI: " + iri + ".");
}
} catch (IllegalStateException e) {
return "";
@@ -54,10 +64,10 @@ public static String guessLocalName(String iri) {
Matcher matcher = IRI_PATTERN.matcher(iri);
if(matcher.matches()) {
- if(matcher.group(10) != null){ // If the IRI has a fragment
- return matcher.group(10);
- } else if(matcher.group(6) != null ) { // If the IRI has no fragment but do not ends with a slash
- return matcher.group(6);
+ if(matcher.group("fragment") != null){ // If the IRI has a fragment
+ return matcher.group("fragment");
+ } else if(matcher.group("finalPath") != null ) { // If the IRI has no fragment but do not ends with a slash
+ return matcher.group("finalPath");
} else { // If the URI ends with a slash
return "";
}
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java
index 8b43d930a..f49af0569 100644
--- a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java
@@ -9,6 +9,7 @@
import fr.inria.corese.core.next.impl.io.parser.jsonld.JSONLDParser;
import fr.inria.corese.core.next.impl.io.parser.nquads.ANTLRNQuadsParser;
import fr.inria.corese.core.next.impl.io.parser.ntriples.ANTLRNTriplesParser;
+import fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLParser;
import fr.inria.corese.core.next.impl.io.parser.turtle.ANTLRTurtleParser;
/**
@@ -44,6 +45,8 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac
return new ANTLRNTriplesParser(model, factory, config);
} else if (format == RDFFormat.NQUADS) {
return new ANTLRNQuadsParser(model, factory, config);
+ } else if (format == RDFFormat.RDFXML) {
+ return new RDFXMLParser(model, factory, config);
}
throw new IllegalArgumentException("Unsupported format: " + format);
}
@@ -65,6 +68,8 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac
return new ANTLRNTriplesParser(model, factory);
} else if (format == RDFFormat.NQUADS) {
return new ANTLRNQuadsParser(model, factory);
+ } else if (format == RDFFormat.RDFXML) {
+ return new RDFXMLParser(model, factory);
}
throw new IllegalArgumentException("Unsupported format: " + format);
}
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java
new file mode 100644
index 000000000..917fa0793
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java
@@ -0,0 +1,459 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.api.base.io.RDFFormat;
+import fr.inria.corese.core.next.api.base.io.parser.AbstractRDFParser;
+import fr.inria.corese.core.next.api.io.IOOptions;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.exception.ParsingErrorException;
+import fr.inria.corese.core.next.impl.io.parser.rdfxml.context.RDFXMLContext;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+import static fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLUtils.*;
+
+/**
+ * SAX-based RDF/XML parser using a shared parsing context ({@link RDFXMLContext}).
+ *
+ * This parser processes RDF/XML documents using the SAX streaming API.
+ * It tracks RDF constructs (resources, properties, literals, containers, collections)
+ * using an explicit stack-based context, and supports features like xml:lang,
+ * rdf:datatype, rdf:parseType, and property attributes.
+ *
+ * The parser adds RDF statements to the provided {@link Model} using
+ * the supplied {@link ValueFactory}. This parser supports nested nodes,
+ * blank nodes, typed nodes, and RDF collections.
+ */
+public class RDFXMLParser extends AbstractRDFParser {
+
+ /** RDF/XML format identifier for this parser. */
+ private final RDFFormat format = RDFFormat.RDFXML;
+
+ /** Buffer for accumulating character data between start and end tags. */
+ private StringBuilder characters = new StringBuilder();
+
+ /** Shared state across SAX callbacks. */
+ private RDFXMLContext ctx;
+
+ private final RDFXMLStatementEmitter emitter;
+
+ /**
+ * Creates a new parser with a target RDF model and factory.
+ *
+ * @param model the RDF model to populate
+ * @param factory the RDF value factory for term creation
+ */
+ public RDFXMLParser(Model model, ValueFactory factory) {
+ this(model, factory, null);
+ }
+
+ /**
+ * Creates a new parser with a target RDF model, factory, and configuration options.
+ *
+ * @param model the RDF model to populate
+ * @param factory the RDF value factory for term creation
+ * @param config optional configuration options for the parser
+ */
+ public RDFXMLParser(Model model, ValueFactory factory, IOOptions config) {
+ super(model, factory, config);
+ this.ctx = new RDFXMLContext(getModel(), getValueFactory());
+ this.emitter = new RDFXMLStatementEmitter(model, factory);
+ }
+
+ @Override
+ public RDFFormat getRDFFormat() {
+ return format;
+ }
+
+ @Override
+ public void parse(InputStream in) throws ParsingErrorException {
+ parse(new InputStreamReader(in, StandardCharsets.UTF_8), null);
+ }
+
+ @Override
+ public void parse(InputStream in, String baseURI) throws ParsingErrorException {
+ parse(new InputStreamReader(in, StandardCharsets.UTF_8), baseURI);
+ }
+
+ public void parse(Reader reader) throws ParsingErrorException {
+ parse(reader, null);
+ }
+
+ @Override
+ public void parse(Reader reader, String baseURI) throws ParsingErrorException {
+ ctx.baseURI = baseURI;
+ try {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ SAXParser saxParser = factory.newSAXParser();
+ InputSource inputSource = new InputSource(reader);
+ saxParser.parse(inputSource, new RdfXmlSaxHandler());
+ }
+ catch (IOException e) {
+ throw new ParsingErrorException("Failed to parse RDF/XML input stream: " + e.getMessage() , e);
+ } catch (Exception e) {
+ throw new ParsingErrorException("Unexpected error during RDF/XML parsing: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Internal SAX handler that delegates to the parser's methods
+ */
+ private class RdfXmlSaxHandler extends DefaultHandler {
+
+ @Override
+ public void characters(char[] ch, int start, int length) {
+ RDFXMLParser.this.handleCharacters(ch, start, length);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attrs) {
+ RDFXMLParser.this.handleStartElement(uri, localName, qName, attrs);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) {
+ RDFXMLParser.this.handleEndElement(uri, localName, qName);
+ }
+ }
+
+ /**
+ * Handles character data between XML elements
+ */
+ private void handleCharacters(char[] ch, int start, int length) {
+ characters.append(ch, start, length);
+ }
+
+ /**
+ * Handles opening of an XML element.
+ * Identifies node elements, container constructs, properties,
+ * and special parseType attributes, updating the parsing context accordingly.
+ */
+ private void handleStartElement(String uri, String localName, String qName, Attributes attrs) {
+
+ // Skip the top-level rdf:RDF wrapper element
+ if (RDFXMLUtils.isRdfRDF(uri, localName)) return;
+
+ // Reset character buffer
+ characters.setLength(0);
+
+ // Handle xml:base (change base URI dynamically)
+ updateBase(attrs);
+
+ // Handle xml:lang
+ updateLang(attrs);
+
+ // Handle rdf:datatype (applies to property literal values)
+ updateDatatype(attrs);
+
+ if (processContainerElement(localName, uri, qName, attrs)) return;
+ if (processCollectionStart(localName, uri, qName, attrs)) return;
+ if (processCollectionItem(localName, uri, attrs)) return;
+ if (processNodeElement(localName, uri, qName, attrs)) return;
+ processPropertyElement(localName, uri, qName, attrs);
+ }
+
+ /**
+ * Handles the end of an XML element, emitting a literal or cleaning up context stacks.
+ */
+ private void handleEndElement(String uri, String localName, String qName) {
+ String text = characters.toString().trim();
+ characters.setLength(0);
+
+ if (!ctx.predicateStack.isEmpty() && !text.isEmpty()) {
+ IRI predicate = ctx.predicateStack.pop();
+ Resource subject = ctx.subjectStack.peek();
+ String datatypeUri = ctx.datatypeStack.isEmpty() ? null : ctx.datatypeStack.pop();
+ //emitLiteral(subject, predicate, text, datatypeUri);
+ String lang = ctx.langStack.isEmpty() ? null : ctx.langStack.peek();
+ emitter.emitLiteral(subject, predicate, text, datatypeUri, lang);
+ return;
+ }
+ cleanEndElement(uri, localName);
+ }
+
+ /**
+ * Updates the base URI for IRI resolution using the xml:base attribute if present.
+ *
+ * @param attrs the XML attributes of the current element
+ */
+ private void updateBase(Attributes attrs) {
+ String xmlBase = attrs.getValue("xml:base");
+ if (xmlBase != null) ctx.baseURI = xmlBase;
+ }
+
+ /**
+ * Updates the language context using the xml:lang attribute if present.
+ * The language value is pushed onto a stack to support nested scope.
+ *
+ * @param attrs the XML attributes of the current element
+ */
+ private void updateLang(Attributes attrs) {
+ String xmlLang = attrs.getValue("xml:lang");
+ if (xmlLang != null) ctx.langStack.push(xmlLang);
+ }
+
+
+ /**
+ * Updates the datatype context using the rdf:datatype attribute if present.
+ * The datatype URI is pushed onto a stack to support nested scope.
+ *
+ * @param attrs the XML attributes of the current element
+ */
+ private void updateDatatype(Attributes attrs) {
+ String datatype = attrs.getValue(RDF.type.getNamespace(), "datatype");
+ if (datatype != null) {
+ ctx.datatypeStack.push(datatype);
+ }
+ }
+
+ /**
+ * Processes the start of an RDF collection indicated by parseType="Collection".
+ * Initializes the internal collection structures and returns true if this is a collection.
+ *
+ * @param localName the local name of the element
+ * @param uri the namespace URI
+ * @param qName the qualified name
+ * @param attrs the attributes of the element
+ * @return true if this element starts a collection, false otherwise
+ */
+ private boolean processCollectionStart(String localName, String uri, String qName, Attributes attrs) {
+ if (!"Collection".equals(getParseType(attrs))) return false;
+ IRI predicate = ctx.factory.createIRI(RDFXMLUtils.expandQName(uri, localName, qName));
+ prepareCollection(predicate);
+ return true;
+ }
+
+ /**
+ * Prepares internal context to collect RDF list elements for a collection.
+ *
+ * @param predicate the predicate that points to the collection
+ */
+ private void prepareCollection(IRI predicate) {
+ ctx.predicateStack.push(predicate);
+ ctx.collectionSubject = ctx.subjectStack.peek();
+ ctx.collectionPredicate = predicate;
+ ctx.collectionBuilder = new ArrayList<>();
+ ctx.inCollection = true;
+ }
+
+ /**
+ * Processes an item inside an RDF collection. Adds the extracted subject to the collection list.
+ *
+ * @param localName the local name of the element
+ * @param uri the namespace URI
+ * @param attrs the attributes of the element
+ * @return true if the element is processed as a collection item, false otherwise
+ */
+ private boolean processCollectionItem(String localName, String uri, Attributes attrs) {
+ if (!ctx.inCollection || !RDFXMLUtils.isDescription(localName, uri)) return false;
+
+ Resource item = extractSubject(attrs, ctx.factory, ctx.baseURI);
+ ctx.collectionBuilder.add(item);
+ ctx.suppressSubject = true;
+
+ return true;
+ }
+
+ /**
+ * Processes RDF container elements like rdf:Bag, rdf:Seq, and code rdf:Alt,
+ * as well as container items like rdf:li and rdf:_n.
+ *
+ * @param localName the local name of the element
+ * @param uri the namespace URI
+ * @param qName the qualified name
+ * @param attrs the attributes of the element
+ * @return true if the element is a container or container item, false otherwise
+ */
+ private boolean processContainerElement(String localName, String uri, String qName, Attributes attrs) {
+ // --- RDF Container Element ---
+ if (isContainer(localName, uri)) {
+ Resource subject = extractSubject(attrs, ctx.factory, ctx.baseURI);
+ ctx.subjectStack.push(subject);
+ ctx.inContainer = true;
+ ctx.liIndex = 1;
+ emitter.emitType(subject, expandQName(uri, localName, qName));
+
+ return true;
+ }
+
+ // --- Container Items (rdf:li, rdf:_n) ---
+
+ if (ctx.inContainer && RDF.type.getNamespace().equals(uri)) {
+ String pred = switch (localName) {
+ case "li" -> RDF.type.getNamespace() + "_" + ctx.liIndex++;
+ default -> localName.matches("_\\d+") ? RDF.type.getNamespace() + localName : null;
+ };
+
+ if (pred != null) {
+ IRI predicate = ctx.factory.createIRI(pred);
+ String resource = attrs.getValue("rdf:resource");
+ if (resource != null) {
+ emitter.emitResourceTriple(ctx.subjectStack.peek(), predicate, resource, ctx.baseURI);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Processes an RDF node element such as rdf:Description or a typed node.
+ * Handles subject creation, optional rdf:type triple emission, and property attributes.
+ *
+ * @param localName the local name of the element
+ * @param uri the namespace URI
+ * @param qName the qualified name
+ * @param attrs the element's attributes
+ * @return true if the element is processed as an RDF node, false otherwise
+ */
+ private boolean processNodeElement(String localName, String uri, String qName, Attributes attrs) {
+ boolean isNode = isDescription(localName, uri)
+ || (ctx.subjectStack.isEmpty() && RDFXMLUtils.isNodeElement(attrs));
+
+ if (!isNode) return false;
+
+ Resource newSubject = RDFXMLUtils.extractSubject(attrs, ctx.factory, ctx.baseURI);
+
+ // Add triple if nested in another node as object
+ if (!ctx.predicateStack.isEmpty() && !ctx.subjectStack.isEmpty()) {
+ Resource parent = ctx.subjectStack.peek();
+ IRI predicate = ctx.predicateStack.pop();
+ emitter.emitTriple(parent, predicate, newSubject);
+ }
+
+ ctx.subjectStack.push(newSubject);
+
+ // Emit rdf:type if typed node
+ if (!isDescription(localName, uri)) {
+ emitter.emitType(newSubject, expandQName(uri, localName, qName));
+ }
+
+ // Handle non-syntax attributes
+ emitter.emitPropertyAttributes(newSubject, attrs);
+ return true;
+ }
+
+ /**
+ * Processes an RDF property element and emits triples accordingly.
+ * Handles {@code rdf:resource}, {@code rdf:nodeID}, {@code parseType="Resource"},
+ * and inline property attributes.
+ *
+ * @param localName the local name of the property element
+ * @param uri the namespace URI
+ * @param qName the qualified name
+ * @param attrs the element's attributes
+ *
+ * @return true if the element is processed as an RDF property element, false otherwise
+ */
+ private boolean processPropertyElement(String localName, String uri, String qName, Attributes attrs) {
+ IRI predicate = ctx.factory.createIRI(RDFXMLUtils.expandQName(uri, localName, qName));
+ ctx.predicateStack.push(predicate);
+
+ String resource = attrs.getValue(RDF.type.getNamespace(), "resource");
+ String nodeID = attrs.getValue(RDF.type.getNamespace(), "nodeID");
+
+ if (resource != null) {
+ emitter.emitResourceTriple(ctx.subjectStack.peek(), predicate, resource, ctx.baseURI);
+ ctx.predicateStack.pop();
+ return true;
+ }
+
+ if (nodeID != null) {
+ emitter.emitBNodeTriple(ctx.subjectStack.peek(), predicate, nodeID);
+ ctx.predicateStack.pop();
+ return true;
+ }
+
+ // parseType="Resource"
+ String parseType = getParseType(attrs);
+ if ("Resource".equals(parseType)) {
+ Resource bnode = emitBnodePredicateObject(predicate);
+ ctx.subjectStack.push(bnode);
+ return true;
+ }
+
+ // Inline attributes
+ if (hasNonSyntaxAttributes(attrs)) {
+ Resource bnode = emitBnodePredicateObject(predicate);
+ emitter.emitPropertyAttributes(bnode, attrs);
+ ctx.predicateStack.pop();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the given attributes contain any non-syntax (i.e., user-defined) attributes.
+ *
+ * @param attrs the XML attributes to inspect
+ * @return true if at least one attribute is not a reserved RDF or XML syntax attribute
+ */
+ private boolean hasNonSyntaxAttributes(Attributes attrs) {
+ for (int i = 0; i < attrs.getLength(); i++) {
+ if (!isSyntaxAttribute(attrs.getURI(i), attrs.getLocalName(i), attrs.getQName(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Emits a blank node as the object of the current predicate and links it to the subject.
+ *
+ * @param predicate the predicate of the triple
+ * @return the newly created blank node
+ */
+ private Resource emitBnodePredicateObject(IRI predicate) {
+ Resource parent = ctx.subjectStack.peek();
+ Resource bnode = ctx.factory.createBNode();
+ emitter.emitTriple(parent, predicate, bnode);
+ return bnode;
+ }
+
+
+
+ /**
+ * Cleans up parsing context stacks when an XML end element is encountered.
+ * @param uri the namespace URI of the element
+ * @param localName the local name of the element
+ */
+ private void cleanEndElement(String uri, String localName) {
+ if (!ctx.langStack.isEmpty()) ctx.langStack.pop();
+ if (!ctx.predicateStack.isEmpty()) ctx.predicateStack.pop();
+ if (RDFXMLUtils.isContainer(localName, uri)) {
+ if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop();
+ ctx.inContainer = false;
+ ctx.liIndex = 1;
+ return;
+ }
+ if (ctx.inCollection && localName.equals(ctx.collectionPredicate.getLocalName())) {
+ Resource listHead = createRdfCollection(ctx.collectionBuilder, ctx.model, ctx.factory);
+ ctx.model.add(ctx.factory.createStatement(ctx.collectionSubject, ctx.collectionPredicate, listHead));
+ ctx.inCollection = false;
+ ctx.collectionBuilder.clear();
+ return;
+ }
+ if (ctx.inCollection && RDFXMLUtils.isDescription(localName, uri)) {
+ if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop();
+ return;
+ }
+ if (RDFXMLUtils.isDescription(localName, uri)) {
+ if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop();
+ }
+ if (!ctx.subjectStack.isEmpty() && !ctx.predicateStack.isEmpty()) {
+ ctx.subjectStack.pop();
+ ctx.predicateStack.pop();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java
new file mode 100644
index 000000000..b8e502446
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java
@@ -0,0 +1,130 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.literal.XSD;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import org.xml.sax.Attributes;
+
+import java.util.Optional;
+
+import static fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLUtils.*;
+
+/**
+ * Emits RDF statements from parsed RDF/XML constructs using a given RDF Model
+ * and ValueFactory.
+ */
+public class RDFXMLStatementEmitter {
+
+ private final Model model;
+ private final ValueFactory factory;
+
+ /**
+ * Constructs a new emitter for the given RDF model and value factory.
+ *
+ * @param model the RDF model where statements will be added
+ * @param factory the RDF value factory used to create RDF terms
+ */
+ public RDFXMLStatementEmitter(Model model, ValueFactory factory) {
+ this.model = model;
+ this.factory = factory;
+ }
+
+ /**
+ * Emits a literal statement with optional datatype or language.
+ *
+ * @param subject the subject of the statement
+ * @param predicate the predicate of the statement
+ * @param text the literal value
+ * @param datatypeUri the datatype URI (optional, may be null)
+ * @param lang the language tag (optional, may be null)
+ */
+ public void emitLiteral(Resource subject, IRI predicate, String text, String datatypeUri, String lang) {
+ Value literal;
+ if (datatypeUri != null && !datatypeUri.isEmpty()) {
+ Optional known = RDFXMLUtils.resolveDatatype(datatypeUri);
+ IRI dtype = known.map(XSD::getIRI).orElseGet(() -> {
+ System.err.printf("[Warning] Unknown datatype: %s%n", datatypeUri);
+ return factory.createIRI(datatypeUri);
+ });
+ literal = factory.createLiteral(text, dtype);
+ } else if (lang != null && !lang.equals("__NO_LANG__")) {
+ literal = factory.createLiteral(text, lang);
+ } else {
+ literal = factory.createLiteral(text);
+ }
+ model.add(factory.createStatement(subject, predicate, literal));
+ }
+
+
+ /**
+ * Emits a rdf:type statement for the given subject and type URI.
+ *
+ * @param subject the subject resource
+ * @param expandedQName the fully expanded IRI for the type
+ */
+ public void emitType(Resource subject, String expandedQName) {
+ model.add(factory.createStatement(subject, RDF.type.getIRI(), factory.createIRI(expandedQName)));
+ }
+
+ /**
+ * Emits RDF statements for non-syntax XML attributes as predicate-object pairs.
+ *
+ * @param subject the subject resource
+ * @param attrs the XML attributes associated with the element
+ */
+ public void emitPropertyAttributes(Resource subject, Attributes attrs) {
+ for (int i = 0; i < attrs.getLength(); i++) {
+ String attrURI = attrs.getURI(i);
+ String attrLocal = attrs.getLocalName(i);
+ String attrQName = attrs.getQName(i);
+ String value = attrs.getValue(i);
+
+ if (isSyntaxAttribute(attrURI, attrLocal, attrQName)) continue;
+
+ IRI pred = factory.createIRI(expandQName(attrURI, attrLocal, attrQName));
+ model.add(factory.createStatement(subject, pred, factory.createLiteral(value)));
+ }
+ }
+
+ /**
+ * Emits a triple where the object is an IRI resolved against the base URI.
+ *
+ * @param subject the subject of the triple
+ * @param predicate the predicate of the triple
+ * @param resource the relative or absolute IRI string
+ * @param baseURI the base URI used to resolve the resource
+ */
+ public void emitResourceTriple(Resource subject, IRI predicate, String resource, String baseURI) {
+ model.add(factory.createStatement(
+ subject,
+ predicate,
+ factory.createIRI(resolveAgainstBase(resource, baseURI))
+ ));
+ }
+
+ /**
+ * Emits a triple where the object is a blank node identified by node ID.
+ *
+ * @param subject the subject of the triple
+ * @param predicate the predicate of the triple
+ * @param nodeID the blank node identifier
+ */
+ public void emitBNodeTriple(Resource subject, IRI predicate, String nodeID) {
+ model.add(factory.createStatement(
+ subject,
+ predicate,
+ factory.createBNode("_:" + nodeID)
+ ));
+ }
+
+ /**
+ * Emits a triple with a resource as object.
+ *
+ * @param subject the subject of the triple
+ * @param predicate the predicate of the triple
+ * @param object the object resource of the triple
+ */
+ public void emitTriple(Resource subject, IRI predicate, Resource object) {
+ model.add(factory.createStatement(subject, predicate, object));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java
new file mode 100644
index 000000000..e31e09eff
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java
@@ -0,0 +1,216 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.literal.XSD;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.exception.IncorrectFormatException;
+import org.xml.sax.*;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Utility methods for processing RDF/XML constructs.
+ *
+ * This class provides helpers for handling RDF/XML syntax attributes,
+ * QName expansion, datatype resolution, subject extraction, and RDF collections.
+ *
+ */
+public class RDFXMLUtils {
+ private RDFXMLUtils() {
+ // Utility class; no instantiation.
+ }
+
+
+ /**
+ * Expands a QName using the given namespace URI and local name.
+ *
+ * @param uri the namespace URI
+ * @param localName the local name
+ * @param qName the qualified name (used as fallback)
+ * @return the expanded IRI, or the qName if the URI is null or empty
+ */
+ public static String expandQName(String uri, String localName, String qName) {
+ return (uri != null && !uri.isEmpty()) ? uri + localName : qName;
+ }
+
+
+ /**
+ * Resolves a datatype URI to a known XSD enum constant.
+ *
+ * @param datatypeUri the datatype URI
+ * @return an Optional containing the matching XSD type if found
+ */
+ public static Optional resolveDatatype(String datatypeUri) {
+ for (XSD xsd : XSD.values()) {
+ if (xsd.getIRI().stringValue().equals(datatypeUri)) return Optional.of(xsd);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Extracts a subject resource from RDF/XML attributes.
+ * Supports rdf:about, rdf:nodeID, rdf:ID.
+ *
+ * @param attrs the XML attributes
+ * @param factory the value factory
+ * @param baseURI the base URI for resolving relative IRIs
+ * @return a Resource representing the subject
+ */
+ public static Resource extractSubject(Attributes attrs, ValueFactory factory, String baseURI) {
+ String about = attrs.getValue(RDF.type.getNamespace(), "about");
+ if (about != null) return factory.createIRI(resolveAgainstBase(about, baseURI));
+
+ String nodeID = attrs.getValue(RDF.type.getNamespace(), "nodeID");
+ if (nodeID != null) return factory.createBNode("_:" + nodeID);
+
+ String id = attrs.getValue(RDF.type.getNamespace(), "ID");
+ if (id != null) return factory.createIRI(resolveAgainstBase("#" + id, baseURI));
+
+ // Default to blank node
+ return factory.createBNode();
+ }
+
+ /**
+ * Resolves a relative IRI against a base URI.
+ *
+ * @param iri the relative or absolute IRI
+ * @param baseURI the base URI
+ * @return the resolved IRI
+ * @throws IncorrectFormatException if URI resolution fails
+ */
+ public static String resolveAgainstBase(String iri, String baseURI) {
+ if (iri == null) return null;
+ if (iri.isEmpty()) return baseURI;
+ if (baseURI == null || iri.matches("^[a-zA-Z][a-zA-Z0-9+.-]*:.*")) {
+ return iri;
+ }
+ try {
+ return new java.net.URI(baseURI).resolve(iri).toString();
+ } catch (Exception e) {
+ throw new IncorrectFormatException("Failed to resolve IRI: " + iri + " against base: " + baseURI, e);
+ }
+ }
+
+ /**
+ * Determines whether the element is a rdf:Description.
+ *
+ * @param localName the local name of the element
+ * @param uri the namespace URI
+ * @return {@code true} if it's an RDF description element
+ */
+ public static boolean isDescription(String localName, String uri) {
+ return RDF.type.getNamespace().equals(uri) && "Description".equals(localName);
+ }
+
+
+ /**
+ * Checks if the attributes define a subject node (via about, nodeID, or ID).
+ *
+ * @param attrs the attributes to check
+ * @return true if any node-identifying attribute is present
+ */
+ public static boolean isNodeElement(Attributes attrs) {
+ return attrs.getValue(RDF.type.getNamespace(), "about") != null ||
+ attrs.getValue(RDF.type.getNamespace(), "nodeID") != null ||
+ attrs.getValue(RDF.type.getNamespace(), "ID") != null;
+ }
+
+
+ /**
+ * Retrieves the value of rdf:parseType from attributes.
+ *
+ * @param attrs the attributes
+ * @return the parseType value, or null if not present
+ */
+ public static String getParseType(Attributes attrs) {
+ return attrs.getValue(RDF.type.getNamespace(), "parseType");
+ }
+
+
+ /**
+ * Determines whether a given attribute is an RDF/XML syntax attribute.
+ *
+ * @param uri the namespace URI
+ * @param localName the local name
+ * @param qName the qualified name
+ * @return true if the attribute is considered syntax-related
+ */
+ public static boolean isSyntaxAttribute(String uri, String localName, String qName) {
+ if (uri != null && RDF.type.getNamespace().equals(uri)) {
+ return switch (localName) {
+ case "about", "ID", "nodeID", "resource", "parseType", "datatype" -> true;
+ default -> false;
+ };
+ }
+ return qName.startsWith("xml:");
+ }
+
+ /**
+ * Resolves an XSD datatype from a URI.
+ *
+ * @param uri the datatype URI
+ * @return an Optional containing the XSD constant if matched
+ */
+ public static Optional fromURI(String uri) {
+ for (XSD xsd : XSD.values()) {
+ if (xsd.getIRI().stringValue().equals(uri)) {
+ return Optional.of(xsd);
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks if an element is the top-level rdf:RDF wrapper.
+ *
+ * @param uri the namespace URI
+ * @param localName the local name
+ * @return true if the element is rdf:RDF
+ */
+ public static boolean isRdfRDF(String uri, String localName) {
+ return RDF.type.equals(uri) && "RDF".equals(localName);
+ }
+
+ /**
+ * Determines if an element is a recognized RDF container: Bag, Seq, or Alt.
+ *
+ * @param localName the local name
+ * @param uri the namespace URI
+ * @return true if the element is a container type
+ */
+ public static boolean isContainer(String localName, String uri) {
+ return RDF.type.getNamespace().equals(uri) &&
+ ("Seq".equals(localName) || "Bag".equals(localName) || "Alt".equals(localName));
+ }
+
+ /**
+ * Creates a linked RDF collection using rdf:first and rdf:rest.
+ *
+ * @param items the list of resource items
+ * @param model the RDF model to populate
+ * @param factory the RDF value factory
+ * @return the head resource of the RDF collection
+ */
+ public static Resource createRdfCollection(List items, Model model, ValueFactory factory) {
+ Resource head = factory.createBNode();
+ Resource current = head;
+
+ for (int i = 0; i < items.size(); i++) {
+ Resource next = (i < items.size() - 1)
+ ? factory.createBNode()
+ : RDF.nil.getIRI(); // rdf:nil
+
+ model.add(factory.createStatement(current,
+ RDF.first.getIRI(),
+ items.get(i)));
+
+ model.add(factory.createStatement(current,
+ RDF.rest.getIRI(),
+ next));
+
+ current = next;
+ }
+ return head;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java
new file mode 100644
index 000000000..4807977d3
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java
@@ -0,0 +1,80 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml.context;
+
+import fr.inria.corese.core.next.api.*;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Holds shared parsing state during RDF/XML parsing.
+ *
+ * This class acts as a context holder for the SAX-based RDF/XML parser,
+ * allowing multiple elements and handlers to share and manipulate parsing state.
+ * It stores stacks for subjects, predicates, datatypes, and languages,
+ * as well as temporary collections used during the construction of RDF lists and containers.
+ *
+ * This context is typically instantiated once per parsing session and passed
+ * throughout the parsing logic.
+ */
+public class RDFXMLContext {
+
+ /** The RDF model to which parsed triples will be added. */
+ public Model model;
+
+ /** The factory used to create IRIs, literals, blank nodes, and statements. */
+ public ValueFactory factory;
+
+ /** The base URI against which relative IRIs are resolved. */
+ public String baseURI;
+
+ /** A single statement buffer (optional use). */
+ public Statement statement;
+
+ /** Builder list for rdf:parseType="Collection" elements. */
+ public List collectionBuilder = new ArrayList<>();
+
+ /** The subject associated with the current RDF collection. */
+ public Resource collectionSubject;
+
+ /** The predicate that connects the collection subject to the list head. */
+ public IRI collectionPredicate;
+
+ /** Stack of subject resources to manage nesting of elements. */
+ public final Deque subjectStack = new ArrayDeque<>();
+
+ /** Stack of predicates for tracking current RDF properties. */
+ public final Deque predicateStack = new ArrayDeque<>();
+
+ /** Stack for xml:lang values scoped by element depth. */
+ public final Deque langStack = new ArrayDeque<>();
+
+ /** Stack for rdf:datatype URIs associated with literals. */
+ public final Deque datatypeStack = new ArrayDeque<>();
+
+ /** Temporary holder for RDF collection items (unused or optional). */
+ public final Deque collectionItems = new ArrayDeque<>();
+
+ /** Whether the parser is currently inside an RDF container (rdf:Seq, rdf:Bag, rdf:Alt). */
+ public boolean inContainer = false;
+
+ /** Whether the parser is currently inside an RDF collection (rdf:parseType="Collection"). */
+ public boolean inCollection = false;
+
+ /** If true, skips pushing a subject onto the stack (used for collection items). */
+ public boolean suppressSubject = false;
+
+ /** Counter for rdf:li to rdf:_n expansion. */
+ public int liIndex = 1;
+
+ /**
+ * Constructs a new context for RDF/XML parsing.
+ *
+ * @param model the RDF model to populate with triples
+ * @param factory the value factory used to create RDF terms
+ */
+ public RDFXMLContext(Model model, ValueFactory factory) {
+ this.model = model;
+ this.factory = factory;
+ }
+}
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java
new file mode 100644
index 000000000..cddf9168c
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java
@@ -0,0 +1,96 @@
+package fr.inria.corese.core.next.impl.io.parser.trig;
+
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.api.ValueFactory;
+import fr.inria.corese.core.next.api.base.io.RDFFormat;
+import fr.inria.corese.core.next.api.base.io.parser.AbstractRDFParser;
+import fr.inria.corese.core.next.api.io.IOOptions;
+
+import fr.inria.corese.core.next.impl.exception.ParsingErrorException;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGLexer;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGParser;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeListener;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * An ANTLR4-based parser for Trig format.
+ * This parser uses an ANTLR grammar to tokenize and parse Trig documents,
+ * then a listener to build the RDF model.
+ */
+public class ANTLRTrigParser extends AbstractRDFParser {
+
+ /**
+ * Constructor for the ANTLRTrigParser.
+ *
+ * @param model The RDF model to populate.
+ * @param factory The ValueFactory for creating RDF resources.
+ */
+ public ANTLRTrigParser(Model model, ValueFactory factory) { super(model, factory); }
+
+ /**
+ * Constructor for the ANTLRTrigParser with configuration options.
+ *
+ * @param model The RDF model to populate.
+ * @param factory The ValueFactory for creating RDF resources.
+ * @param config The configuration options for parsing.
+ */
+ public ANTLRTrigParser(Model model, ValueFactory factory, IOOptions config) {super(model, factory, config);}
+
+ @Override
+ public RDFFormat getRDFFormat() {
+ return RDFFormat.TRIG;
+ }
+
+ @Override
+ public void setConfig(IOOptions config) {}
+
+ @Override
+ public void parse(InputStream in) throws ParsingErrorException {
+ parse(new InputStreamReader(in, StandardCharsets.UTF_8), null);
+ }
+
+ @Override
+ public void parse(InputStream in, String baseURI) throws ParsingErrorException {
+ parse(new InputStreamReader(in, StandardCharsets.UTF_8), baseURI);
+ }
+
+ @Override
+ public void parse(Reader reader) throws ParsingErrorException {
+ parse(reader, null);
+ }
+
+ /**
+ * Parses Trig data from a Reader using ANTLR4.
+ *
+ * @param reader The Reader to read RDF data from.
+ * @param baseURI The base URI.
+ * @throws ParsingErrorException if a parsing or I/O error occurs.
+ */
+ @Override
+ public void parse(Reader reader, String baseURI) throws ParsingErrorException {
+ try {
+ CharStream charStream = CharStreams.fromReader(reader);
+ TriGLexer triGLexer = new TriGLexer(charStream);
+ CommonTokenStream tokens = new CommonTokenStream(triGLexer);
+ TriGParser triGParser = new TriGParser(tokens);
+ ParseTreeWalker walker = new ParseTreeWalker();
+ ParseTree tree = triGParser.trigDoc();
+ TriGListerner listerner = new TriGListerner(getModel(), getValueFactory(), this.getConfig());
+ walker.walk((ParseTreeListener) listerner, tree);
+ } catch (IOException e) {
+ throw new ParsingErrorException("Failed to parse TriG RDF: " + e.getMessage(), e);
+ } catch (Exception e) {
+ throw new ParsingErrorException("Unexpected error during TriG parsing: " + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java
new file mode 100644
index 000000000..a7f199392
--- /dev/null
+++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java
@@ -0,0 +1,253 @@
+package fr.inria.corese.core.next.impl.io.parser.trig;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.api.io.IOOptions;
+import fr.inria.corese.core.next.api.io.parser.RDFParserBaseIRIOptions;
+import fr.inria.corese.core.next.impl.common.literal.XSD;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGBaseListener;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGParser;
+import fr.inria.corese.core.next.api.ValueFactory;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Listener for the ANTLR4 generated parser for TriG.
+ * This listener traverses the parse tree and builds the RDF model,
+ * supporting named graphs. It includes unescaping logic for URIs and literals.
+ */
+public class TriGListerner extends TriGBaseListener {
+ private final Model model;
+ private String baseURI;
+ private final Map prefixMap = new HashMap<>();
+ private final ValueFactory factory;
+
+ private Resource currentSubject;
+ private IRI currentPredicate;
+ private Resource currentGraph;
+
+
+ /**
+ * Constructor for the TriGListerner.
+ *
+ * @param model The RDF model to populate.
+ * @param factory The ValueFactory for creating RDF resources.
+ * @param options IOOptions for configuration (if any).
+ */
+ public TriGListerner(Model model, ValueFactory factory, IOOptions options) {
+ this.model = model;
+ this.baseURI = baseURI != null ? baseURI : "";
+ if (options != null && options instanceof RDFParserBaseIRIOptions);
+ this.factory = factory;
+ }
+
+ @Override
+ public void exitPrefixID(TriGParser.PrefixIDContext ctx) {
+ String prefix = ctx.PNAME_NS().getText();
+ String iri = ctx.IRIREF().getText();
+ prefix = prefix.substring(0, prefix.length() - 1);
+ iri = iri.substring(1, iri.length() - 1);
+ prefixMap.put(prefix, iri);
+ model.setNamespace(prefix, iri);
+ }
+
+ @Override
+ public void exitSparqlBase(TriGParser.SparqlBaseContext ctx) {
+ baseURI = ctx.IRIREF().getText().replaceAll("^<|>$", "");
+ }
+
+ @Override
+ public void enterBlock(TriGParser.BlockContext ctx) {
+ currentGraph = ctx.Graph_w() != null && ctx.labelOrSubject() != null
+ ? extractLabelOrSubject(ctx.labelOrSubject())
+ : null;
+ }
+
+ @Override
+ public void exitBlock(TriGParser.BlockContext ctx) {
+ currentGraph = null;
+ }
+
+ @Override
+ public void enterTriplesOrGraph(TriGParser.TriplesOrGraphContext ctx) {
+ if (ctx.labelOrSubject() != null && ctx.predicateObjectList() != null) {
+ currentSubject = extractLabelOrSubject(ctx.labelOrSubject());
+ processPredicateObjectList(ctx.predicateObjectList());
+ }
+ }
+
+ @Override
+ public void enterTriples(TriGParser.TriplesContext ctx) {
+ currentSubject = extractSubject(ctx.subject());
+ processPredicateObjectList(ctx.predicateObjectList());
+ }
+
+ /**
+ * Processes a PredicateObjectList context, extracting verbs and corresponding object lists,
+ * and adding triples to the model for the current subject and graph.
+ *
+ * @param ctx the predicate-object list context from the parser
+ */
+ private void processPredicateObjectList(TriGParser.PredicateObjectListContext ctx) {
+ List verbs = ctx.verb();
+ List objLists = ctx.objectList();
+
+ for (int i = 0; i < verbs.size(); i++) {
+ currentPredicate = extractVerb(verbs.get(i));
+ List objects = objLists.get(i).object();
+ for (TriGParser.ObjectContext objCtx : objects) {
+ Value object = extractObject(objCtx);
+ model.add(currentSubject, currentPredicate, object, currentGraph);
+ }
+ }
+ }
+
+ /**
+ * Extracts an RDF object from the ObjectContext.
+ * Supports IRIs, blank nodes, literals, and inline blank node property lists.
+ *
+ * @param ctx the object context
+ * @return the extracted RDF Value
+ */
+ private Value extractObject(TriGParser.ObjectContext ctx) {
+ if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText()));
+ if (ctx.blank() != null) return extractBlank(ctx.blank());
+ if (ctx.literal() != null) return extractLiteral(ctx.literal());
+ if (ctx.blankNodePropertyList() != null) return processBlankNodePropertyList(ctx.blankNodePropertyList());
+ throw new RuntimeException("Unsupported object: " + ctx.getText());
+ }
+
+ /**
+ * Processes an inline blank node with its property list, returning the blank node as a Resource.
+ * Temporarily updates the current subject to the new blank node during processing.
+ *
+ * @param ctx the blank node property list context
+ * @return the new blank node resource
+ */
+ private Resource processBlankNodePropertyList(TriGParser.BlankNodePropertyListContext ctx) {
+ Resource bnode = factory.createBNode();
+ Resource savedSubject = currentSubject;
+ currentSubject = bnode;
+ processPredicateObjectList(ctx.predicateObjectList());
+ currentSubject = savedSubject;
+ return bnode;
+ }
+
+ /**
+ * Extracts a subject from a SubjectContext, which can be an IRI or a blank node.
+ *
+ * @param ctx the subject context
+ * @return the extracted subject as a Resource
+ */
+ private Resource extractSubject(TriGParser.SubjectContext ctx) {
+ if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText()));
+ if (ctx.blank() != null) return extractBlank(ctx.blank());
+ throw new RuntimeException("Unsupported subject: " + ctx.getText());
+ }
+
+ /**
+ * Extracts a blank node from a BlankContext, supporting labeled (_:b) and anonymous ([]) forms.
+ *
+ * @param ctx the blank context
+ * @return the blank node as a Resource
+ */
+ private Resource extractBlank(TriGParser.BlankContext ctx) {
+ TriGParser.BlankNodeContext node = ctx.blankNode();
+ if (node != null) {
+ if (node.BLANK_NODE_LABEL() != null)
+ return factory.createBNode(node.BLANK_NODE_LABEL().getText());
+ if (node.ANON() != null)
+ return factory.createBNode();
+ }
+ throw new RuntimeException("Unsupported blank node structure: " + ctx.getText());
+ }
+
+ /**
+ * Extracts a graph label or subject from a LabelOrSubjectContext.
+ * Supports IRI and blank node.
+ *
+ * @param ctx the label or subject context
+ * @return the extracted resource
+ */
+ private Resource extractLabelOrSubject(TriGParser.LabelOrSubjectContext ctx) {
+ if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText()));
+ if (ctx.blankNode() != null) return factory.createBNode(ctx.blankNode().getText());
+ throw new RuntimeException("Unsupported labelOrSubject: " + ctx.getText());
+ }
+
+ /**
+ * Extracts a predicate IRI from a VerbContext.
+ * Handles the special keyword 'a' as rdf:type.
+ *
+ * @param ctx the verb context
+ * @return the extracted IRI
+ */
+ private IRI extractVerb(TriGParser.VerbContext ctx) {
+ return factory.createIRI(resolveIRI(ctx.getText()));
+ }
+
+ /**
+ * Extracts a Literal from a LiteralContext, handling typed, language-tagged, boolean, and numeric literals.
+ *
+ * @param ctx the literal context
+ * @return the extracted Literal
+ */
+ private Literal extractLiteral(TriGParser.LiteralContext ctx) {
+ if (ctx.rDFLiteral() != null) {
+ String label = stripQuotes(ctx.rDFLiteral().string().getText());
+ if (ctx.rDFLiteral().LANGTAG() != null)
+ return factory.createLiteral(label, ctx.rDFLiteral().LANGTAG().getText().substring(1));
+ if (ctx.rDFLiteral().iri() != null)
+ return factory.createLiteral(label, factory.createIRI(resolveIRI(ctx.rDFLiteral().iri().getText())));
+ return factory.createLiteral(label);
+ }
+ if (ctx.BooleanLiteral() != null)
+ return factory.createLiteral(ctx.BooleanLiteral().getText(), XSD.BOOLEAN.getIRI());
+ if (ctx.numericLiteral() != null) {
+ if (ctx.numericLiteral().INTEGER() != null)
+ return factory.createLiteral(ctx.numericLiteral().INTEGER().getText(), XSD.INTEGER.getIRI());
+ if (ctx.numericLiteral().DECIMAL() != null)
+ return factory.createLiteral(ctx.numericLiteral().DECIMAL().getText(), XSD.DECIMAL.getIRI());
+ if (ctx.numericLiteral().DOUBLE() != null)
+ return factory.createLiteral(ctx.numericLiteral().DOUBLE().getText(), XSD.DOUBLE.getIRI());
+ }
+ throw new RuntimeException("Unsupported literal: " + ctx.getText());
+ }
+
+ /**
+ * Resolves an IRI or QName into a full URI string.
+ * Handles full IRIs in angle brackets, QNames using prefixes, and special case "a".
+ *
+ * @param raw the raw string
+ * @return the resolved URI string
+ */
+ private String resolveIRI(String raw) {
+ raw = raw.trim();
+ if (raw.startsWith("<") && raw.endsWith(">")) return raw.substring(1, raw.length() - 1);
+ if (raw.equals("a")) return RDF.type.getIRI().stringValue();
+ if (raw.contains(":")) {
+ String[] parts = raw.split(":", 2);
+ String ns = prefixMap.get(parts[0]);
+ if (ns != null) return ns + parts[1];
+ throw new IllegalArgumentException("Undeclared prefix: " + parts[0]);
+ }
+ return baseURI + raw;
+ }
+
+ /**
+ * Strips surrounding quotes from a string literal, including single, double, and multi-line forms.
+ *
+ * @param text the quoted string
+ * @return the unquoted string
+ */
+ private String stripQuotes(String text) {
+ if (text == null || text.length() < 2) return text;
+ if ((text.startsWith("\"") && text.endsWith("\"")) ||
+ (text.startsWith("\"\"\"") && text.endsWith("\"\"\"")) ||
+ (text.startsWith("'''") && text.endsWith("'''"))) {
+ return text.substring(1, text.length() - 1);
+ }
+ return text;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/inria/corese/core/util/CoreseInfo.java b/src/main/java/fr/inria/corese/core/util/CoreseInfo.java
index 16fb462b7..c202ac361 100644
--- a/src/main/java/fr/inria/corese/core/util/CoreseInfo.java
+++ b/src/main/java/fr/inria/corese/core/util/CoreseInfo.java
@@ -6,7 +6,7 @@
*/
public class CoreseInfo {
- private static final String VERSION = "4.6.4-SNAPSHOT";
+ private static final String VERSION = "4.6.4";
/**
* Retrieves the current version of the Corese application.
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java
new file mode 100644
index 000000000..f4677cdab
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java
@@ -0,0 +1,582 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.Literal;
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.api.Value;
+import fr.inria.corese.core.next.api.ValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseModel;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for the RDFXMLParser class.
+ * These tests verify the parser's ability to correctly parse RDF/XML
+ * and interact with the Model and ValueFactory, including error handling
+ * and unescaping of IRIs and literals, and named graphs.
+ */
+public class RDFXMLParserTest {
+ /**
+ * Helper method to parse the RDF/XML String
+ * @param rdfXml
+ * @return model
+ * @throws Exception
+ */
+ private Model parseRdfXml(String rdfXml) throws Exception {
+ Model model = new CoreseModel();
+ ValueFactory valueFactory = new CoreseAdaptedValueFactory();
+ try (InputStream inputStream = new ByteArrayInputStream(rdfXml.getBytes(StandardCharsets.UTF_8))) {
+ RDFXMLParser parser = new RDFXMLParser(model, valueFactory);
+ parser.parse(inputStream);
+ }
+ return model;
+ }
+
+ /**
+ * Helper method to print the model.
+ * @param model
+ */
+ private void printModel(Model model) {
+ model.stream().forEach(stmt -> {
+ Value obj = stmt.getObject();
+ if (obj instanceof Literal literal) {
+ if (literal.getLanguage().isPresent()) {
+ System.out.printf("(%s, %s, \"%s\"@%s)%n",
+ stmt.getSubject().stringValue(),
+ stmt.getPredicate().stringValue(),
+ literal.getLabel(),
+ literal.getLanguage().get());
+ } else {
+ System.out.printf("(%s, %s, \"%s\")%n",
+ stmt.getSubject().stringValue(),
+ stmt.getPredicate().stringValue(),
+ literal.getLabel());
+ }
+ } else {
+ System.out.printf("(%s, %s, %s)%n",
+ stmt.getSubject().stringValue(),
+ stmt.getPredicate().stringValue(),
+ obj.stringValue());
+ }
+ });
+ }
+
+
+ /**
+ * Test node elements with IRIs
+ * @throws Exception
+ */
+ @Test
+ public void testNodeElementsWithIRIs() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(2, model.size(), "Expected two RDF statements");
+
+ }
+
+ /**
+ * Test a basic RDF/XML file
+ * @throws Exception
+ */
+ @Test
+ public void testBasicRdfParsing() throws Exception {
+ String rdfXml = """
+
+
+
+ John Smith
+ 2025-07-07
+
+
+ """;
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(2, model.size(), "Expected two RDF statements");
+ }
+
+ /**
+ * Test a RDF/XML file with Complete description of all graph paths
+ * @throws Exception
+ */
+ @Test
+ public void testExample3CompleteDescriptionOfAllGraphPaths() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dave Beckett
+
+
+
+
+
+ RDF 1.2 XML Syntax
+
+
+ """.trim();
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(5, model.size(), "Expected five RDF statements");
+ }
+
+ /**
+ * Test RDF/XML File Using multiple property elements on a node element
+ * @throws Exception
+ */
+ @Test
+ public void testExample4UsingMultiplePropertyElements() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+ Dave Beckett
+
+
+ RDF 1.2 XML Syntax
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected four RDF statements");
+ }
+
+ /**
+ * Test RDF/XML with Empty property elements
+ * @throws Exception
+ */
+ @Test
+ public void testExample5EmptyPropertyElements() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+ Dave Beckett
+
+
+ RDF 1.2 XML Syntax
+
+
+ """.trim();
+
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected four RDF statements");
+
+ }
+
+ /**
+ * Test a RDF/XML file with Replacing property elements with string literal content into property attributes
+ * @throws Exception
+ */
+ @Test
+ public void testExample6ReplacingPropertyElementsWithStringLiteral() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected four RDF statements");
+
+ }
+
+ /**
+ * Test a Complete RDF/XML
+ * @throws Exception
+ */
+ @Test
+ public void testExample7CompleteRDFXML() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+
+ """.trim();
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected four RDF statements");
+ }
+
+ /**
+ * Test a Complete example of xml:lang
+ * @throws Exception
+ */
+ @Test
+ public void testExample8CompleteExampleXmlLang() throws Exception {
+ String rdfXml = """
+
+
+
+
+ RDF 1.2 XML Syntax
+ RDF 1.2 XML Syntax
+ RDF 1.2 XML Syntax
+
+
+
+ Der Baum
+ Das Buch ist außergewöhnlich
+ The Tree
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(6, model.size(), "Expected six RDF statements");
+ }
+
+ @Test
+ public void testExample11CompleteExamplerdfDatatype() throws Exception {
+ String rdfXml = """
+
+
+
+
+ 123
+
+
+
+ """.trim();
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(1, model.size(), "Expected four RDF statements");
+ }
+
+ /**
+ * Test a Complete RDF/XML file with a description of graph using rdf:nodeID
+ * @throws Exception
+ */
+ @Test
+ public void testExample12CompleteRDFXMLUsingRdfNodeID() throws Exception {
+
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+
+ // Assert or inspect the result
+ assertEquals(4, model.size(), "Expected five RDF statements");
+ }
+
+ /**
+ * Test a RDF/XML file with a Complete example using rdf:parseType=Resource
+ * @throws Exception
+ */
+ @Test
+ public void testExample13CompleteExampleUsingRdfparseTypeResource() throws Exception {
+ String rdfXml = """
+
+
+
+
+ Dave Beckett
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected four RDF statements");
+
+ }
+
+ /**
+ * Test a RDF/XML file with a Complete example of property attributes on an empty property element
+ * @throws Exception
+ */
+ @Test
+ public void testExample14CompleteExampleOfPorpertyAttributesOnAnEmptyPropertyElement() throws Exception {
+
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(3, model.size(), "Expected three RDF statements");
+ }
+
+ /**
+ * Test a RDF/XML file with a Complete example with rdf:type
+ * @throws Exception
+ */
+ @Test
+ public void testExample15CompleteExampleWithRdfType() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+ A marvelous thing
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(2, model.size(), "Expected four RDF statements");
+ }
+
+ /**
+ * Test a RDF/XML file with a Complete example using a typed node element to replace an rdf:type
+ * @throws Exception
+ */
+ @Test
+ public void testExample16CompleteExampleUsingATypedNodeElementToReplaceAnRdfType() throws Exception {
+ String rdfXml = """
+
+
+
+
+ A marvelous thing
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(2, model.size(), "Expected two RDF statements");
+
+ }
+
+ @Test
+ /**
+ * Test a XML/RDF File using rdf:ID and xml:base
+ */
+ public void testExample17CompleteExampleUsingRdfIDAndXmlbase() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(1, model.size(), "Expected one RDF statement");
+
+ }
+
+ /**
+ * Test a Complex example using RDF list properties
+ * @throws Exception
+ */
+ @Test
+ public void testExample18ComplexExampleUsingRdfListProperties() throws Exception {
+
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected three RDF statements");
+ }
+
+ /**
+ * Test a Complete example using rdf:li
+ * @throws Exception
+ */
+ @Test
+ public void testExample19CompleteExampleUsingRdfliProperties() throws Exception {
+
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(4, model.size(), "Expected three RDF statements");
+
+ }
+
+ /**
+ * Test a Complete example of a RDF collection
+ * @throws Exception
+ */
+ @Test
+ public void testExample20CompleteExampleOfARdfCollectionOfNodes() throws Exception {
+ String rdfXml = """
+
+
+
+
+
+
+
+
+
+
+
+
+ """.trim();
+
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(7, model.size(), "Expected three RDF statements");
+ }
+
+ /**
+ * Test a Complete example of rdf:ID reifying a property element
+ * @throws Exception
+ */
+ @Test
+ public void testExample21CompleteExampleOfRdfID() throws Exception {
+ String rdfXml = """
+
+
+
+ blah
+
+
+
+ """.trim();
+ Model model = parseRdfXml(rdfXml);
+ printModel(model);
+ assertEquals(1, model.size(), "Expected one RDF statement");
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java
new file mode 100644
index 000000000..62a5d9671
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java
@@ -0,0 +1,164 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.*;
+import fr.inria.corese.core.next.impl.common.literal.XSD;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseModel;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.AttributesImpl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for the RDFXMLStatementEmitter class.
+ *
+ * This test suite verifies that the emitter correctly adds RDF statements to the provided
+ * Model based on various RDF/XML constructs including:
+ * - Plain literals
+ * - Typed literals
+ * - Language-tagged literals
+ * - Resource IRIs
+ * - Blank nodes
+ * - RDF types
+ * - Property attributes
+ */
+public class RDFXMLStatementEmitterTest {
+
+ private Model model;
+ private ValueFactory factory;
+ private RDFXMLStatementEmitter emitter;
+
+ @BeforeEach
+ public void setUp() {
+ model = new CoreseModel();
+ factory = new CoreseAdaptedValueFactory();
+ emitter = new RDFXMLStatementEmitter(model, factory);
+ }
+
+ /**
+ * Test emitting a plain literal statement without language or datatype.
+ * Asserts that the triple is added to the model correctly.
+ */
+ @Test
+ public void testEmitLiteral_plain() {
+ Literal literal = factory.createLiteral("hello");
+ Resource subject = factory.createBNode();
+ IRI predicate = factory.createIRI("http://example.org/predicate");
+ emitter.emitLiteral(subject, predicate, "hello", null, null);
+ assertEquals(1, model.size());
+ Iterable statements = model.getStatements(subject, predicate, literal);
+ boolean found = false;
+ for (Statement stmt : statements) {
+ if (stmt.getSubject().equals(subject) &&
+ stmt.getPredicate().equals(predicate) &&
+ stmt.getObject().stringValue().equals(literal.stringValue())) {
+ found = true;
+ break;
+ }
+ }
+
+ assertTrue(found, "Expected statement not found in model");
+ }
+
+ /**
+ * Test emitting a literal with a language tag.
+ * Verifies that the correct literal is added to the model.
+ */
+ @Test
+ public void testEmitLiteral_withLang() {
+ Resource subject = factory.createBNode();
+ IRI predicate = factory.createIRI("http://example.org/predicate");
+ emitter.emitLiteral(subject, predicate, "bonjour", null, "fr");
+
+ Value obj = model.objects().iterator().next();
+ assertTrue(obj.isLiteral());
+ assertEquals("bonjour", obj.stringValue());
+ }
+
+ /**
+ * Test emitting a literal with a datatype IRI.
+ * Verifies that the correct typed literal is added to the model.
+ */
+ @Test
+ public void testEmitLiteral_withDatatype() {
+ Resource subject = factory.createBNode();
+ IRI predicate = factory.createIRI("http://example.org/age");
+ emitter.emitLiteral(subject, predicate, "42", XSD.INTEGER.getIRI().stringValue(), null);
+
+ Value obj = model.objects().iterator().next();
+ assertTrue(obj.isLiteral());
+ assertEquals("42", obj.stringValue());
+ }
+
+ /**
+ * Test emitting a rdf:type statement for a subject.
+ * Verifies that the rdf:type triple is correctly created.
+ */
+ @Test
+ public void testEmitType() {
+ Resource subject = factory.createIRI("http://example.org/Alice");
+ emitter.emitType(subject, "http://example.org/Person");
+
+ assertTrue(model.contains(subject, RDF.type.getIRI(), factory.createIRI("http://example.org/Person")));
+ }
+
+ /**
+ * Test emitting a triple where the object is a resource IRI resolved against a base.
+ */
+ @Test
+ public void testEmitResourceTriple() {
+ Resource subject = factory.createIRI("http://example.org/Alice");
+ IRI predicate = factory.createIRI("http://example.org/knows");
+ emitter.emitResourceTriple(subject, predicate, "Bob", "http://example.org/");
+
+ assertTrue(model.contains(subject, predicate, factory.createIRI("http://example.org/Bob")));
+ }
+
+ /**
+ * Test emitting a triple where the object is a blank node identified by nodeID.
+ */
+ @Test
+ public void testEmitBNodeTriple() {
+ Resource subject = factory.createIRI("http://example.org/Alice");
+ IRI predicate = factory.createIRI("http://example.org/knows");
+ emitter.emitBNodeTriple(subject, predicate, "b123");
+
+ assertTrue(model.size() == 1);
+ Value obj = model.objects().iterator().next();
+ assertTrue(obj.stringValue().contains("_:b123"));
+ }
+
+ /**
+ * Test emitting a generic triple with subject, predicate, and object resources.
+ */
+ @Test
+ public void testEmitTriple() {
+ Resource s = factory.createIRI("http://example.org/s");
+ IRI p = factory.createIRI("http://example.org/p");
+ Resource o = factory.createIRI("http://example.org/o");
+
+ emitter.emitTriple(s, p, o);
+
+ assertTrue(model.contains(s, p, o));
+ }
+
+ /**
+ * Test emitting triples from XML attributes.
+ */
+ @Test
+ public void testEmitPropertyAttributes() {
+ Resource s = factory.createIRI("http://example.org/thing");
+ AttributesImpl attrs = new AttributesImpl();
+ attrs.addAttribute("http://example.org/", "foo", "ex:foo", "CDATA", "val1");
+ attrs.addAttribute("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "about", "rdf:about", "CDATA", "ignored");
+
+ emitter.emitPropertyAttributes(s, attrs);
+
+ assertEquals(1, model.size());
+ Value object = model.objects().iterator().next();
+ assertEquals("val1", object.stringValue());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java
new file mode 100644
index 000000000..89321f2ee
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java
@@ -0,0 +1,124 @@
+package fr.inria.corese.core.next.impl.io.parser.rdfxml;
+
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.api.Resource;
+import fr.inria.corese.core.next.api.ValueFactory;
+import fr.inria.corese.core.next.impl.common.literal.XSD;
+import fr.inria.corese.core.next.impl.common.vocabulary.RDF;
+import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseModel;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.AttributesImpl;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the RDFXMLUtils utility class.
+ *
+ * This test suite validates the correct behavior of various utility methods
+ * related to RDF/XML parsing, including QName expansion, datatype resolution,
+ * subject extraction, IRI resolution, container detection, syntax attribute recognition,
+ * and RDF collection creation.
+ */
+public class RDFXMLUtilsTest {
+
+ private final ValueFactory factory = new CoreseAdaptedValueFactory();
+
+ /**
+ * Tests expansion of QNames into full IRIs using provided namespace and local name.
+ */
+ @Test
+ public void testExpandQName() {
+ assertEquals("http://example.org/test", RDFXMLUtils.expandQName("http://example.org/", "test", "ex:test"));
+ assertEquals("ex:test", RDFXMLUtils.expandQName(null, null, "ex:test"));
+ }
+
+ /**
+ * Tests resolution of known and unknown datatype URIs.
+ */
+ @Test
+ public void testResolveDatatype() {
+ assertEquals(Optional.of(XSD.STRING), RDFXMLUtils.resolveDatatype(XSD.STRING.getIRI().stringValue()));
+ assertTrue(RDFXMLUtils.resolveDatatype("http://nonexistentdatatype").isEmpty());
+ }
+
+ /**
+ * Tests subject extraction using the rdf:about attribute.
+ */
+ @Test
+ public void testExtractSubjectWithAbout() {
+ AttributesImpl attrs = new AttributesImpl();
+ attrs.addAttribute(RDF.type.getNamespace(), "about", "", "CDATA", "http://example.org/subject");
+ Resource subject = RDFXMLUtils.extractSubject(attrs, factory, null);
+ assertEquals("http://example.org/subject", subject.stringValue());
+ }
+
+
+ /**
+ * Tests subject extraction using the rdf:nodeID attribute.
+ */
+ @Test
+ public void testExtractSubjectWithNodeID() {
+ AttributesImpl attrs = new AttributesImpl();
+ attrs.addAttribute(RDF.type.getNamespace(), "nodeID", "", "CDATA", "b123");
+ Resource subject = RDFXMLUtils.extractSubject(attrs, factory, null);
+ assertTrue(subject.stringValue().contains("_:b123"));
+ }
+
+ /**
+ * Tests subject extraction using the rdf:ID attribute with base URI resolution.
+ */
+ @Test
+ public void testExtractSubjectWithID() {
+ AttributesImpl attrs = new AttributesImpl();
+ attrs.addAttribute(RDF.type.getNamespace(), "ID", "", "CDATA", "id123");
+ Resource subject = RDFXMLUtils.extractSubject(attrs, factory, "http://example.org/");
+ assertEquals("http://example.org/id123", subject.stringValue());
+ }
+
+ /**
+ * Tests resolving a relative IRI against a base URI.
+ */
+ @Test
+ public void testResolveAgainstBase() {
+ assertEquals("http://base.org/path", RDFXMLUtils.resolveAgainstBase("path", "http://base.org/"));
+ }
+
+ /**
+ * Tests recognition of RDF/XML syntax attributes.
+ */
+ @Test
+ public void testIsSyntaxAttribute() {
+ assertTrue(RDFXMLUtils.isSyntaxAttribute(RDF.type.getNamespace(), "about", "rdf:about"));
+ assertTrue(RDFXMLUtils.isSyntaxAttribute(null, "lang", "xml:lang"));
+ assertFalse(RDFXMLUtils.isSyntaxAttribute("http://example.org/", "type", "ex:type"));
+ }
+
+ /**
+ * Tests detection of RDF container types (Bag, Seq, Alt).
+ */
+ @Test
+ public void testIsContainer() {
+ assertTrue(RDFXMLUtils.isContainer("Bag", RDF.type.getNamespace()));
+ assertFalse(RDFXMLUtils.isContainer("notAContainer", "http://example.org/"));
+ }
+
+ /**
+ * Tests creation of an RDF collection using rdf:first, rdf:rest, and rdf:nil.
+ */
+ @Test
+ public void testCreateRdfCollection() {
+ Model model = new CoreseModel();
+ Resource r1 = factory.createIRI("http://example.org/A");
+ Resource r2 = factory.createIRI("http://example.org/B");
+ Resource head = RDFXMLUtils.createRdfCollection(List.of(r1, r2), model, factory);
+
+ assertNotNull(head);
+ assertTrue(model.size() > 0);
+ assertTrue(model.contains(null, RDF.first.getIRI(), r1));
+ assertTrue(model.contains(null, RDF.rest.getIRI(), RDF.nil.getIRI()));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java
new file mode 100644
index 000000000..8a9cc7bec
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java
@@ -0,0 +1,183 @@
+package fr.inria.corese.core.next.impl.io.parser.trig;
+
+import fr.inria.corese.core.next.api.Literal;
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.api.Value;
+import fr.inria.corese.core.next.api.ValueFactory;
+import fr.inria.corese.core.next.api.io.parser.RDFParser;
+import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseModel;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.StringReader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for the ANTLRTrigParser class.
+ * These tests verify the parser's ability to correctly parse Trig
+ * and interact with the Model and ValueFactory, including error handling
+ * and unescaping of IRIs and literals, and named graphs.
+ */
+class ANTLRTrigParserTest {
+
+ private static final Logger logger = LoggerFactory.getLogger(ANTLRTrigParserTest.class);
+
+ /**
+ * helper method to parse trig data into corese model
+ *
+ * @param trigData a string of rdf data in trig format
+ * @param baseURI the base uri
+ * @return Corese rdf model
+ * @throws Exception
+ */
+
+ private Model parseFromString(String trigData, String baseURI) throws Exception {
+ Model model = new CoreseModel();
+ ValueFactory factory = new CoreseAdaptedValueFactory();
+ RDFParser parser = new ANTLRTrigParser(model, factory);
+ parser.parse(new StringReader(trigData), baseURI);
+ return model;
+ }
+
+ /**
+ * Helper method to print the model.
+ * @param model
+ */
+ private void printModel(Model model) {
+ model.stream().forEach(stmt -> {
+ Value obj = stmt.getObject();
+ String subjectString = stmt.getSubject().stringValue();
+ String predicateString = stmt.getPredicate().stringValue();
+
+ if (obj instanceof Literal literal) {
+ String label = String.valueOf(literal.getLabel());
+ String languageTag = literal.getLanguage().orElse(null);
+
+ if (languageTag != null) {
+ logger.debug("({}, {}, \"{}\"@{})",
+ subjectString,
+ predicateString,
+ label,
+ languageTag);
+ } else {
+ logger.debug("({}, {}, \"{}\")",
+ subjectString,
+ predicateString,
+ label);
+ }
+ } else {
+ logger.debug("({}, {}, {})",
+ subjectString,
+ predicateString,
+ obj.stringValue());
+ }
+ });
+ }
+
+ @Test
+ void testNamedGraphParsing() throws Exception {
+ String trig = """
+ @prefix ex:
+ ex:Graph1 {
+ ex:Alice ex:knows ex:Bob .
+ }""".trim();
+
+ Model model = parseFromString(trig, null);
+ printModel(model);
+ assertEquals(1, model.size());
+
+ assertEquals(1, model.getNamespaces().size());
+
+ assertEquals(1, model.contexts().size());
+ }
+
+ @Test
+ void testDocumentThatContainsOneGraphExample1() throws Exception {
+ String trig = """
+ # This document encodes one graph.
+ @prefix ex: .
+ @prefix : .
+
+ :G1 { :Monica a ex:Person ;
+ ex:name "Monica Murphy" ;
+ ex:homepage ;
+ ex:email ;
+ ex:hasSkill ex:Management ,
+ ex:Programming . }
+ """.trim();
+
+ Model model = parseFromString(trig, null);
+ printModel(model);
+
+ assertEquals(6, model.size());
+
+ assertEquals(2, model.getNamespaces().size());
+
+ assertEquals(1, model.contexts().size());
+ }
+
+ @Test
+ void testDocumentThatContainsTwoGraphExample() throws Exception {
+ String trig = """
+ # This document contains a same data as the
+ # previous example.
+
+ @prefix rdf: .
+ @prefix dc: .
+ @prefix foaf: .
+
+ # default graph - no {} used.
+ dc:publisher "Bob" .
+ dc:publisher "Alice" .
+
+ # GRAPH keyword to highlight a named graph
+ # Abbreviation of triples using ;
+ GRAPH
+ {
+ [] foaf:name "Bob" ;
+ foaf:mbox ;
+ foaf:knows _:b .
+ }
+
+ GRAPH
+ {
+ _:b foaf:name "Alice" ;
+ foaf:mbox
+ }
+ """.trim();
+
+ Model model = parseFromString(trig, null);
+ printModel(model);
+
+ assertEquals(7, model.size());
+
+ assertEquals(3, model.getNamespaces().size());
+
+ assertEquals(3, model.contexts().size());
+ }
+
+ @Test
+ void testNestedBlankNodesWithSharedIdentifiers() throws Exception {
+ String trig = """
+ @prefix ex: .
+
+ GRAPH ex:graph1 {
+ ex:Alice ex:knows [
+ ex:name "Bob" ;
+ ex:knows [
+ ex:name "Charlie"
+ ]
+ ] ;
+ ex:email "alice@example.org" .
+ }
+ """.trim();
+ Model model = parseFromString(trig, null);
+ printModel(model);
+
+ assertEquals(5, model.size());
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java
new file mode 100644
index 000000000..869f5ff5f
--- /dev/null
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java
@@ -0,0 +1,102 @@
+package fr.inria.corese.core.next.impl.io.parser.trig;
+
+import fr.inria.corese.core.next.api.Model;
+import fr.inria.corese.core.next.api.ValueFactory;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGLexer;
+import fr.inria.corese.core.next.impl.parser.antlr.TriGParser;
+import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
+import fr.inria.corese.core.next.impl.temp.CoreseModel;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.junit.jupiter.api.Test;
+
+import java.io.StringReader;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the TriGListenerImpl class.
+ * These tests verify that the listener correctly processes ANTLR parse tree contexts
+ * to extract and unescape RDF terms (IRIs, Blank Nodes, Literals) and add them to the model.
+ */
+class TriGListenerImplTest {
+ private Model parseTrig(String trigData) throws Exception {
+ ValueFactory factory = new CoreseAdaptedValueFactory();
+
+ CharStream input = CharStreams.fromReader(new StringReader(trigData));
+ TriGLexer lexer = new TriGLexer(input);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ TriGParser parser = new TriGParser(tokens);
+ ParseTree tree = parser.trigDoc();
+
+ Model model = new CoreseModel();
+ TriGListerner listener = new TriGListerner(model, factory, null);
+ ParseTreeWalker.DEFAULT.walk(listener, tree);
+
+ return model;
+ }
+
+ @Test
+ void testSimpleNamedGraph() throws Exception {
+ String trig = """
+ @prefix ex: .
+
+ GRAPH ex:graph {
+ ex:subject ex:predicate "Hello" .
+ }
+ """;
+
+ Model model = parseTrig(trig);
+ assertEquals(1, model.size());
+ assertEquals(1, model.contexts().size());
+ }
+
+ @Test
+ void testBlankNodeWithProperties() throws Exception {
+ String trig = """
+ @prefix ex: .
+ GRAPH ex:graph {
+ ex:Bob ex:knows [ ex:name "Charlie" ] .
+ }
+ """;
+
+ Model model = parseTrig(trig);
+ assertEquals(2, model.size());
+ }
+
+ @Test
+ void testMultipleGraphsAndBase() throws Exception {
+ String trig = """
+ @base .
+ @prefix dc: .
+ @prefix ex: .
+
+ dc:creator "Bob" .
+
+ GRAPH ex:other {
+ dc:creator "Alice" .
+ }
+ """;
+
+ Model model = parseTrig(trig);
+ assertEquals(2, model.contexts().size());
+ assertEquals(2, model.size());
+ }
+
+ @Test
+ void testTypedLiteralsAndLang() throws Exception {
+ String trig = """
+ @prefix ex: .
+ @prefix xsd: .
+
+ ex:subject ex:age "30"^^xsd:integer ;
+ ex:name "Jean"@fr .
+ """;
+
+ Model model = parseTrig(trig);
+ assertEquals(2, model.size());
+ }
+}
diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java
index 13a245645..bdf67298b 100644
--- a/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java
+++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java
@@ -11,6 +11,12 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+/**
+ * Unit tests for the ANTLRTurtle class.
+ * These tests verify the parser's ability to correctly parse Turtle
+ * and interact with the Model and ValueFactory, including error handling
+ * and unescaping of IRIs and literals, and named graphs.
+ */
public class ANTLRTurtleParserTest {
private Model parseFromString(String turtleData, String baseURI) throws Exception {
Model model = new CoreseModel();
diff --git a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java
index 9519cd4f0..77bfc928c 100644
--- a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java
+++ b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java
@@ -30,6 +30,21 @@ public void constructorStringTest() {
assertEquals("test", coreseIRI.getLocalName());
}
+ @Test
+ public void constructorStringTest_otherURIS() {
+ CoreseIRI coreseIRI_noSlash = new CoreseIRI("http://www.monicamurphy.org");
+ assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.stringValue());
+ assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.getCoreseNode().getLabel());
+ assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.getNamespace());
+ assertEquals("", coreseIRI_noSlash.getLocalName());
+
+ CoreseIRI coreseIRI_email = new CoreseIRI("mailto:monica@monicamurphy.org");
+ assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.stringValue());
+ assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.getCoreseNode().getLabel());
+ assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.getNamespace());
+ assertEquals("", coreseIRI_email.getLocalName());
+ }
+
@Test
public void constructorIriTest() {
CoreseIRI coreseIRI = new CoreseIRI("http://example.org/test");