diff --git a/src/main/antlr/TriG.g4 b/src/main/antlr/TriG.g4 deleted file mode 100644 index 1d0f7884a..000000000 --- a/src/main/antlr/TriG.g4 +++ /dev/null @@ -1,335 +0,0 @@ -// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false -// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging - -grammar TriG; - -trigDoc - : ( directive | block )* EOF - ; - -block - : triplesOrGraph - | wrappedGraph - | triples2 - | Graph_w labelOrSubject wrappedGraph - ; - -triplesOrGraph - : labelOrSubject (wrappedGraph | predicateObjectList '.') - ; - -triples2 - : blankNodePropertyList predicateObjectList? '.' - | collection predicateObjectList? '.' - ; - -wrappedGraph - : '{' triplesBlock? '}' - ; - -triplesBlock - : triples ('.' triplesBlock?)? - ; - -labelOrSubject - : iri - | blankNode - ; - -directive - : prefixID - | base - | sparqlPrefix - | sparqlBase - ; - -prefixID - : '@prefix' PNAME_NS IRIREF '.' - ; - -base - : '@base' IRIREF '.' - ; - -sparqlPrefix - : Prefix_w PNAME_NS IRIREF - ; - -sparqlBase - : Base_w IRIREF - ; - -triples - : subject predicateObjectList - | blankNodePropertyList predicateObjectList? - ; - -predicateObjectList - : verb objectList (';' (verb objectList)?)* - ; - -objectList - : object (',' object)* - ; - -verb - : predicate - | 'a' - ; - -subject - : iri - | blank - ; - -predicate - : iri - ; - -object - : iri - | blank - | blankNodePropertyList - | literal - ; - -literal - : rDFLiteral - | numericLiteral - | BooleanLiteral - ; - -blank - : blankNode - | collection - ; - -blankNodePropertyList - : '[' predicateObjectList ']' - ; - -collection - : '(' object* ')' - ; - -numericLiteral - : INTEGER - | DECIMAL - | DOUBLE - ; - -rDFLiteral - : string LANGTAG - | string ('^^' iri)? - ; - -string - : STRING_LITERAL_QUOTE - | STRING_LITERAL_SINGLE_QUOTE - | STRING_LITERAL_LONG_SINGLE_QUOTE - | STRING_LITERAL_LONG_QUOTE - ; - -iri - : prefixedName - | IRIREF - ; - -prefixedName - : PNAME_LN - | PNAME_NS - ; - -blankNode - : BLANK_NODE_LABEL - | ANON - ; - -WS - : (('\u0020' | '\u0009' | '\u000A' | '\u000D' ) )+ -> skip - ; - - -Graph_w options { caseInsensitive=true; } - : 'GRAPH' - ; - -Base_w options { caseInsensitive=true; } - : 'BASE' - ; - -Prefix_w options { caseInsensitive=true; } - : 'PREFIX' - ; - -BooleanLiteral - : 'true' - | 'false' - ; - -IRIREF - : '<' (PN_CHARS | '.' | ':' | '#' | '@' | '%' | '&' | '$' | '!' | '\'' | '*' | '+' | '/' | '(' | ')' | '-' | ',' | '?' | '~' | ';' | '=' | UCHAR)* '>' - ; - -BLANK_NODE_LABEL - : '_:' (PN_CHARS_U | [0-9] | PLX) ((PN_CHARS | '.' | PLX)* (PN_CHARS | PLX))? - ; - -PNAME_NS - : PN_PREFIX ':' - | ':' - ; - -PNAME_LN - : PNAME_NS PN_LOCAL - ; - -LANGTAG - : '@' ('a'.. 'z' | 'A' .. 'Z')+ ('-' ('a'.. 'z' | 'A' .. 'Z' | '0' .. '9')* )* - ; - -INTEGER - : ('+' | '-' )? ('0' .. '9')+ - ; - -DECIMAL - : ('+' | '-' )? ('0' .. '9')* '.' ('0' .. '9')+ - ; - -DOUBLE - : ('+' | '-')? ( - ( ('0'..'9')+ '.' ('0'..'9')* EXPONENT ) - | ( '.' ('0'..'9')+ EXPONENT ) - | ( ('0'..'9')+ EXPONENT ) - ) - ; - -EXPONENT - : ('e' | 'E') ('+' | '-' )? ('0' .. '9')+ - ; - -STRING_LITERAL_QUOTE - : '"' ((~[\u0022\u005C\u000A\u000D]) | ECHAR | UCHAR)* '"' - ; - -STRING_LITERAL_SINGLE_QUOTE - : '\'' ((~[\u0027\u005C\u000A\u000D]) | ECHAR | UCHAR)* '\'' - ; - -STRING_LITERAL_LONG_SINGLE_QUOTE - : '\'\'\'' (LONG_STRING_CHAR_SINGLE | ECHAR | UCHAR)* '\'\'\'' - ; - -STRING_LITERAL_LONG_QUOTE - : '"""' (LONG_STRING_CHAR_DOUBLE | ECHAR | UCHAR)* '"""' - ; - -fragment LONG_STRING_CHAR_DOUBLE - : ~[\\"] - | '"' ~["] - | '"' '"' ~["] - ; - -fragment LONG_STRING_CHAR_SINGLE - : ~[\\'] - | '\'' ~['] - | '\'' '\'' ~['] - ; - -UCHAR - : '\\u' HEX HEX HEX HEX - | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX - ; - -ECHAR options { caseInsensitive=true; } - : '\\' [tbnrf"'\\] - ; - -WHITESPACE - : [\u0020\u0009\u000A\u000D] - ; - -ANON - : '[' [\u0009\u000A\u000D\u0020]* ']' - ; - -fragment PN_CHARS_BASE - : 'A' .. 'Z' - | 'a' .. 'z' - | '\u00C0' .. '\u00D6' - | '\u00D8' .. '\u00F6' - | '\u00F8' .. '\u02FF' - | '\u0370' .. '\u037D' - | '\u037F' .. '\u1FFF' - | '\u200C' .. '\u200D' - | '\u2070' .. '\u218F' - | '\u2C00' .. '\u2FEF' - | '\u3001' .. '\uD7FF' - | '\uF900' .. '\uFDCF' - | '\uFDF0' .. '\uFFFD' - | '\u{10000}'..'\u{EFFFF}' - ; - -fragment PN_CHARS_U - : PN_CHARS_BASE - | '_' - ; - -fragment PN_CHARS - : PN_CHARS_U - | '-' - | [0-9] - | [\u00B7] - | [\u0300-\u036F] - | [\u203F-\u2040] - ; - -fragment PN_PREFIX - : PN_CHARS_BASE ((PN_CHARS | '.')* PN_CHARS)? - ; - -fragment PN_LOCAL - : (PN_CHARS_U | ':' | [0-9] | PLX) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX))? - ; - -fragment PLX - : PERCENT - | PN_LOCAL_ESC - ; - -fragment PERCENT - : '%' HEX HEX - ; - -fragment HEX - : [0-9a-fA-F] - ; - -fragment PN_LOCAL_ESC - : '\\' ( - '_' - | '~' - | '.' - | '-' - | '!' - | '$' - | '&' - | '\'' - | '(' - | ')' - | '*' - | '+' - | ',' - | ';' - | '=' - | '/' - | '?' - | '#' - | '@' - | '%' - ) - ; - -LC - : '#' ~[\r\n]+ -> channel(HIDDEN) - ; diff --git a/src/main/antlr/Turtle.g4 b/src/main/antlr/Turtle.g4 deleted file mode 100644 index e7260d14c..000000000 --- a/src/main/antlr/Turtle.g4 +++ /dev/null @@ -1,311 +0,0 @@ -/* - [The "BSD licence"] - Copyright (c) 2014, Alejandro Medrano (@ Universidad Politecnica de Madrid, http://www.upm.es/) - All rights reserved. -*/ -/* Derived from http://www.w3.org/TR/turtle/#sec-grammar-grammar */ - -grammar Turtle; - -turtleDoc - : statement* EOF - ; - -statement - : directive - | triples '.' - ; - -directive - : prefixID - | base - | sparqlPrefix - | sparqlBase - ; - -triples - : subject predicateObjectList - | blankNodePropertyList predicateObjectList? - ; - -predicateObjectList - : verb objectList (';' (verb objectList)?)* - ; - -objectList - : object_ (',' object_)* - ; - -verb - : predicate - | 'a' - ; - -subject - : iri - | BlankNode - | collection - ; - -predicate - : iri - ; - - -object_ - : iri - | BlankNode - | collection - | blankNodePropertyList - | literal - ; - -literal - : rdfLiteral - | numericLiteral - | BooleanLiteral - ; - -blankNodePropertyList - : '[' predicateObjectList ']' - ; - -collection - : '(' object_* ')' - ; - -prefixID - : '@prefix' PNAME_NS IRIREF '.' - ; - -base - : '@base' IRIREF '.' - ; - -sparqlBase - : Base_w IRIREF - ; - -sparqlPrefix - : Prefix_w PNAME_NS IRIREF - ; - -numericLiteral - : INTEGER - | DECIMAL - | DOUBLE - ; - -rdfLiteral - : string (LANGTAG | '^^' iri)? - ; - -BooleanLiteral - : 'true' - | 'false' - ; - -string - : STRING_LITERAL_QUOTE - | STRING_LITERAL_SINGLE_QUOTE - | STRING_LITERAL_LONG_SINGLE_QUOTE - | STRING_LITERAL_LONG_QUOTE - ; - -iri - : IRIREF - | PNAME_LN - | PNAME_NS - ; - -BlankNode - : BLANK_NODE_LABEL - | ANON - ; - -WS - : ([\t\r\n\u000C] | ' ')+ -> skip - ; - -// LEXER - -Base_w options { caseInsensitive=true; } - : 'BASE' - ; - -Prefix_w options { caseInsensitive=true; } - : 'PREFIX' - ; - -PN_PREFIX - : PN_CHARS_BASE ((PN_CHARS | '.')* PN_CHARS)? - ; - -IRIREF - : '<' (~( '\u0000' | '\u0020' | '<' | '>' | '"' | '{' | '}' | '|' | '^' | '`' | '\\' ) | UCHAR)* '>' - ; - -// Prefix alone -PNAME_NS - : PN_PREFIX? ':' - ; - -PNAME_LN - : PNAME_NS PN_LOCAL - ; - -BLANK_NODE_LABEL - : '_:' (PN_CHARS_U | '0' .. '9') ((PN_CHARS | '.')* PN_CHARS)? - ; - -LANGTAG - : '@' ('a'.. 'z' | 'A' .. 'Z')+ ('-' ('a'.. 'z' | 'A' .. 'Z' | '0' .. '9')* )* - ; - -INTEGER - : ('+' | '-' )? ('0' .. '9')+ - ; - -DECIMAL - : ('+' | '-' )? ('0' .. '9')* '.' ('0' .. '9')+ - ; - -DOUBLE - : ('+' | '-' )? (('0' .. '9')+ '.' ('0' .. '9')* EXPONENT - | '.' ('0' .. '9')+ EXPONENT - | ('0' .. '9')+ EXPONENT) - ; - -EXPONENT - : ('e' | 'E') ('+' | '-' )? ('0' .. '9')+ - ; - -STRING_LITERAL_LONG_SINGLE_QUOTE - : '\'\'\'' (LONG_STRING_CHAR_SINGLE | ECHAR | UCHAR)* '\'\'\'' - ; - -STRING_LITERAL_LONG_QUOTE - : '"""' (LONG_STRING_CHAR_DOUBLE | ECHAR | UCHAR)* '"""' - ; - -fragment LONG_STRING_CHAR_DOUBLE - : ~[\\"] - | '"' ~["] - | '"' '"' ~["] - ; - -fragment LONG_STRING_CHAR_SINGLE - : ~[\\'] - | '\'' ~['] - | '\'' '\'' ~['] - ; - -STRING_LITERAL_QUOTE - : '"' (~["\\\r\n] | ECHAR | UCHAR)* '"' - ; - -STRING_LITERAL_SINGLE_QUOTE - : '\'' (~['\\\r\n] | ECHAR | UCHAR)* '\'' - ; - -// Hexadecimal unicode character -UCHAR - : '\\u' HEX HEX HEX HEX - | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX - ; - -// Escaped character -ECHAR - : '\\' [tbnrf"'\\] - ; - -ANON_WS - : ' ' - | '\t' - | '\r' - | '\n' - ; - -ANON - : '[' ANON_WS* ']' - ; - -PN_CHARS_BASE - : 'A' .. 'Z' - | 'a' .. 'z' - | '\u00C0' .. '\u00D6' - | '\u00D8' .. '\u00F6' - | '\u00F8' .. '\u02FF' - | '\u0370' .. '\u037D' - | '\u037F' .. '\u1FFF' - | '\u200C' .. '\u200D' - | '\u2070' .. '\u218F' - | '\u2C00' .. '\u2FEF' - | '\u3001' .. '\uD7FF' - | '\uF900' .. '\uFDCF' - | '\uFDF0' .. '\uFFFD' - | '\u{10000}'..'\u{EFFFF}' - ; - -PN_CHARS_U - : PN_CHARS_BASE - | '_' - ; - -PN_CHARS - : PN_CHARS_U - | '-' - | [0-9] - | [\u00B7] - | [\u0300-\u036F] - | [\u203F-\u2040] - ; - -PN_LOCAL - : (PN_CHARS_U | ':' | [0-9] | PLX) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX))? - ; - -PLX - : PERCENT - | PN_LOCAL_ESC - ; - -PERCENT - : '%' HEX HEX - ; - -HEX - : [0-9] - | [A-F] - | [a-f] - ; - -PN_LOCAL_ESC - : '\\' ( - '_' - | '~' - | '.' - | '-' - | '!' - | '$' - | '&' - | '\'' - | '(' - | ')' - | '*' - | '+' - | ',' - | ';' - | '=' - | '/' - | '?' - | '#' - | '@' - | '%' - ) - ; - -LC - : '#' ~[\r\n]* -> channel(HIDDEN) - ; - diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/DefaultRDFParserFactory.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/DefaultRDFParserFactory.java index 34354bdca..fdf53c777 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/DefaultRDFParserFactory.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/DefaultRDFParserFactory.java @@ -17,10 +17,10 @@ import fr.inria.corese.core.next.data.impl.io.parser.rdfxml.RDFXMLParser; import fr.inria.corese.core.next.data.impl.io.parser.rdfxml.RDFXMLParserOptions; import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.data.impl.io.parser.turtle.TurtleParser; import fr.inria.corese.core.next.data.impl.io.parser.turtle.TurtleParserOptions; -import fr.inria.corese.core.next.data.impl.io.parser.trig.TriGParser; +import fr.inria.corese.core.next.data.impl.io.parser.turtle.TurtleScanner; import fr.inria.corese.core.next.data.impl.io.parser.trig.TriGParserOptions; +import fr.inria.corese.core.next.data.impl.io.parser.trig.TriGScanner; import fr.inria.corese.core.next.data.api.io.JSONLDOptions; import java.util.Objects; @@ -61,7 +61,7 @@ public RDFParser createRDFParser( if (RDFFormat.JSONLD.equals(format)) { return new JSONLDParser(model, factory, config); } else if (RDFFormat.TURTLE.equals(format)) { - return new TurtleParser(model, factory, config); + return new TurtleScanner(model, factory, config); } else if (RDFFormat.NTRIPLES.equals(format)) { return new NTriplesParser(model, factory, config); } else if (RDFFormat.NQUADS.equals(format) || RDFFormat.RDFC_1_0.equals(format)) { @@ -69,7 +69,7 @@ public RDFParser createRDFParser( } else if (RDFFormat.RDFXML.equals(format)) { return new RDFXMLParser(model, factory, config); } else if (RDFFormat.TRIG.equals(format)) { - return new TriGParser(model, factory, config); + return new TriGScanner(model, factory, config); } else if (RDFFormat.RDFA.equals(format)) { return new RDFaParser(model, factory, config); } @@ -91,7 +91,7 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac if (RDFFormat.JSONLD.equals(format)) { return new JSONLDParser(model, factory); } else if (RDFFormat.TURTLE.equals(format)) { - return new TurtleParser(model, factory); + return new TurtleScanner(model, factory); } else if (RDFFormat.NTRIPLES.equals(format)) { return new NTriplesParser(model, factory); } else if (RDFFormat.NQUADS.equals(format) || RDFFormat.RDFC_1_0.equals(format)) { @@ -99,7 +99,7 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac } else if (RDFFormat.RDFXML.equals(format)) { return new RDFXMLParser(model, factory); } else if (RDFFormat.TRIG.equals(format)) { - return new TriGParser(model, factory); + return new TriGScanner(model, factory); } else if (RDFFormat.RDFA.equals(format)) { return new RDFaParser(model, factory); } diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java deleted file mode 100644 index f4d1017d3..000000000 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/AbstractTurtleTriGListener.java +++ /dev/null @@ -1,424 +0,0 @@ -package fr.inria.corese.core.next.data.impl.io.parser.support; - -import fr.inria.corese.core.next.common.text.RdfText; -import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.literal.XSDDatatype; -import fr.inria.corese.core.next.data.api.model.Model; -import fr.inria.corese.core.next.data.api.term.*; -import fr.inria.corese.core.next.data.api.vocabulary.RDF; -import fr.inria.corese.core.next.data.impl.namespace.PrefixHandler; -import fr.inria.corese.core.next.data.spi.term.IRIUtils; - -import java.util.HashMap; -import java.util.Map; - -import static fr.inria.corese.core.next.data.spi.term.IRIUtils.isAbsoluteIRI; -import static fr.inria.corese.core.next.data.spi.term.IRIUtils.normalizeURI; - -/** - * Base class for RDF parsers (Turtle, TriG) providing common functionality. - * Implements IRI resolution according to RFC 3986, Unicode escape handling, - * prefix management, and RDF term creation. - * - */ -public abstract class AbstractTurtleTriGListener { - - public final Model model; - public final ValueFactory factory; - public final PrefixHandler prefixHandler; - - public String baseURI; - - public Resource currentSubject; - public IRI currentPredicate; - - /** - * Per-document blank-node scope. - */ - private final Map blankNodeScope = new HashMap<>(); - - /** - * Constructs a parser listener with the specified model, factory and base URI. - * - * @param model RDF model to populate with parsed statements - * @param factory factory for creating RDF terms (IRIs, blank nodes, literals) - * @param baseURI base URI for resolving relative IRI references - */ - protected AbstractTurtleTriGListener(Model model, ValueFactory factory, String baseURI) { - this.model = model; - this.factory = factory; - this.baseURI = baseURI; - this.prefixHandler = new PrefixHandler(true); - - } - - /** - * Returns the blank node for the given Turtle/TriG label, creating a fresh one on first use. - * - *

Using this method instead of {@code factory.createBNode(label)} ensures that blank-node - * labels are scoped to this parse session: the same label always maps to the same blank node - * within one document, but maps to a different blank node in every other document.

- * - * @param label the raw blank-node label (without the {@code _:} prefix) - * @return the document-scoped blank node for {@code label} - */ - public BNode scopedBlankNode(String label) { - return blankNodeScope.computeIfAbsent(label, k -> factory.createBNode()); - } - - /** - * Extracts IRI from angle brackets and processes Unicode escape sequences. - * - * @param text raw IRI text including angle brackets - * @return unescaped IRI string - * @throws ParsingException if the IRI contains invalid characters after escape processing - */ - public String extractAndUnescapeIRI(String text) { - String iri = text.substring(1, text.length() - 1); - return unescapeIRI(iri); - } - - /** - * Updates the base URI and registers it in prefix mappings. - * - * @param newBase new base URI to set - */ - public void updateBaseURI(String newBase) { - this.baseURI = resolveIRIAgainstBase(newBase); - validateIRI(this.baseURI); - } - - /** - * Registers a namespace prefix with its corresponding IRI. - * - * @param prefix namespace prefix - * @param iri namespace IRI - */ - public void registerPrefix(String prefix, String iri) { - String resolvedIRI = resolveIRIAgainstBase(iri); - validateIRI(resolvedIRI); - prefixHandler.setPrefix(prefix, resolvedIRI); - model.setNamespace(prefix, resolvedIRI); - } - - /** - * Resolves an IRI reference to absolute form. - * Handles prefixed names (QNames), relative IRIs, and absolute IRIs. - * - * @param raw raw IRI string - * @return resolved absolute IRI - * @throws ParsingException if the IRI cannot be resolved - */ - @SuppressWarnings("java:S3776") - public String resolveIRI(String raw) { - try { - - raw = raw.trim(); - - if (raw.equals(ParserConstants.RDF_TYPE_SHORTCUT)) { - return RDF.type.getIRI().stringValue(); - } - - if (raw.equals(ParserConstants.COLON)) { - String ns = prefixHandler.getNamespace(ParserConstants.EMPTY_STRING); - if (ns == null) { - throw new ParsingException( - "Undeclared prefix: '' (empty prefix). " + - "Use '@prefix : .' to declare it."); - } - return ns; - } - - if (raw.startsWith(ParserConstants.IRI_START) && raw.endsWith(ParserConstants.IRI_END)) { - String iri = raw.substring(1, raw.length() - 1); - iri = unescapeIRI(iri); - validateIRI(iri); - return iri.isEmpty() ? getEffectiveBaseURI() : resolveIRIAgainstBase(iri); - } - - if (raw.contains(ParserConstants.COLON)) { - String[] parts = raw.split(ParserConstants.COLON, 2); - String prefix = parts[0]; - String localName = parts[1]; - - if (prefixHandler.hasPrefix(prefix)) { - localName = unescapeIRI(localName); - String ns = prefixHandler.getNamespace(prefix); - if (ns != null) { - String result = ns + localName; - validateIRI(result); - return result; - } - } else if (isAbsoluteIRI(raw)) { - return raw; - } else { - if (prefix.isEmpty()) { - throw new ParsingException( - "Undeclared prefix: '' (empty prefix). " + - "Use '@prefix : .' to declare it."); - } - throw new ParsingException("Undeclared prefix: " + prefix); - } - } - - return resolveIRIAgainstBase(raw); - - } catch (ParsingException e) { - throw new ParsingException(e.getMessage(), e); - } - } - - /** - * Resolves a relative IRI reference against the base URI using RFC 3986 algorithm. - * - * @param iri IRI reference to resolve - * @return resolved absolute IRI - */ - public String resolveIRIAgainstBase(String iri) { - String effectiveBase = getEffectiveBaseURI(); - - if (isAbsoluteIRI(iri)) { - return iri; - } - return IRIUtils.resolveIRIAgainstBase(effectiveBase, iri); - } - - /** - * Returns the effective base URI, using a default if none is set. - * - * @return effective base URI - */ - public String getEffectiveBaseURI() { - String effective = (baseURI != null && !baseURI.isEmpty()) ? baseURI : ParserConstants.getDefaultBaseURI(); - return normalizeURI(effective); - } - - /** - * Processes Unicode escape sequences in IRIs. - * - * @param rawIri IRI string potentially containing escape sequences - * @return unescaped IRI string - * @throws IllegalArgumentException if escape sequences are malformed or contain surrogates - */ - @SuppressWarnings({"java:S3776", "java:S127"}) - public String unescapeIRI(String rawIri) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < rawIri.length(); i++) { - char c = rawIri.charAt(i); - if (c == '\\' && i + 1 < rawIri.length()) { - char next = rawIri.charAt(i + 1); - if (next == 'u' || next == 'U') { - int len = (next == 'u') ? 4 : 8; - if (i + len + 1 <= rawIri.length()) { - String hex = rawIri.substring(i + 2, i + 2 + len); - int codePoint = Integer.parseInt(hex, 16); - if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { - throw new IllegalArgumentException("Surrogates not allowed: \\u" + hex); - } - sb.appendCodePoint(codePoint); - i += len + 1; - } else { - throw new IllegalArgumentException("Incomplete Unicode escape"); - } - } else { - sb.append(next); - i++; - } - } else { - sb.append(c); - } - } - return sb.toString(); - } - - /** - * Processes escape sequences in RDF string literals. - * Handles standard escapes (\t, \n, \r, etc.) and Unicode escapes. - * - * @param text literal string including delimiters (quotes or triple-quotes) - * @return unescaped string content - * @throws IllegalArgumentException if escape sequences are malformed - */ - @SuppressWarnings({"java:S3776", "java:S127"}) - public String unescapeString(String text) { - if (text == null || text.length() < 2) { - return text; - } - - boolean isMultiline = text.startsWith(ParserConstants.TRIPLE_QUOTE) || text.startsWith(ParserConstants.TRIPLE_APOSTROPHE); - String content = isMultiline ? text.substring(3, text.length() - 3) : text.substring(1, text.length() - 1); - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < content.length(); i++) { - char c = content.charAt(i); - if (c == '\\' && i + 1 < content.length()) { - char next = content.charAt(i + 1); - switch (next) { - case 't': - sb.append('\t'); - i++; - break; - case 'n': - sb.append('\n'); - i++; - break; - case 'r': - sb.append('\r'); - i++; - break; - case 'b': - sb.append('\b'); - i++; - break; - case 'f': - sb.append('\f'); - i++; - break; - case '"': - sb.append('"'); - i++; - break; - case '\'': - sb.append('\''); - i++; - break; - case '\\': - sb.append('\\'); - i++; - break; - case 'u', 'U': - int len = (next == 'u') ? 4 : 8; - if (i + len + 1 <= content.length()) { - String hex = content.substring(i + 2, i + 2 + len); - int codePoint = Integer.parseInt(hex, 16); - if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { - throw new IllegalArgumentException("Invalid Unicode escape: Surrogate code points not allowed"); - } - sb.appendCodePoint(codePoint); - i += len + 1; - } else { - throw new IllegalArgumentException("Incomplete Unicode escape sequence"); - } - break; - default: - sb.append(c).append(next); - i++; - break; - } - } else { - sb.append(c); - } - } - return sb.toString(); - } - - /** - * Adds an RDF statement to the model with exception handling. - * Subclasses may override to support named graphs or other extensions. - * - * @param subject statement subject - * @param predicate statement predicate - * @param object statement object - * @throws ParsingException if the statement cannot be added - */ - public void safeAddStatement(Resource subject, IRI predicate, Value object) { - try { - model.add(subject, predicate, object); - } catch (Exception e) { - throw new ParsingException("Failed to add statement: " + e.getMessage(), e); - } - } - - /** - * Creates an RDF literal with optional language tag or datatype. - * - * @param label literal lexical value - * @param langTag language tag (may be null) - * @param datatypeIRI datatype IRI (may be null) - * @return RDF literal - */ - public Literal createLiteral(String label, String langTag, String datatypeIRI) { - if (langTag != null) { - return factory.createLiteral(label, langTag); - } - if (datatypeIRI != null) { - return factory.createLiteral(label, factory.createIRI(datatypeIRI)); - } - return factory.createLiteral(label); - } - - /** - * Creates a boolean literal. - * - * @param text boolean value as string ("true" or "false") - * @return boolean literal with xsd:boolean datatype - */ - public Literal createBooleanLiteral(String text) { - return factory.createLiteral(text, XSDDatatype.BOOLEAN.getIRI()); - } - - /** - * Creates a numeric literal with appropriate XSD datatype. - * - * @param text numeric value as string - * @param type numeric type (INTEGER, DECIMAL, or DOUBLE) - * @return numeric literal with corresponding XSD datatype - */ - public Literal createNumericLiteral(String text, NumericType type) { - return switch (type) { - case DOUBLE -> factory.createLiteral(text, XSDDatatype.DOUBLE.getIRI()); - case DECIMAL -> factory.createLiteral(text, XSDDatatype.DECIMAL.getIRI()); - default -> factory.createLiteral(text, XSDDatatype.INTEGER.getIRI()); - }; - } - - /** - * Validates that an IRI contains only valid characters after escape sequence processing. - * - * @param iri the IRI string to validate (after escape sequences have been processed) - * @throws ParsingException if the IRI contains forbidden characters - */ - private void validateIRI(String iri) throws ParsingException { - if (iri == null || iri.isEmpty()) { - return; - } - - // Check each character in the IRI - for (int i = 0; i < iri.length(); i++) { - char c = iri.charAt(i); - - // Check for forbidden characters - if (IRIUtils.isInvalidIRICharacter(c)) { - String codePoint = String.format("U+%04X", (int) c); - String charDesc = RdfText.describeCharacter(c); - String displayIRI = RdfText.escapeForDisplay(iri); - - throw new ParsingException( - "Invalid character in IRI: " + codePoint + " (" + charDesc + ") " + - "at position " + i + ". " + - "IRI after escape processing: " + displayIRI + ". " + - "IRIs cannot contain space, control characters, or reserved characters." - ); - } - } - } - - /** - * Enumeration of numeric literal types corresponding to XSD datatypes. - */ - public enum NumericType { - /** - * XSD integer type - */ - INTEGER, - /** - * XSD decimal type - */ - DECIMAL, - /** - * XSD double type - */ - DOUBLE - } -} diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/TurtleTriGDocument.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/TurtleTriGDocument.java new file mode 100644 index 000000000..f813f4285 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/support/TurtleTriGDocument.java @@ -0,0 +1,904 @@ +package fr.inria.corese.core.next.data.impl.io.parser.support; + +import fr.inria.corese.core.next.data.api.exception.IncorrectFormatException; +import fr.inria.corese.core.next.data.api.exception.ParsingException; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; +import fr.inria.corese.core.next.data.api.model.Model; +import fr.inria.corese.core.next.data.api.term.BNode; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.term.Resource; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.data.spi.term.IRIUtils; + +import java.io.IOException; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +/** + * Character-level streaming FSM for Turtle and TriG documents. + * + *

Implements an event-driven byte-streaming scanner that handles Turtle/TriG + * directives ({@code @prefix}, {@code PREFIX}, {@code @base}, {@code BASE}) and + * triples/quads without allocating intermediate AST nodes. Override + * {@link #emitTriple} to route statements to named graphs (TriG) or override + * {@link #parseDocument} to intercept graph blocks.

+ * + *

Memory is O(longest term + distinct blank node labels + IRI cache size). + * Diagnostics use one-based lines and UTF-16 columns; CRLF counts as one line + * ending. Input is borrowed and never closed.

+ */ +public class TurtleTriGDocument { + + // ── Constants ────────────────────────────────────────────────────────── + static final int BUFFER_SIZE = 64 * 1024; + static final int IRI_CACHE_LIMIT = 4096; + private static final int NO_PUSHBACK = Integer.MIN_VALUE; + private static final String RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; + private static final String RDF_FIRST = "http://www.w3.org/1999/02/22-rdf-syntax-ns#first"; + private static final String RDF_REST = "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"; + private static final String RDF_NIL = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"; + private static final String XSD_INTEGER = "http://www.w3.org/2001/XMLSchema#integer"; + private static final String XSD_DECIMAL = "http://www.w3.org/2001/XMLSchema#decimal"; + private static final String XSD_DOUBLE = "http://www.w3.org/2001/XMLSchema#double"; + private static final String XSD_BOOLEAN = "http://www.w3.org/2001/XMLSchema#boolean"; + + // ── I/O state ────────────────────────────────────────────────────────── + private final Reader reader; + private final char[] buffer = new char[BUFFER_SIZE]; + private int bufOffset; + private int bufLimit; + private boolean eof; + /** One-character pushback slot used for dot-in-local-name disambiguation. */ + private int pushback = NO_PUSHBACK; + + // ── Position tracking ────────────────────────────────────────────────── + protected long line = 1; + protected long column = 1; + private boolean afterCr; + + // ── RDF infrastructure ───────────────────────────────────────────────── + protected final Model model; + protected final ValueFactory factory; + + // ── Shared term builder – reset before every leaf token ──────────────── + private final StringBuilder term = new StringBuilder(256); + + // ── Term caches ──────────────────────────────────────────────────────── + private final Map iriCache = new HashMap<>(); + private final Map blankNodes = new HashMap<>(); + + // ── Prefix / base state ──────────────────────────────────────────────── + protected final Map prefixes = new HashMap<>(); + protected String baseURI; + + // ── Pre-cached RDF / XSD constants ──────────────────────────────────── + private final IRI rdfType; + private final IRI rdfFirst; + private final IRI rdfRest; + private final IRI rdfNil; + private final IRI xsdInteger; + private final IRI xsdDecimal; + private final IRI xsdDouble; + private final IRI xsdBoolean; + + + public TurtleTriGDocument(Reader reader, Model model, ValueFactory factory, String baseURI) { + this.reader = reader; + this.model = model; + this.factory = factory; + this.baseURI = baseURI != null ? baseURI : ""; + rdfType = intern(RDF_TYPE); + rdfFirst = intern(RDF_FIRST); + rdfRest = intern(RDF_REST); + rdfNil = intern(RDF_NIL); + xsdInteger = intern(XSD_INTEGER); + xsdDecimal = intern(XSD_DECIMAL); + xsdDouble = intern(XSD_DOUBLE); + xsdBoolean = intern(XSD_BOOLEAN); + } + + + public final void parse() { + try { + parseDocument(); + } catch (IOException e) { + throw new ParsingException(location("Failed to read Turtle/TriG input"), e); + } catch (IllegalArgumentException | IncorrectFormatException e) { + throw new ParsingException(location("Value factory rejected RDF term: " + e.getMessage()), e); + } + } + + + protected void parseDocument() throws IOException { + while (true) { + ws(); + if (peek() == -1) break; + checkInterrupted(); + topLevel(); + } + } + + /** + * Dispatches one top-level item: {@code @prefix}, {@code @base}, + * SPARQL {@code PREFIX}/{@code BASE}, or a triple statement. + */ + protected void topLevel() throws IOException { + int c = peek(); + + // @prefix / @base + if (c == '@') { atDirective(); return; } + + // Blank node property list as subject: [ ... ] predicateObjectList? . + if (c == '[') { + Resource subj = blankNodePropertyList(); + ws(); + if (peek() != '.') { predicateObjectList(subj); ws(); } + expect('.'); + return; + } + + // Collection as subject: ( items ) predicateObjectList . + if (c == '(') { + Resource subj = collection(); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + return; + } + + // Angle-bracket IRI as subject + if (c == '<') { + Resource subj = intern(iriRefContent()); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + return; + } + + // Blank node _:label as subject + if (c == '_') { + Resource subj = blankNodeLabel(); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + return; + } + + // Default-prefix name :local + if (c == ':') { + take(); + Resource subj = intern(lookupPrefix("") + readLocalPart()); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + return; + } + + // Name: SPARQL keyword (PREFIX / BASE) or prefixed-name subject + if (isAsciiAlpha(c) || unicodeNameStart(c)) { + String name = readName(); + if (peek() == ':') { + take(); + Resource subj = intern(lookupPrefix(name) + readLocalPart()); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + } else if (name.equalsIgnoreCase("PREFIX")) { + ws(); + sparqlPrefixDirective(); + } else if (name.equalsIgnoreCase("BASE")) { + ws(); + sparqlBaseDirective(); + } else { + throw error("Expected ':' after '" + name + "' or a valid directive keyword"); + } + return; + } + + throw error("Unexpected character at statement start: '" + (char) c + "'"); + } + + + protected void atDirective() throws IOException { + expect('@'); + String kw = readName(); + ws(); + if (kw.equals("prefix")) turtlePrefixBody(false); + else if (kw.equals("base")) turtleBaseBody(false); + else throw error("Unknown directive: @" + kw); + } + + protected void sparqlPrefixDirective() throws IOException { turtlePrefixBody(true); } + protected void sparqlBaseDirective() throws IOException { turtleBaseBody(true); } + + protected void turtlePrefixBody(boolean sparql) throws IOException { + term.setLength(0); + while (peek() != ':' && peek() != -1 && !isWhitespace(peek())) { + term.appendCodePoint(scalar()); + } + String prefix = term.toString(); + expect(':'); + ws(); + String ns = iriRefContent(); + registerPrefix(prefix, ns); + if (!sparql) { ws(); expect('.'); } + } + + protected void turtleBaseBody(boolean sparql) throws IOException { + baseURI = iriRefContent(); + if (!sparql) { ws(); expect('.'); } + } + + protected void registerPrefix(String prefix, String ns) { + prefixes.put(prefix, ns); + model.setNamespace(prefix, ns); + } + + + protected void predicateObjectList(Resource subj) throws IOException { + IRI pred = verb(); + ws(); + objectList(subj, pred); + ws(); + while (peek() == ';') { + take(); + ws(); + int c = peek(); + if (c == '.' || c == '}' || c == ']' || c == -1) break; // trailing semicolon is legal + if (c != ';') { // Consecutive semicolons are legal. + pred = verb(); + ws(); + objectList(subj, pred); + ws(); + } + } + } + + private void objectList(Resource subj, IRI pred) throws IOException { + Value obj = object(); + emitTriple(subj, pred, obj); + ws(); + while (peek() == ',') { + take(); + ws(); + obj = object(); + emitTriple(subj, pred, obj); + ws(); + } + } + + protected IRI verb() throws IOException { + int c = peek(); + if (c == '<') return intern(iriRefContent()); + if (c == '_') throw error("Blank node cannot be used as predicate"); + if (c == ':') { take(); return intern(lookupPrefix("") + readLocalPart()); } + String name = readName(); + if (peek() == ':') { take(); return intern(lookupPrefix(name) + readLocalPart()); } + if (name.equals("a")) return rdfType; + throw error("Expected predicate IRI or 'a', got: '" + name + "'"); + } + + + protected Resource subject() throws IOException { + int c = peek(); + if (c == '<') return intern(iriRefContent()); + if (c == '_') return blankNodeLabel(); + if (c == '[') return blankNodePropertyList(); + if (c == '(') return collection(); + if (c == ':') { take(); return intern(lookupPrefix("") + readLocalPart()); } + if (isAsciiAlpha(c) || unicodeNameStart(c)) { + String name = readName(); + if (peek() == ':') { take(); return intern(lookupPrefix(name) + readLocalPart()); } + throw error("Expected ':' after prefix '" + name + "'"); + } + throw error("Expected subject (IRI, blank node, or collection), got: '" + (char) c + "'"); + } + + + protected Value object() throws IOException { + int c = peek(); + return switch (c) { + case '<' -> intern(iriRefContent()); + case '_' -> blankNodeLabel(); + case '[' -> blankNodePropertyList(); + case '(' -> collection(); + case '"', '\'' -> literal(); + case '+', '-' -> numericLiteral(); + case '.' -> numericLeadingDot(); + case ':' -> { + take(); + yield intern(lookupPrefix("") + readLocalPart()); + } + default -> digit(c) ? numericLiteral() : namedObject(c); + }; + } + + private Value namedObject(int c) throws IOException { + if (!isAsciiAlpha(c) && !unicodeNameStart(c)) { + throw error("Expected object (IRI, literal, blank node, or collection), got: '" + (char) c + "'"); + } + String name = readName(); + if (peek() == ':') { + take(); + return intern(lookupPrefix(name) + readLocalPart()); + } + return switch (name) { + case "true", "false" -> factory.createLiteral(name, xsdBoolean); + default -> throw error("Unexpected token in object position: '" + name + "'"); + }; + } + + + /** + * Reads {@code }, resolves against {@link #baseURI}, and returns + * the absolute IRI string. Does not create an IRI object; caller should use + * {@link #intern(String)}. + */ + protected String iriRefContent() throws IOException { + expect('<'); + term.setLength(0); + while (peek() != '>') { + int c = peek(); + if (c == -1 || isLineEnd(c)) throw error("Unterminated IRI reference"); + int scalar = (c == '\\') ? escape(false) : scalar(); + if (forbiddenIriChar(scalar)) throw error("Forbidden character U+" + Integer.toHexString(scalar) + " in IRI"); + term.appendCodePoint(scalar); + } + expect('>'); + return resolveIRI(term.toString()); + } + + protected String resolveIRI(String raw) { + if (raw.isEmpty()) { + int hash = baseURI.indexOf('#'); + return hash >= 0 ? baseURI.substring(0, hash) : baseURI; + } + if (isAbsoluteIri(raw)) return raw; + if (baseURI.isEmpty()) return raw; + return IRIUtils.resolveIRIAgainstBase(baseURI, raw); + } + + protected String lookupPrefix(String prefix) { + String ns = prefixes.get(prefix); + if (ns == null) { + throw error(prefix.isEmpty() + ? "Undeclared default prefix ':'; declare with '@prefix : .'" + : "Undeclared prefix: '" + prefix + "'"); + } + return ns; + } + + + protected BNode blankNodeLabel() throws IOException { + expect('_'); + expect(':'); + term.setLength(0); + int first = scalar(); + if (!nameStart(first) && !digit(first)) throw error("Invalid blank node label start character"); + term.appendCodePoint(first); + int trailingDots = blankNodeTail(); + if (trailingDots > 1) throw error("Blank node label cannot end with multiple dots"); + if (trailingDots == 1) { + term.setLength(term.length() - 1); + pushBack(); + } + return blankNodes.computeIfAbsent(term.toString(), k -> factory.createBNode()); + } + + private int blankNodeTail() throws IOException { + int dots = 0; + while (true) { + int c = peek(); + if (c == '.') { + take(); term.append('.'); dots++; + } else if (nameCharacter(c) || Character.isHighSurrogate((char) c)) { + int cp = scalar(); + if (!nameCharacter(cp)) throw error("Invalid character in blank node label"); + term.appendCodePoint(cp); dots = 0; + } else { + return dots; + } + } + } + + + protected Resource blankNodePropertyList() throws IOException { + expect('['); + ws(); + BNode bnode = factory.createBNode(); + if (peek() != ']') { + predicateObjectList(bnode); + ws(); + } + expect(']'); + return bnode; + } + + + protected Resource collection() throws IOException { + expect('('); + ws(); + if (peek() == ')') { take(); return rdfNil; } + BNode head = factory.createBNode(); + BNode current = head; + while (true) { + Value item = object(); + ws(); + emitTriple(current, rdfFirst, item); + if (peek() == ')') { + emitTriple(current, rdfRest, rdfNil); + take(); + break; + } + BNode next = factory.createBNode(); + emitTriple(current, rdfRest, next); + current = next; + } + return head; + } + + + private Literal literal() throws IOException { + int q = peek(); + take(); // opening quote + String label; + if (peek() == q) { // potential triple quote or empty string + take(); + if (peek() == q) { // confirmed triple quote + take(); + label = longStringContent((char) q); + } else { + label = ""; // empty short string "" + } + } else { + label = shortStringContent((char) q); + } + ws(); // Turtle permits whitespace before language tags and datatype suffixes. + // Language tag + if (peek() == '@') { + take(); + return factory.createLiteral(label, language()); + } + // Datatype IRI ^^ or ^^prefix:local + if (peek() == '^') { + expect('^'); expect('^'); + ws(); + String dtIri; + if (peek() == '<') { + dtIri = iriRefContent(); + } else { + String pfx = peek() == ':' ? "" : readName(); + expect(':'); + dtIri = lookupPrefix(pfx) + readLocalPart(); + } + return factory.createLiteral(label, intern(dtIri)); + } + return factory.createLiteral(label); + } + + private String shortStringContent(char q) throws IOException { + term.setLength(0); + while (true) { + int c = peek(); + if ((char) c == q) { take(); return term.toString(); } + if (c == -1 || isLineEnd(c)) throw error("Unterminated string literal"); + term.appendCodePoint(c == '\\' ? escape(true) : scalar()); + } + } + + private String longStringContent(char q) throws IOException { + term.setLength(0); + while (true) { + int c = peek(); + if (c == -1) throw error("Unterminated long string literal"); + if ((char) c == q) { + if (consumeLongStringQuotes(q)) return term.toString(); + } else { + term.appendCodePoint(c == '\\' ? escape(true) : scalar()); + } + } + } + + /** Consumes up to three quotes, appending them unless they close the string. */ + private boolean consumeLongStringQuotes(char quote) throws IOException { + take(); + if (peek() != quote) { + term.append(quote); + return false; + } + take(); + if (peek() != quote) { + term.append(quote).append(quote); + return false; + } + take(); + return true; + } + + private String language() throws IOException { + term.setLength(0); + if (!letter(peek())) throw error("Language tag must start with an ASCII letter"); + while (letter(peek())) term.append((char) take()); + while (peek() == '-') { + term.append((char) take()); + if (!letter(peek()) && !digit(peek())) throw error("Expected alphanumeric language subtag after '-'"); + while (letter(peek()) || digit(peek())) term.append((char) take()); + } + return term.toString(); + } + + + private Literal numericLiteral() throws IOException { + term.setLength(0); + int c = peek(); + if (c == '+' || c == '-') term.append((char) take()); // sign + if (!digit(peek()) && peek() != '.') throw error("Expected digit or '.' in numeric literal"); + return numericBody(); + } + + /** Called when leading '.' is seen in object position (potential {@code .5}). */ + private Value numericLeadingDot() throws IOException { + // We haven't consumed the '.' yet. Check next char for digit. + // Peek at buffer position +1 if available; otherwise take+pushBack. + boolean followedByDigit; + if (pushback == NO_PUSHBACK && bufOffset + 1 < bufLimit) { + followedByDigit = digit(buffer[bufOffset + 1]); + } else { + take(); // consume '.' + followedByDigit = digit(peek()); + pushBack(); + } + if (!followedByDigit) throw error("Unexpected '.' in object position"); + return numericLiteral(); + } + + private Literal numericBody() throws IOException { + appendDigits(); + boolean hasDot = readFractionalPart(); + boolean hasExp = readExponent(); + String text = term.toString(); + if (term.isEmpty() || text.equals("+") || text.equals("-")) + throw error("Invalid numeric literal: '" + text + "'"); + if (hasExp) return factory.createLiteral(text, xsdDouble); + if (hasDot) return factory.createLiteral(text, xsdDecimal); + return factory.createLiteral(text, xsdInteger); + } + + private void appendDigits() throws IOException { + while (digit(peek())) term.append((char) take()); + } + + private boolean readFractionalPart() throws IOException { + // Decimal dot (only if followed by digit or exponent — e.g. "1.e5" is valid) + if (peek() != '.') return false; + take(); // tentatively consume + if (digit(peek()) || peek() == 'e' || peek() == 'E') { + term.append('.'); + appendDigits(); + return true; + } + pushBack(); // not a decimal dot + return false; + } + + private boolean readExponent() throws IOException { + int e = peek(); + if (e != 'e' && e != 'E') return false; + term.append((char) take()); + int sign = peek(); + if (sign == '+' || sign == '-') term.append((char) take()); + if (!digit(peek())) throw error("Expected digits after exponent marker"); + appendDigits(); + return true; + } + + /** + * Reads the local-name part of a prefixed name (after the {@code :}). + * Handles {@code %XX} percent-encoded sequences and {@code \X} backslash escapes. + * Internal dots are included only when followed by another local-name character. + */ + protected String readLocalPart() throws IOException { + term.setLength(0); + // '-' is a PN_CHARS character but NOT a valid PN_LOCAL start character + if (peek() == '-') throw error("Local name cannot begin with '-'"); + while (appendLocalNameCharacter()) { + // Each successful step appends one local-name character or escape. + } + return term.toString(); + } + + private boolean appendLocalNameCharacter() throws IOException { + int c = peek(); + switch (c) { + case '%': + appendLocalNamePercentEncoding(); + return true; + case '\\': + appendLocalNameEscape(); + return true; + case '.': + return appendLocalNameDot(); + default: + if (!localNameChar(c) && !Character.isHighSurrogate((char) c)) return false; + int cp = scalar(); + if (!localNameChar(cp)) return false; + term.appendCodePoint(cp); + return true; + } + } + + private void appendLocalNamePercentEncoding() throws IOException { + take(); + term.append('%'); + int h1 = take(); + int h2 = take(); + if (hexValue(h1) < 0 || hexValue(h2) < 0) throw error("Invalid %XX encoding in local name"); + term.append((char) h1).append((char) h2); + } + + private void appendLocalNameEscape() throws IOException { + take(); + int esc = take(); + if (!isLocalNameEscapeChar(esc)) throw error("Invalid '\\' escape in local name: '\\" + (char) esc + "'"); + term.append((char) esc); + } + + private boolean appendLocalNameDot() throws IOException { + take(); + int next = peek(); + if (localNameChar(next) || next == '.' || next == '%' || next == '\\' + || Character.isHighSurrogate((char) next)) { + term.append('.'); + return true; + } + pushBack(); + return false; + } + + + /** + * Reads a name token (prefix label, keyword, or {@code a}). + * Stops at {@code :}, whitespace, or non-name characters. + * Internal dots are included; a single trailing dot is pushed back as punctuation. + */ + protected String readName() throws IOException { + term.setLength(0); + int first = scalar(); + if (!nameStart(first) && !digit(first)) + throw error("Expected name start character, got: '" + (char) first + "'"); + term.appendCodePoint(first); + while (true) { + int c = peek(); + if (nameCharacter(c) || Character.isHighSurrogate((char) c)) { + term.appendCodePoint(scalar()); + } else if (c == '.') { + take(); term.append('.'); + } else { + break; + } + } + int trailingDots = 0; + while (!term.isEmpty() && term.charAt(term.length() - 1) == '.') { + term.setLength(term.length() - 1); + trailingDots++; + } + if (trailingDots > 1) { + throw error("Prefix or name cannot end with multiple dots"); + } + if (trailingDots == 1) { + pushBack(); + } + return term.toString(); + } + + + /** + * Emits a parsed triple. Override in TriG subclasses to include named graph context. + */ + protected void emitTriple(Resource s, IRI p, Value o) { + model.add(s, p, o); + } + + + protected IRI intern(String text) { + IRI cached = iriCache.get(text); + if (cached != null) return cached; + IRI result = factory.createIRI(text); + if (iriCache.size() == IRI_CACHE_LIMIT) iriCache.clear(); + iriCache.put(text, result); + return result; + } + + private int escape(boolean inLiteral) throws IOException { + expect('\\'); + int type = take(); + if (type == 'u' || type == 'U') return unicodeEscape(type == 'u' ? 4 : 8); + if (!inLiteral) throw error("Only Unicode escapes (\\uXXXX / \\UXXXXXXXX) are allowed in IRIs"); + return switch (type) { + case 't' -> '\t'; + case 'b' -> '\b'; + case 'n' -> '\n'; + case 'r' -> '\r'; + case 'f' -> '\f'; + case '"' -> '"'; + case '\'' -> '\''; + case '\\' -> '\\'; + default -> throw error("Invalid literal escape sequence: '\\" + (char) type + "'"); + }; + } + + private int unicodeEscape(int digits) throws IOException { + long result = 0; + for (int i = 0; i < digits; i++) { + int hex = hexValue(peek()); + if (hex < 0) throw error("Expected hexadecimal digit in Unicode escape"); + take(); + result = (result << 4) | hex; + } + if (result > Character.MAX_CODE_POINT || (result >= 0xD800 && result <= 0xDFFF)) { + throw error("Unicode escape 0x" + Long.toHexString(result) + " is not a valid scalar value"); + } + return (int) result; + } + + + /** Skips whitespace (space, tab, CR, LF) and line comments ({@code # ...}). */ + protected void ws() throws IOException { + while (true) { + int c = peek(); + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + take(); + } else if (c == '#') { + while (peek() != -1 && !isLineEnd(peek())) take(); + } else { + break; + } + } + } + + private int scalar() throws IOException { + int first = peek(); + if (first == -1) throw error("Unexpected end of input"); + if (Character.isLowSurrogate((char) first)) throw error("Unpaired low surrogate"); + take(); + if (!Character.isHighSurrogate((char) first)) return first; + int second = peek(); + if (second == -1 || !Character.isLowSurrogate((char) second)) throw error("Unpaired high surrogate"); + take(); + return Character.toCodePoint((char) first, (char) second); + } + + protected int peek() throws IOException { + if (pushback != NO_PUSHBACK) return pushback; + if (bufOffset == bufLimit && !eof) { + checkInterrupted(); + int count = reader.read(buffer, 0, buffer.length); + if (count == 0) throw new IOException("Reader returned zero characters for a nonempty buffer"); + eof = count < 0; + bufOffset = 0; + bufLimit = Math.max(count, 0); + } + return bufOffset == bufLimit ? -1 : buffer[bufOffset]; + } + + protected int take() throws IOException { + int c; + if (pushback != NO_PUSHBACK) { + c = pushback; + pushback = NO_PUSHBACK; + } else { + c = peek(); + if (c == -1) return -1; + bufOffset++; + } + updatePosition(c); + return c; + } + + /** + * Pushes back a {@code '.'} character. Used exclusively for dot-in-local-name + * and decimal-dot disambiguation. Only one character can be pushed back at a time. + */ + protected void pushBack() { + pushback = '.'; + if (column > 1) column--; + } + + protected void expect(int expected) throws IOException { + int actual = peek(); + if (actual != expected) { + throw error("Expected '" + (char) expected + "' but got " + + (actual == -1 ? "end of input" : "'" + (char) actual + "'")); + } + take(); + } + + private void updatePosition(int c) { + if (c == '\r') { + line++; column = 1; + } else if (c == '\n') { + if (!afterCr) { line++; column = 1; } + } else { + column++; + } + afterCr = (c == '\r'); + } + + protected void checkInterrupted() { + if (Thread.currentThread().isInterrupted()) throw error("Parsing interrupted"); + } + + private static boolean forbiddenIriChar(int c) { + return c <= 0x20 || "<>\"{}|^`\\".indexOf(c) >= 0; + } + + private static boolean isAbsoluteIri(String text) { + if (text == null || text.isEmpty() || !isAsciiAlpha(text.charAt(0))) return false; + int i = 1; + int n = text.length(); + while (i < n && isSchemeChar(text.charAt(i))) i++; + return i < n && text.charAt(i) == ':'; + } + + protected static boolean isAsciiAlpha(int c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isSchemeChar(char c) { + return isAsciiAlpha(c) || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.'; + } + + protected static boolean letter(int c) { return isAsciiAlpha(c); } + protected static boolean digit(int c) { return c >= '0' && c <= '9'; } + + private static boolean isLineEnd(int c) { return c == '\r' || c == '\n'; } + private static boolean isWhitespace(int c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } + + protected static boolean nameStart(int c) { return c == '_' || letter(c) || unicodeNameStart(c); } + + protected static boolean unicodeNameStart(int c) { + return between(c, 0xC0, 0xD6) || between(c, 0xD8, 0xF6) + || between(c, 0xF8, 0x2FF) || between(c, 0x370, 0x37D) + || between(c, 0x37F, 0x1FFF) || between(c, 0x200C, 0x200D) + || between(c, 0x2070, 0x218F) || between(c, 0x2C00, 0x2FEF) + || between(c, 0x3001, 0xD7FF) || between(c, 0xF900, 0xFDCF) + || between(c, 0xFDF0, 0xFFFD) || between(c, 0x10000, 0xEFFFF); + } + + protected static boolean nameCharacter(int c) { + return nameStart(c) || digit(c) || c == '-' || c == 0xB7 + || between(c, 0x300, 0x36F) || between(c, 0x203F, 0x2040); + } + + private static boolean localNameChar(int c) { + return nameCharacter(c) || c == ':'; + } + + private static boolean isLocalNameEscapeChar(int c) { + return "_~.-!$&'()*+,;=/?#@%".indexOf(c) >= 0; + } + + private static int hexValue(int c) { + if (digit(c)) return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; + } + + private static boolean between(int c, int lo, int hi) { return c >= lo && c <= hi; } + + + protected ParsingException error(String message) { + return new ParsingException(location(message)); + } + + protected String location(String message) { + return message + " at line " + line + ", column " + column; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGListener.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGListener.java deleted file mode 100644 index a33fd9ea2..000000000 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGListener.java +++ /dev/null @@ -1,568 +0,0 @@ -package fr.inria.corese.core.next.data.impl.io.parser.trig; - -import fr.inria.corese.core.next.data.api.term.*; -import fr.inria.corese.core.next.data.api.model.*; -import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.option.IOOptions; -import fr.inria.corese.core.next.data.api.io.option.BaseIRIOptions; -import fr.inria.corese.core.next.data.api.vocabulary.RDF; -import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.data.impl.io.parser.support.AbstractTurtleTriGListener; -import fr.inria.corese.core.next.data.impl.io.parser.support.ParserConstants; -import fr.inria.corese.core.next.generated.antlr.TriGBaseListener; -import fr.inria.corese.core.next.generated.antlr.TriGParser; - -import java.util.List; - -/** - * ANTLR listener for parsing TriG documents into RDF datasets. - * and adds support for named graphs as specified in the TriG format. - * - - */ -public class TriGListener extends TriGBaseListener { - - private final TriGListenerDelegate delegate; - private boolean insideGraphBlock = false; - - /** - * Constructs a TriG listener with explicit base URI. - * - * @param model RDF model to populate with parsed quads - * @param factory factory for creating RDF terms - * @param options I/O options (unused in this constructor) - * @param baseURI base URI for resolving relative references - */ - public TriGListener(Model model, ValueFactory factory, IOOptions options, String baseURI) { - this.delegate = new TriGListenerDelegate(model, factory, baseURI); - } - - /** - * Constructs a TriG listener extracting base URI from options. - * - * @param model RDF model to populate with parsed quads - * @param factory factory for creating RDF terms - * @param options I/O options potentially containing base URI - */ - public TriGListener(Model model, ValueFactory factory, IOOptions options) { - String baseURI; - if (options instanceof BaseIRIOptions baseIRIOptions) { - baseURI = baseIRIOptions.getBaseIRI() != null ? baseIRIOptions.getBaseIRI() : ParserConstants.EMPTY_STRING; - } else { - baseURI = ParserConstants.EMPTY_STRING; - } - this.delegate = new TriGListenerDelegate(model, factory, baseURI); - } - - /** - * Handles directive by updating the base URI for relative IRI resolution. - * - * @param ctx base directive context - */ - @Override - public void exitBase(TriGParser.BaseContext ctx) { - String newBase = delegate.extractAndUnescapeIRI(ctx.IRIREF().getText()); - delegate.updateBaseURI(newBase); - } - - /** - * Handles {@code @prefix} directive by registering a namespace prefix. - * - * @param ctx prefix directive context - */ - @Override - public void exitPrefixID(TriGParser.PrefixIDContext ctx) { - String prefix = ctx.PNAME_NS().getText(); - prefix = prefix.substring(0, prefix.length() - 1); - String iri = delegate.extractAndUnescapeIRI(ctx.IRIREF().getText()); - delegate.registerPrefix(prefix, iri); - } - - /** - * Handles SPARQL-style {@code BASE} directive by updating the base URI. - * - * @param ctx SPARQL base directive context - */ - @Override - public void exitSparqlBase(TriGParser.SparqlBaseContext ctx) { - String newBase = delegate.extractAndUnescapeIRI(ctx.IRIREF().getText()); - delegate.updateBaseURI(newBase); - } - - /** - * Handles SPARQL-style {@code PREFIX} directive by registering a namespace prefix. - * - * @param ctx SPARQL prefix directive context - */ - @Override - public void exitSparqlPrefix(TriGParser.SparqlPrefixContext ctx) { - String prefix = ctx.PNAME_NS().getText(); - prefix = prefix.substring(0, prefix.length() - 1); - String iri = delegate.extractAndUnescapeIRI(ctx.IRIREF().getText()); - delegate.registerPrefix(prefix, iri); - } - - /** - * Handles entry into a graph block, setting the current graph context. - * If a graph label is present, it becomes the target graph for subsequent statements. - * - * @param ctx block context - */ - @Override - public void enterBlock(TriGParser.BlockContext ctx) { - if (ctx.Graph_w() != null && ctx.labelOrSubject() != null) { - delegate.setCurrentGraph(extractLabelOrSubject(ctx.labelOrSubject())); - } else { - delegate.setCurrentGraph(null); - } - } - - /** - * Handles exit from a graph block, clearing the current graph context. - * - * @param ctx block context - */ - @Override - public void exitBlock(TriGParser.BlockContext ctx) { - delegate.setCurrentGraph(null); - } - - /** - * Marks entry into a wrapped graph block - * Used for validating standalone collection syntax. - * - * @param ctx wrapped graph context - */ - @Override - public void enterWrappedGraph(TriGParser.WrappedGraphContext ctx) { - insideGraphBlock = true; - } - - /** - * Marks exit from a wrapped graph block. - * - * @param ctx wrapped graph context - */ - @Override - public void exitWrappedGraph(TriGParser.WrappedGraphContext ctx) { - insideGraphBlock = false; - } - - /** - * Handles triples or graph declarations (disambiguated by following syntax). - * Either processes triples with an explicit subject or sets up a named graph. - * - * @param ctx triples or graph context - */ - @Override - public void enterTriplesOrGraph(TriGParser.TriplesOrGraphContext ctx) { - if (ctx.labelOrSubject() != null) { - Resource resource = extractLabelOrSubject(ctx.labelOrSubject()); - if (ctx.predicateObjectList() != null) { - delegate.setCurrentSubject(resource); - processPredicateObjectList(ctx.predicateObjectList()); - } else if (ctx.wrappedGraph() != null) { - delegate.setCurrentGraph(resource); - } - } - } - - /** - * Handles standard triple declarations starting with a subject or blank node property list. - * Subject context is saved and restored to handle nested structures. - * - * @param ctx triples context - */ - @Override - public void enterTriples(TriGParser.TriplesContext ctx) { - Resource savedSubject = delegate.getCurrentSubject(); - try { - if (ctx.subject() != null) { - delegate.setCurrentSubject(extractSubject(ctx.subject())); - } else if (ctx.blankNodePropertyList() != null) { - delegate.setCurrentSubject(processBlankNodePropertyList(ctx.blankNodePropertyList())); - } - - if (ctx.predicateObjectList() != null) { - processPredicateObjectList(ctx.predicateObjectList()); - } - } finally { - delegate.setCurrentSubject(savedSubject); - } - } - - /** - * Handles alternative triple forms starting with blank node property lists or collections. - * Validates that standalone collections only appear within graph blocks. - * Subject context is saved and restored to handle nested structures. - * - * @param ctx triples2 context - */ - @Override - public void enterTriples2(TriGParser.Triples2Context ctx) { - Resource savedSubject = delegate.getCurrentSubject(); - - if (ctx.collection() != null && ctx.predicateObjectList() == null) { - validateStandaloneCollection(ctx); - } - - try { - if (ctx.blankNodePropertyList() != null) { - delegate.setCurrentSubject(processBlankNodePropertyList(ctx.blankNodePropertyList())); - } else if (ctx.collection() != null) { - delegate.setCurrentSubject(processCollection(ctx.collection())); - } - - if (ctx.predicateObjectList() != null) { - processPredicateObjectList(ctx.predicateObjectList()); - } - } finally { - delegate.setCurrentSubject(savedSubject); - } - } - - /** - * Validates that standalone collections only appear within graph blocks. - * TriG syntax requires collections without predicates to be inside braces. - * - * @param ctx triples2 context containing a potential standalone collection - * @throws ParsingException if a standalone collection appears outside graph blocks - */ - private void validateStandaloneCollection(TriGParser.Triples2Context ctx) { - if (!insideGraphBlock) { - List objects = ctx.collection().object(); - if (objects.isEmpty()) { - throw new ParsingException("Free-standing list of zero-elements outside {} : bad syntax"); - } else { - throw new ParsingException("Free-standing list outside {} : bad syntax"); - } - } - } - - /** - * Processes a blank node property list - * Creates a fresh blank node and parses its property list. - * - * @param ctx blank node property list context - * @return created blank node resource - */ - private Resource processBlankNodePropertyList(TriGParser.BlankNodePropertyListContext ctx) { - Resource bnode = delegate.createBNode(); - Resource savedSubject = delegate.getCurrentSubject(); - IRI savedPredicate = delegate.getCurrentPredicate(); - - try { - delegate.setCurrentSubject(bnode); - if (ctx.predicateObjectList() != null) { - processPredicateObjectList(ctx.predicateObjectList()); - } - } finally { - delegate.setCurrentSubject(savedSubject); - delegate.setCurrentPredicate(savedPredicate); - } - - return bnode; - } - - /** - * Processes an RDF collection - * Constructs the linked list structure - * - * @param ctx collection context - * @return head of the list (blank node) or {@code rdf:nil} for empty lists - */ - private Resource processCollection(TriGParser.CollectionContext ctx) { - List objects = ctx.object(); - - if (objects.isEmpty()) { - return delegate.createIRI(RDF.nil.getIRI().stringValue()); - } - - Resource head = delegate.createBNode(); - Resource current = head; - Resource savedSubject = delegate.getCurrentSubject(); - IRI savedPredicate = delegate.getCurrentPredicate(); - - try { - IRI firstPredicate = delegate.createIRI(RDF.first.getIRI().stringValue()); - IRI restPredicate = delegate.createIRI(RDF.rest.getIRI().stringValue()); - Value nilValue = delegate.createIRI(RDF.nil.getIRI().stringValue()); - - for (int i = 0; i < objects.size(); i++) { - Value object = extractObject(objects.get(i)); - delegate.addStatement(current, firstPredicate, object); - - if (i == objects.size() - 1) { - delegate.addStatement(current, restPredicate, nilValue); - } else { - Resource next = delegate.createBNode(); - delegate.addStatement(current, restPredicate, next); - current = next; - } - } - } finally { - delegate.setCurrentSubject(savedSubject); - delegate.setCurrentPredicate(savedPredicate); - } - - return head; - } - - /** - * Processes a predicate-object list, generating statements for each predicate-object pair. - * - * @param ctx predicate-object list context - */ - private void processPredicateObjectList(TriGParser.PredicateObjectListContext ctx) { - for (int i = 0; i < ctx.verb().size(); i++) { - TriGParser.VerbContext verb = ctx.verb(i); - TriGParser.ObjectListContext objectList = ctx.objectList(i); - - IRI savedPredicate = delegate.getCurrentPredicate(); - try { - delegate.setCurrentPredicate(extractVerb(verb)); - - if (objectList != null) { - for (TriGParser.ObjectContext objectCtx : objectList.object()) { - Value object = extractObject(objectCtx); - delegate.addStatement(object); - } - } - } finally { - delegate.setCurrentPredicate(savedPredicate); - } - } - } - - /** - * Extracts an RDF value from an object context. - * - * @param ctx object context - * @return extracted RDF value (IRI, blank node, or literal) - * @throws ParsingException if the object type is unsupported - */ - private Value extractObject(TriGParser.ObjectContext ctx) { - if (ctx.iri() != null) { - return delegate.createIRI(delegate.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 ParsingException("Unsupported object: " + ctx.getText()); - } - - /** - * Extracts a blank node resource from a blank node context. - * - * @param ctx blank node context - * @return blank node resource or collection head - * @throws ParsingException if the blank node format is unsupported - */ - private Resource extractBlank(TriGParser.BlankContext ctx) { - TriGParser.BlankNodeContext node = ctx.blankNode(); - if (node != null) { - if (node.BLANK_NODE_LABEL() != null) { - return delegate.createBNode(node.BLANK_NODE_LABEL().getText().substring(2)); - } - if (node.ANON() != null) { - return delegate.createBNode(); - } - } - - TriGParser.CollectionContext collection = ctx.collection(); - if (collection != null) { - return processCollection(collection); - } - - throw new ParsingException("Unsupported blank node: " + ctx.getText()); - } - - /** - * Extracts a subject resource from a subject context. - * - * @param ctx subject context - * @return subject resource (IRI, blank node, or collection head) - * @throws ParsingException if the subject type is unsupported - */ - private Resource extractSubject(TriGParser.SubjectContext ctx) { - if (ctx.iri() != null) { - return delegate.createIRI(delegate.resolveIRI(ctx.iri().getText())); - } - if (ctx.blank() != null) { - if (ctx.blank().blankNode() != null) { - TriGParser.BlankNodeContext node = ctx.blank().blankNode(); - if (node.BLANK_NODE_LABEL() != null) { - return delegate.createBNode(node.BLANK_NODE_LABEL().getText().substring(2)); - } - if (node.ANON() != null) { - return delegate.createBNode(); - } - } else if (ctx.blank().collection() != null) { - return processCollection(ctx.blank().collection()); - } - } - throw new ParsingException("Unsupported subject: " + ctx.getText()); - } - - /** - * Extracts a graph label or subject resource. - * - * @param ctx label or subject context - * @return resource serving as graph name or statement subject - * @throws ParsingException if IRI resolution fails or type is unsupported - */ - private Resource extractLabelOrSubject(TriGParser.LabelOrSubjectContext ctx) { - if (ctx.iri() != null) { - String iriText = ctx.iri().getText(); - try { - return delegate.createIRI(delegate.resolveIRI(iriText)); - } catch (Exception e) { - throw new ParsingException("Failed to resolve IRI: " + iriText, e); - } - } - if (ctx.blankNode() != null) { - if (ctx.blankNode().BLANK_NODE_LABEL() != null) { - return delegate.createBNode(ctx.blankNode().BLANK_NODE_LABEL().getText().substring(2)); - } - if (ctx.blankNode().ANON() != null) { - return delegate.createBNode(); - } - } - throw new ParsingException("Unsupported label or subject: " + ctx.getText()); - } - - /** - * Extracts a predicate IRI from a verb context. - * Handles the special case of {@code a} as shorthand for {@code rdf:type}. - * - * @param ctx verb context - * @return predicate IRI - */ - private IRI extractVerb(TriGParser.VerbContext ctx) { - String verbText = ctx.getText(); - if (verbText.equals(ParserConstants.RDF_TYPE_SHORTCUT)) { - return delegate.createIRI(RDF.type.getIRI().stringValue()); - } - return delegate.createIRI(delegate.resolveIRI(verbText)); - } - - /** - * Extracts an RDF literal from a literal context. - * Handles plain, language-tagged, typed, boolean, and numeric literals. - * - * @param ctx literal context - * @return RDF literal value - * @throws ParsingException if the literal type is unsupported - */ - private Literal extractLiteral(TriGParser.LiteralContext ctx) { - if (ctx.rDFLiteral() != null) { - String label = delegate.unescapeString(ctx.rDFLiteral().string().getText()); - if (ctx.rDFLiteral().LANGTAG() != null) { - return delegate.createLiteral(label, ctx.rDFLiteral().LANGTAG().getText().substring(1), null); - } - if (ctx.rDFLiteral().iri() != null) { - return delegate.createLiteral(label, null, delegate.resolveIRI(ctx.rDFLiteral().iri().getText())); - } - return delegate.createLiteral(label, null, null); - } - if (ctx.BooleanLiteral() != null) { - return delegate.createBooleanLiteral(ctx.BooleanLiteral().getText()); - } - if (ctx.numericLiteral() != null) { - String numericText = ctx.numericLiteral().getText(); - - if (ctx.numericLiteral().DOUBLE() != null) { - return delegate.createNumericLiteral(numericText, AbstractTurtleTriGListener.NumericType.DOUBLE); - } else if (ctx.numericLiteral().DECIMAL() != null) { - return delegate.createNumericLiteral(numericText, AbstractTurtleTriGListener.NumericType.DECIMAL); - } else { - return delegate.createNumericLiteral(numericText, AbstractTurtleTriGListener.NumericType.INTEGER); - } - } - throw new ParsingException("Unsupported literal: " + ctx.getText()); - } - - /** - * Delegate extending {@link AbstractTurtleTriGListener} with TriG-specific named graph support. - * Overrides statement addition to handle quads (subject, predicate, object, graph). - */ - private static class TriGListenerDelegate extends AbstractTurtleTriGListener { - private Resource currentGraph; - - public TriGListenerDelegate(Model model, ValueFactory factory, String baseURI) { - super(model, factory, baseURI); - } - - public Resource getCurrentSubject() { - return currentSubject; - } - - public void setCurrentSubject(Resource subject) { - this.currentSubject = subject; - } - - public IRI getCurrentPredicate() { - return currentPredicate; - } - - public void setCurrentPredicate(IRI predicate) { - this.currentPredicate = predicate; - } - - - public void setCurrentGraph(Resource graph) { - this.currentGraph = graph; - } - - public void addStatement(Value object) { - safeAddStatement(currentSubject, currentPredicate, object); - } - - public void addStatement(Resource subject, IRI predicate, Value object) { - safeAddStatement(subject, predicate, object); - } - - public Resource createBNode() { - return factory.createBNode(); - } - - public Resource createBNode(String id) { - return scopedBlankNode(id); - } - - public IRI createIRI(String iri) { - return factory.createIRI(iri); - } - - /** - * Adds a quad to the model with fallback to default graph on failure. - * - * @param subject statement subject - * @param predicate statement predicate - * @param object statement object - * @throws ParsingException if statement cannot be added to any graph - */ - @Override - public void safeAddStatement(Resource subject, IRI predicate, Value object) { - try { - model.add(subject, predicate, object, currentGraph); - } catch (Exception e) { - if (currentGraph != null) { - try { - model.add(subject, predicate, object, (Resource) null); - } catch (Exception e2) { - throw new ParsingException("Failed to add statement: " + e.getMessage(), e); - } - } else { - throw new ParsingException("Failed to add statement: " + e.getMessage(), e); - } - } - } - } -} diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGParser.java deleted file mode 100644 index 9eb62e915..000000000 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGParser.java +++ /dev/null @@ -1,161 +0,0 @@ -package fr.inria.corese.core.next.data.impl.io.parser.trig; - -import fr.inria.corese.core.next.data.api.model.Model; -import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.api.io.format.RDFFormat; -import fr.inria.corese.core.next.data.spi.io.parser.AbstractRDFParser; -import fr.inria.corese.core.next.data.api.io.option.RDFParsingOptions; -import fr.inria.corese.core.next.data.api.exception.ParsingException; -import fr.inria.corese.core.next.generated.antlr.TriGLexer; -import org.antlr.v4.runtime.BaseErrorListener; -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.Recognizer; -import org.antlr.v4.runtime.Token; -import org.antlr.v4.runtime.tree.ParseTree; -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; -import java.util.ArrayList; -import java.util.List; - -/** - * A parser for the TriG format based on ANTLR4. - * It uses an ANTLR grammar to tokenize and parse TriG documents, - * then a listener to build the RDF model. - */ -public class TriGParser extends AbstractRDFParser { - - /** - * Constructs a TriGParser with default options. - * - * @param model The RDF model to be populated. - * @param factory The value factory for creating RDF resources. - */ - public TriGParser(Model model, ValueFactory factory) { - this(model, factory, new TriGParserOptions.Builder().build()); - } - - /** - * Constructs a TriGParser with configuration options. - * - * @param model The RDF model to be populated. - * @param factory The value factory for creating RDF resources. - * @param config Configuration options for parsing. - */ - public TriGParser(Model model, ValueFactory factory, RDFParsingOptions config) { - super(model, factory, config); - } - - @Override - public RDFFormat getRDFFormat() { - return RDFFormat.TRIG; - } - - @Override - public void parse(InputStream in, String baseURI) throws ParsingException { - parse(new InputStreamReader(in, StandardCharsets.UTF_8), baseURI); - } - - /** - * Parses TriG data from a {@link Reader} using ANTLR4. - * - * @param reader The {@link Reader} to read the RDF data. - * @param baseURI The base URI. - * @throws ParsingException if a parsing or I/O error occurs. - */ - @Override - public void parse(Reader reader, String baseURI) throws ParsingException { - try { - CharStream charStream = CharStreams.fromReader(reader); - TriGLexer triGLexer = new TriGLexer(charStream); - - TrigErrorListener trigErrorListener = new TrigErrorListener(); - triGLexer.removeErrorListeners(); - triGLexer.addErrorListener(trigErrorListener); - - CommonTokenStream tokens = new CommonTokenStream(triGLexer); - fr.inria.corese.core.next.generated.antlr.TriGParser triGParser = new fr.inria.corese.core.next.generated.antlr.TriGParser(tokens); - - - triGParser.removeErrorListeners(); - triGParser.addErrorListener(trigErrorListener); - - ParseTreeWalker walker = new ParseTreeWalker(); - ParseTree tree = triGParser.trigDoc(); - - if (trigErrorListener.hasErrors()) { - throw new ParsingException("Syntax error in TriG document: " + trigErrorListener.getErrorMessage()); - } - RDFParsingOptions optionsWithBaseURI = new TriGParserOptions.Builder() - .baseIRI(baseURI) - .build(); - TriGListener listener = new TriGListener(getModel(), getValueFactory(), optionsWithBaseURI); - walker.walk(listener, tree); - - } catch (ParsingException e) { - throw e; - } catch (IOException e) { - throw new ParsingException("Failed to parse TriG RDF: " + e.getMessage(), e); - } catch (Exception e) { - throw new ParsingException("Unexpected error during TriG parsing: " + e.getMessage(), e); - } - } - - - /** - * A custom error listener to collect errors from the lexer and parser. - */ - private static class TrigErrorListener extends BaseErrorListener { - private final List errors = new ArrayList<>(); - - /** - * Records syntax errors generated by ANTLR. - * - * @param recognizer The recognizer that detected the error. - * @param offendingSymbol The symbol that caused the error. - * @param line The line number where the error occurred. - * @param charPositionInLine The character position on the line. - * @param msg The error message. - * @param e The recognition exception. - */ - @Override - public void syntaxError(Recognizer recognizer, Object offendingSymbol, - int line, int charPositionInLine, String msg, RecognitionException e) { - if (msg != null && (msg.contains("token recognition error") || msg.contains("mismatched input")) - && offendingSymbol instanceof Token token) { - String tokenText = token.getText(); - if (msg.contains("token recognition error") && tokenText != null && tokenText.contains("\"")) { - msg = "Invalid string literal - possibly unterminated or contains invalid escape sequence: " + msg; - } - } - - String error = "line " + line + ":" + charPositionInLine + " " + msg; - errors.add(error); - } - - /** - * Checks if parsing errors have been found. - * - * @return `true` if the error list is not empty, otherwise `false`. - */ - public boolean hasErrors() { - return !errors.isEmpty(); - } - - /** - * Returns a formatted error message containing all found errors. - * - * @return A {@link String} containing the error messages. - */ - public String getErrorMessage() { - return String.join("; ", errors); - } - } -} diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGScanner.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGScanner.java new file mode 100644 index 000000000..f82cb37c2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/trig/TriGScanner.java @@ -0,0 +1,288 @@ +package fr.inria.corese.core.next.data.impl.io.parser.trig; + +import fr.inria.corese.core.next.data.api.exception.ParsingException; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; +import fr.inria.corese.core.next.data.api.io.format.RDFFormat; +import fr.inria.corese.core.next.data.api.io.option.BaseIRIOptions; +import fr.inria.corese.core.next.data.api.io.option.RDFParsingOptions; +import fr.inria.corese.core.next.data.api.model.Model; +import fr.inria.corese.core.next.data.api.term.BNode; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Resource; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.data.impl.io.parser.support.TurtleTriGDocument; +import fr.inria.corese.core.next.data.spi.io.parser.AbstractRDFParser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * Streaming RDF 1.1 TriG parser without tokens, parse trees, or ANTLR. + * + *

Extends the Turtle FSM with named-graph block support: + * {@code GRAPH

+ * + *

All Turtle features (prefixes, base, blank-node property lists, collections, + * long literals, numeric and boolean literals) are available both at the top level + * and inside graph blocks.

+ */ +public final class TriGScanner extends AbstractRDFParser { + + public TriGScanner(Model model, ValueFactory factory) { + this(model, factory, new TriGParserOptions.Builder().build()); + } + + public TriGScanner(Model model, ValueFactory factory, RDFParsingOptions config) { + super(model, factory, config); + } + + @Override + public RDFFormat getRDFFormat() { + return RDFFormat.TRIG; + } + + @Override + public void parse(InputStream input) throws ParsingException { + parse(input, null); + } + + @Override + public void parse(InputStream input, String baseURI) throws ParsingException { + Objects.requireNonNull(input, "input"); + var decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + parse(new InputStreamReader(input, decoder), baseURI); + } + + @Override + public void parse(Reader reader, String baseURI) throws ParsingException { + String base = baseURI; + if (base == null && getConfig() instanceof BaseIRIOptions o) { + base = o.getBaseIRI(); + } + new TriGDocument( + Objects.requireNonNull(reader, "reader"), + getModel(), getValueFactory(), base + ).parse(); + } + + + private static final class TriGDocument extends TurtleTriGDocument { + + private static final String EXPECTED_GRAPH_TRIPLE_SEPARATOR = + "Expected '.' between triples in graph block, got: '"; + + /** Active named graph; {@code null} means the default graph. */ + private Resource currentGraph; + + TriGDocument(Reader reader, Model model, ValueFactory factory, String baseURI) { + super(reader, model, factory, baseURI); + } + + // ── Route statements to the current named graph ──────────────────── + + @Override + protected void emitTriple(Resource s, IRI p, Value o) { + model.add(s, p, o, currentGraph); + } + + // ── TriG document loop ───────────────────────────────────────────── + + @Override + protected void parseDocument() throws IOException { + while (true) { + ws(); + if (peek() == -1) break; + checkInterrupted(); + trigTopLevel(); + } + } + + /** + * Dispatches one top-level TriG item: directive, anonymous graph block, + * {@code GRAPH} block, or triple/graph-label statement. + */ + private void trigTopLevel() throws IOException { + int c = peek(); + switch (c) { + case '@' -> atDirective(); + case '{' -> wrappedGraph(null); + case '[' -> blankNodeTopLevel(); + case '(' -> collectionTopLevel(); + case '<' -> resourceTopLevel(intern(iriRefContent())); + case '_' -> resourceTopLevel(blankNodeLabel()); + case ':' -> { + take(); + resourceTopLevel(intern(lookupPrefix("") + readLocalPart())); + } + default -> namedTopLevel(c); + } + } + + private void blankNodeTopLevel() throws IOException { + expect('['); + ws(); + BNode bnode = factory.createBNode(); + boolean isEmpty = (peek() == ']'); + if (!isEmpty) { + predicateObjectList(bnode); + ws(); + } + expect(']'); + ws(); + if (isEmpty && peek() == '{') { + wrappedGraph(bnode); + } else { + if (!isEmpty && peek() == '{') { + throw error("A blank node property list cannot be used as a named graph label"); + } + blankNodeTriples(bnode); + } + } + + private void blankNodeTriples(BNode bnode) throws IOException { + if (peek() != '.' && peek() != -1) { + predicateObjectList(bnode); + ws(); + } + expect('.'); + } + + private void collectionTopLevel() throws IOException { + Resource subj = collection(); + ws(); + predicateObjectList(subj); + ws(); + expect('.'); + } + + private void resourceTopLevel(Resource res) throws IOException { + ws(); + labelOrTriples(res); + } + + private void namedTopLevel(int c) throws IOException { + if (isAsciiAlpha(c) || unicodeNameStart(c)) { + String name = readName(); + if (peek() == ':') { + take(); + Resource res = intern(lookupPrefix(name) + readLocalPart()); + ws(); + labelOrTriples(res); + } else if (name.equalsIgnoreCase("GRAPH")) { + ws(); + trigGraphBlock(); + } else if (name.equalsIgnoreCase("PREFIX")) { + ws(); + sparqlPrefixDirective(); + } else if (name.equalsIgnoreCase("BASE")) { + ws(); + sparqlBaseDirective(); + } else { + throw error("Unexpected keyword at TriG top level: '" + name + "'"); + } + return; + } + + throw error("Unexpected character in TriG document: '" + (char) c + "'"); + } + + /** + * After reading a resource at top level, determines whether it is a + * named-graph label (followed by {@code {}) or a triple subject. + */ + private void labelOrTriples(Resource res) throws IOException { + if (peek() == '{') { + wrappedGraph(res); + } else { + predicateObjectList(res); + ws(); + expect('.'); + } + } + + /** Handles {@code GRAPH