diff --git a/src/main/antlr/NQuads.g4 b/src/main/antlr/NQuads.g4 new file mode 100644 index 000000000..ad04f65ff --- /dev/null +++ b/src/main/antlr/NQuads.g4 @@ -0,0 +1,111 @@ +grammar NQuads; + +nquadsDoc + : statement? (EOL* statement)* EOL* + ; + +statement + : subject predicate object graphLabel? '.' + ; + +subject + : IRIREF + | BLANK_NODE_LABEL + ; + +predicate + : IRIREF + ; + +object + : IRIREF + | BLANK_NODE_LABEL + | literal + ; + +graphLabel + : IRIREF + | BLANK_NODE_LABEL + ; + +literal + : STRING_LITERAL_QUOTE ('^^' IRIREF | LANGTAG)? + ; + +LANGTAG + : '@' [a-zA-Z]+ ('-' [a-zA-Z0-9]+)* + ; + +EOL + : [\u000D\u000A]+ + ; + +IRIREF +// '<' ([^#x00-#x20<>"{}|^`\] | UCHAR)* '>' + : '<' [a-zA-Z0-9-]+':' ((~( [\u0000-\u0020] | '<' | '>' | '"' | '{'| '}' | '|'| '^'| '`' | '\\' )) | UCHAR)* '>' + ; + +STRING_LITERAL_QUOTE + : '"' ( ~( [\u0022] | [\u005C] | [\u000A] | [\u000D] ) | ECHAR | UCHAR )* '"' + ; + +BLANK_NODE_LABEL +// '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)? + : '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)? + ; + +UCHAR + : '\\u' HEX HEX HEX HEX + | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX + ; + +HEX + : [0-9] + | [A-F] + | [a-f] + ; + +ECHAR + : '\\' [tbnrf"'\\] + ; + +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' +// | '\u10000' .. '\uEFFFF' + ; + +PN_CHARS_U +// PN_CHARS_BASE | '_' | ':' + : PN_CHARS_BASE + | '_' +// | ':' + ; + +PN_CHARS + : PN_CHARS_U + | '-' + | [0-9] + | [\u00B7] + | [\u0300-\u036F] + | [\u203F-\u2040] + ; + +LC + : '#' ~[\r\n]* -> channel(HIDDEN) + ; + +WS + : ([\t\r\n\u000C] | ' ')+ -> skip + ; \ No newline at end of file diff --git a/src/main/antlr/NTriples.g4 b/src/main/antlr/NTriples.g4 new file mode 100644 index 000000000..783d53ce0 --- /dev/null +++ b/src/main/antlr/NTriples.g4 @@ -0,0 +1,113 @@ + +// $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 NTriples; + + +ntriplesDoc +// triple? (EOL triple)* EOL? + : triple? (EOL* triple)* EOL* + ; + +triple + : subject predicate object '.' + ; + +subject + : IRIREF + | BLANK_NODE_LABEL + ; + +predicate + : IRIREF + ; + +object + : IRIREF + | BLANK_NODE_LABEL + | literal + ; + +literal + : STRING_LITERAL_QUOTE ('^^' IRIREF | LANGTAG)? + ; + +LANGTAG + : '@' [a-zA-Z]+ ('-' [a-zA-Z0-9]+)* + ; + +EOL + : [\u000D\u000A]+ + ; + +IRIREF +// '<' ([^#x00-#x20<>"{}|^`\] | UCHAR)* '>' + : '<' [a-zA-Z0-9-]+':' ((~( [\u0000-\u0020] | '<' | '>' | '"' | '{'| '}' | '|'| '^'| '`' | '\\' )) | UCHAR)* '>' + ; + +STRING_LITERAL_QUOTE + : '"' ( ~( [\u0022] | [\u005C] | [\u000A] | [\u000D] ) | ECHAR | UCHAR )* '"' + ; + +BLANK_NODE_LABEL +// '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)? + : '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)? + ; + +UCHAR + : '\\u' HEX HEX HEX HEX + | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX + ; + +HEX + : [0-9] + | [A-F] + | [a-f] + ; + +ECHAR + : '\\' [tbnrf"'\\] + ; + +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' +// | '\u10000' .. '\uEFFFF' + ; + +PN_CHARS_U +// PN_CHARS_BASE | '_' | ':' + : PN_CHARS_BASE + | '_' +// | ':' + ; + +PN_CHARS + : PN_CHARS_U + | '-' + | [0-9] + | [\u00B7] + | [\u0300-\u036F] + | [\u203F-\u2040] + ; + +LC + : '#' ~[\r\n]* -> channel(HIDDEN) + ; + +WS + : ([\t\r\n\u000C] | ' ')+ -> skip + ; \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/literal/AbstractDuration.java b/src/main/java/fr/inria/corese/core/next/api/base/model/literal/AbstractDuration.java index 3628af23f..f5475f737 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/literal/AbstractDuration.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/literal/AbstractDuration.java @@ -1,12 +1,16 @@ package fr.inria.corese.core.next.api.base.model.literal; -import fr.inria.corese.core.next.api.literal.CoreDatatype; -import fr.inria.corese.core.next.impl.common.literal.XSD; - import java.time.DateTimeException; import java.time.temporal.TemporalAmount; import java.time.temporal.TemporalUnit; -import java.util.*; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import fr.inria.corese.core.next.api.literal.CoreDatatype; +import fr.inria.corese.core.next.impl.common.literal.XSD; /** * Abstract class representing a duration literal in RDF. 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 a331284b5..8b43d930a 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 @@ -7,6 +7,8 @@ import fr.inria.corese.core.next.api.io.parser.RDFParser; import fr.inria.corese.core.next.api.io.parser.RDFParserOptions; 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.turtle.ANTLRTurtleParser; /** @@ -34,10 +36,14 @@ public ParserFactory() { */ @Override public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory factory, RDFParserOptions config) { - if(format == RDFFormat.JSONLD) { + if (format == RDFFormat.JSONLD) { return new JSONLDParser(model, factory, config); - } else if(format == RDFFormat.TURTLE) { + } else if (format == RDFFormat.TURTLE) { return new ANTLRTurtleParser(model, factory, config); + } else if (format == RDFFormat.NTRIPLES) { + return new ANTLRNTriplesParser(model, factory, config); + } else if (format == RDFFormat.NQUADS) { + return new ANTLRNQuadsParser(model, factory, config); } throw new IllegalArgumentException("Unsupported format: " + format); } @@ -51,10 +57,14 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac */ @Override public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory factory) { - if(format == RDFFormat.JSONLD) { + if (format == RDFFormat.JSONLD) { return new JSONLDParser(model, factory); - } else if(format == RDFFormat.TURTLE) { + } else if (format == RDFFormat.TURTLE) { return new ANTLRTurtleParser(model, factory); + } else if (format == RDFFormat.NTRIPLES) { + return new ANTLRNTriplesParser(model, factory); + } else if (format == RDFFormat.NQUADS) { + return new ANTLRNQuadsParser(model, factory); } throw new IllegalArgumentException("Unsupported format: " + format); } diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParser.java new file mode 100644 index 000000000..742f2aa27 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParser.java @@ -0,0 +1,101 @@ +package fr.inria.corese.core.next.impl.io.parser.nquads; + +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.NQuadsLexer; +import fr.inria.corese.core.next.impl.parser.antlr.NQuadsParser; +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 N-Quads format. + * This parser uses an ANTLR grammar to tokenize and parse N-Quads documents, + * then a listener to build the RDF model. + */ +public class ANTLRNQuadsParser extends AbstractRDFParser { + + /** + * Constructor for the ANTLRNQuadsParser. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + */ + public ANTLRNQuadsParser(Model model, ValueFactory factory) { + super(model, factory); + } + + /** + * Constructor for the ANTLRNQuadsParser 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 ANTLRNQuadsParser(Model model, ValueFactory factory, IOOptions config) { + super(model, factory, config); + } + + @Override + public RDFFormat getRDFFormat() { + return RDFFormat.NQUADS; + } + + + @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 N-Quads data from a Reader using ANTLR4. + * + * @param reader The Reader to read RDF data from. + * @param baseURI The base URI (ignored for N-Quads as all URIs are absolute). + * @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); + NQuadsLexer lexer = new NQuadsLexer(charStream); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + NQuadsParser antlrParser = new NQuadsParser(tokens); + ParseTreeWalker walker = new ParseTreeWalker(); + ParseTree tree = antlrParser.nquadsDoc(); + + NQuadsListener listener = new NQuadsListener(getModel(), getValueFactory(), getConfig()); + + walker.walk((ParseTreeListener) listener, tree); + + } catch (IOException e) { + throw new ParsingErrorException("Failed to parse N-Quads: " + e.getMessage(), e); + } catch (Exception e) { + throw new ParsingErrorException("Unexpected error during N-Quads parsing: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListener.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListener.java new file mode 100644 index 000000000..6280d47a8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListener.java @@ -0,0 +1,297 @@ +package fr.inria.corese.core.next.impl.io.parser.nquads; + +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.impl.parser.antlr.NQuadsBaseListener; +import fr.inria.corese.core.next.impl.parser.antlr.NQuadsParser; + +/** + * Listener for the ANTLR4 generated parser for N-Quads. + * This listener traverses the parse tree and builds the RDF model, + * supporting named graphs. It includes unescaping logic for URIs and literals. + */ +public class NQuadsListener extends NQuadsBaseListener { + + private final Model model; + private final ValueFactory factory; + @SuppressWarnings("unused") + private final IOOptions options; + + private Resource currentSubject; + private IRI currentPredicate; + private Resource currentGraph; + + /** + * Constructor for the NQuadsListener. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + * @param options IOOptions for configuration (if any). + */ + public NQuadsListener(Model model, ValueFactory factory, IOOptions options) { + this.model = model; + this.factory = factory; + this.options = options; + } + + @Override + public void enterStatement(NQuadsParser.StatementContext ctx) { + + currentSubject = extractSubject(ctx.subject()); + currentPredicate = extractPredicate(ctx.predicate()); + if (ctx.graphLabel() != null) { + currentGraph = extractGraph(ctx.graphLabel()); + } else { + currentGraph = null; + } + } + + @Override + public void exitStatement(NQuadsParser.StatementContext ctx) { + + Value object = extractObject(ctx.object()); + if (currentGraph != null) { + model.add(currentSubject, currentPredicate, object, currentGraph); + } else { + model.add(currentSubject, currentPredicate, object); + } + currentSubject = null; + currentPredicate = null; + currentGraph = null; + } + + /** + * Extracts a resource (IRI or Blank Node) from the subject context. + */ + protected Resource extractSubject(NQuadsParser.SubjectContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + if (ctx.BLANK_NODE_LABEL() != null) { + return factory.createBNode(ctx.BLANK_NODE_LABEL().getText().substring(2)); + } + throw new IllegalArgumentException("Unsupported N-Quads subject: " + ctx.getText()); + } + + /** + * Extracts a predicate (IRI) from the predicate context. + */ + protected IRI extractPredicate(NQuadsParser.PredicateContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + throw new IllegalArgumentException("Unsupported N-Quads predicate: " + ctx.getText()); + } + + /** + * Extracts a value (IRI, Blank Node, or Literal) from the object context. + */ + protected Value extractObject(NQuadsParser.ObjectContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + if (ctx.BLANK_NODE_LABEL() != null) { + return factory.createBNode(ctx.BLANK_NODE_LABEL().getText().substring(2)); + } + if (ctx.literal() != null) { + return extractLiteral(ctx.literal()); + } + throw new IllegalArgumentException("Unsupported N-Quads object: " + ctx.getText()); + } + + /** + * Extracts a graph (IRI or Blank Node) from the graph context. + */ + protected Resource extractGraph(NQuadsParser.GraphLabelContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + if (ctx.BLANK_NODE_LABEL() != null) { + return factory.createBNode(ctx.BLANK_NODE_LABEL().getText().substring(2)); + } + throw new IllegalArgumentException("Unsupported N-Quads graph: " + ctx.getText()); + } + + /** + * Extracts and unescapes a literal from the ANTLR context. + * This method handles string literals with or without datatype/language. + */ + protected Literal extractLiteral(NQuadsParser.LiteralContext ctx) { + String label = ctx.STRING_LITERAL_QUOTE().getText(); + label = unescapeLiteral(label); + + if (ctx.IRIREF() != null) { + IRI datatype = factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + return factory.createLiteral(label, datatype); + } + if (ctx.LANGTAG() != null) { + String lang = ctx.LANGTAG().getText().substring(1); + return factory.createLiteral(label, lang); + } + + return factory.createLiteral(label); + } + + /** + * Unescapes common N-Quads literal escape sequences. + * This method handles `\"`, `\\`, `\n`, `\t`, `\r`, `\b`, `\f`. + * It also handles `\ uXXXX` and `\UXXXXXXXX` for Unicode escapes. + * It also removes the surrounding quotes from the literal string. + * + * @param literalText The raw literal string from ANTLR (including quotes and escapes). + * @return The unescaped literal string without surrounding quotes. + */ + protected String unescapeLiteral(String literalText) { + String unquotedLiteral = literalText.substring(1, literalText.length() - 1); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < unquotedLiteral.length(); i++) { + char c = unquotedLiteral.charAt(i); + if (c == '\\' && i + 1 < unquotedLiteral.length()) { + char nextChar = unquotedLiteral.charAt(i + 1); + switch (nextChar) { + case '"': + sb.append('"'); + i++; + break; + case '\\': + sb.append('\\'); + i++; + break; + case 'n': + sb.append('\n'); + i++; + break; + case 't': + sb.append('\t'); + i++; + break; + case 'r': + sb.append('\r'); + i++; + break; + case 'b': + sb.append('\b'); + i++; + break; + case 'f': + sb.append('\f'); + i++; + break; + case 'u': + if (i + 5 < unquotedLiteral.length()) { + String hex = unquotedLiteral.substring(i + 2, i + 6); + try { + int unicodeChar = Integer.parseInt(hex, 16); + sb.append((char) unicodeChar); + i += 5; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\uXXXX escape sequence in literal: \\u" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\uXXXX escape sequence in literal: " + unquotedLiteral.substring(i)); + } + break; + case 'U': + if (i + 9 < unquotedLiteral.length()) { + String hex = unquotedLiteral.substring(i + 2, i + 10); + try { + int unicodeChar = Integer.parseInt(hex, 16); + if (Character.isSupplementaryCodePoint(unicodeChar)) { + sb.append(Character.highSurrogate(unicodeChar)); + sb.append(Character.lowSurrogate(unicodeChar)); + } else { + sb.append((char) unicodeChar); + } + i += 9; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\UXXXXXXXX escape sequence in literal: \\U" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\UXXXXXXXX escape sequence in literal: " + unquotedLiteral.substring(i)); + } + break; + default: + sb.append(c).append(nextChar); + i++; + break; + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Unescapes common N-Quads URI escape sequences. + * This method handles `\>`, `\\`, `\ uXXXX`, `\UXXXXXXXX`. + * + * @param uri The escaped URI string. + * @return The unescaped URI string. + */ + protected String unescapeUri(String uri) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < uri.length(); i++) { + char c = uri.charAt(i); + if (c == '\\' && i + 1 < uri.length()) { + char nextChar = uri.charAt(i + 1); + switch (nextChar) { + case '>': + sb.append('>'); + i++; + break; + case '\\': + sb.append('\\'); + i++; + break; + case 'u': + if (i + 5 < uri.length()) { + String hex = uri.substring(i + 2, i + 6); + try { + int unicodeChar = Integer.parseInt(hex, 16); + sb.append((char) unicodeChar); + i += 5; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\uXXXX escape sequence in URI: \\u" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\uXXXX escape sequence in URI: " + uri.substring(i)); + } + break; + case 'U': + if (i + 9 < uri.length()) { + String hex = uri.substring(i + 2, i + 10); + try { + int unicodeChar = Integer.parseInt(hex, 16); + if (Character.isSupplementaryCodePoint(unicodeChar)) { + sb.append(Character.highSurrogate(unicodeChar)); + sb.append(Character.lowSurrogate(unicodeChar)); + } else { + sb.append((char) unicodeChar); + } + i += 9; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\UXXXXXXXX escape sequence in URI: \\U" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\UXXXXXXXX escape sequence in URI: " + uri.substring(i)); + } + break; + default: + sb.append(c).append(nextChar); + i++; + break; + } + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParser.java new file mode 100644 index 000000000..75370cf8f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParser.java @@ -0,0 +1,102 @@ +package fr.inria.corese.core.next.impl.io.parser.ntriples; + +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.NTriplesLexer; +import fr.inria.corese.core.next.impl.parser.antlr.NTriplesParser; +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 N-Triples format. + * This parser uses an ANTLR grammar to tokenize and parse N-Triples documents, + * then a listener to build the RDF model. + */ +public class ANTLRNTriplesParser extends AbstractRDFParser { + + /** + * Constructor for the ANTLRNTriplesParser. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + */ + public ANTLRNTriplesParser(Model model, ValueFactory factory) { + super(model, factory); + } + + /** + * Constructor for the ANTLRNTriplesParser 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 ANTLRNTriplesParser(Model model, ValueFactory factory, IOOptions config) { + super(model, factory, config); + } + + @Override + public RDFFormat getRDFFormat() { + return RDFFormat.NTRIPLES; + } + + + @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 N-Triples data from a Reader using ANTLR4. + * + * @param reader The Reader to read RDF data from. + * @param baseURI The base URI (ignored for N-Triples as all URIs are absolute). + * @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); + NTriplesLexer lexer = new NTriplesLexer(charStream); + CommonTokenStream tokens = new CommonTokenStream(lexer); + + NTriplesParser antlrParser = new NTriplesParser(tokens); + ParseTreeWalker walker = new ParseTreeWalker(); + ParseTree tree = antlrParser.ntriplesDoc(); + + + NTriplesListener listener = new NTriplesListener(getModel(), getValueFactory(), getConfig()); + + walker.walk((ParseTreeListener) listener, tree); + + } catch (IOException e) { + throw new ParsingErrorException("Failed to parse N-Triples: " + e.getMessage(), e); + } catch (Exception e) { + throw new ParsingErrorException("Unexpected error during N-Triples parsing: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListener.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListener.java new file mode 100644 index 000000000..898ca2053 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListener.java @@ -0,0 +1,270 @@ +package fr.inria.corese.core.next.impl.io.parser.ntriples; + +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.impl.parser.antlr.NTriplesBaseListener; +import fr.inria.corese.core.next.impl.parser.antlr.NTriplesParser; + +/** + * Listener for the ANTLR4 generated parser for N-Triples. + * This listener traverses the parse tree and builds the RDF model. + * It includes unescaping logic for URIs and literals. + */ +public class NTriplesListener extends NTriplesBaseListener { + + private final Model model; + private final ValueFactory factory; + @SuppressWarnings("unused") + private final IOOptions options; + + private Resource currentSubject; + private IRI currentPredicate; + + /** + * Constructor for the NTriplesListener. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + * @param options IOOptions for configuration (if any). + */ + public NTriplesListener(Model model, ValueFactory factory, IOOptions options) { + this.model = model; + this.factory = factory; + this.options = options; + } + + @Override + public void enterTriple(NTriplesParser.TripleContext ctx) { + currentSubject = extractSubject(ctx.subject()); + currentPredicate = extractPredicate(ctx.predicate()); + } + + @Override + public void exitTriple(NTriplesParser.TripleContext ctx) { + Value object = extractObject(ctx.object()); + model.add(currentSubject, currentPredicate, object); + currentSubject = null; + currentPredicate = null; + } + + /** + * Extracts a resource (IRI or Blank Node) from the subject context. + */ + protected Resource extractSubject(NTriplesParser.SubjectContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + if (ctx.BLANK_NODE_LABEL() != null) { + return factory.createBNode(ctx.BLANK_NODE_LABEL().getText().substring(2)); + } + throw new IllegalArgumentException("Unsupported N-Triples subject: " + ctx.getText()); + } + + /** + * Extracts a predicate (IRI) from the predicate context. + */ + protected IRI extractPredicate(NTriplesParser.PredicateContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + throw new IllegalArgumentException("Unsupported N-Triples predicate: " + ctx.getText()); + } + + /** + * Extracts a value (IRI, Blank Node, or Literal) from the object context. + */ + protected Value extractObject(NTriplesParser.ObjectContext ctx) { + if (ctx.IRIREF() != null) { + return factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + } + if (ctx.BLANK_NODE_LABEL() != null) { + return factory.createBNode(ctx.BLANK_NODE_LABEL().getText().substring(2)); + } + if (ctx.literal() != null) { + return extractLiteral(ctx.literal()); + } + throw new IllegalArgumentException("Unsupported N-Triples object: " + ctx.getText()); + } + + /** + * Extracts and unescapes a literal from the ANTLR context. + * This method handles string literals with or without datatype/language. + */ + protected Literal extractLiteral(NTriplesParser.LiteralContext ctx) { + String label = ctx.STRING_LITERAL_QUOTE().getText(); + label = unescapeLiteral(label); + + if (ctx.IRIREF() != null) { + IRI datatype = factory.createIRI(unescapeUri(ctx.IRIREF().getText().substring(1, ctx.IRIREF().getText().length() - 1))); + return factory.createLiteral(label, datatype); + } + if (ctx.LANGTAG() != null) { + String lang = ctx.LANGTAG().getText().substring(1); + return factory.createLiteral(label, lang); + } + return factory.createLiteral(label); + } + + /** + * Unescapes common N-Triples literal escape sequences. + * This method handles `\"`, `\\`, `\n`, `\t`, `\r`, `\b`, `\f`. + * It also handles `\ uXXXX` and `\UXXXXXXXX` for Unicode escapes. + * It also removes the surrounding quotes from the literal string. + * + * @param literalText The raw literal string from ANTLR (including quotes and escapes). + * @return The unescaped literal string without surrounding quotes. + */ + protected String unescapeLiteral(String literalText) { + String unquotedLiteral = literalText.substring(1, literalText.length() - 1); + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < unquotedLiteral.length(); i++) { + char c = unquotedLiteral.charAt(i); + if (c == '\\' && i + 1 < unquotedLiteral.length()) { + char nextChar = unquotedLiteral.charAt(i + 1); + switch (nextChar) { + case '"': + builder.append('"'); + i++; + break; + case '\\': + builder.append('\\'); + i++; + break; + case 'n': + builder.append('\n'); + i++; + break; + case 't': + builder.append('\t'); + i++; + break; + case 'r': + builder.append('\r'); + i++; + break; + case 'b': + builder.append('\b'); + i++; + break; + case 'f': + builder.append('\f'); + i++; + break; + case 'u': + if (i + 5 < unquotedLiteral.length()) { + String hex = unquotedLiteral.substring(i + 2, i + 6); + try { + int unicodeChar = Integer.parseInt(hex, 16); + builder.append((char) unicodeChar); + i += 5; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\uXXXX escape sequence in literal: \\u" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\uXXXX escape sequence in literal: " + unquotedLiteral.substring(i)); + } + break; + case 'U': + if (i + 9 < unquotedLiteral.length()) { + String hex = unquotedLiteral.substring(i + 2, i + 10); + try { + int unicodeChar = Integer.parseInt(hex, 16); + if (Character.isSupplementaryCodePoint(unicodeChar)) { + builder.append(Character.highSurrogate(unicodeChar)); + builder.append(Character.lowSurrogate(unicodeChar)); + } else { + builder.append((char) unicodeChar); + } + i += 9; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\UXXXXXXXX escape sequence in literal: \\U" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\UXXXXXXXX escape sequence in literal: " + unquotedLiteral.substring(i)); + } + break; + default: + builder.append(c).append(nextChar); + i++; + break; + } + } else { + builder.append(c); + } + } + return builder.toString(); + } + + /** + * Unescapes common N-Triples URI escape sequences. + * This method handles `\>`, `\\`, `\ uXXXX`, `\UXXXXXXXX`. + * + * @param uri The escaped URI string. + * @return The unescaped URI string. + */ + protected String unescapeUri(String uri) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < uri.length(); i++) { + char c = uri.charAt(i); + if (c == '\\' && i + 1 < uri.length()) { + char nextChar = uri.charAt(i + 1); + switch (nextChar) { + case '>': + builder.append('>'); + i++; + break; + case '\\': + builder.append('\\'); + i++; + break; + case 'u': + if (i + 5 < uri.length()) { + String hex = uri.substring(i + 2, i + 6); + try { + int unicodeChar = Integer.parseInt(hex, 16); + builder.append((char) unicodeChar); + i += 5; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\uXXXX escape sequence in URI: \\u" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\uXXXX escape sequence in URI: " + uri.substring(i)); + } + break; + case 'U': + if (i + 9 < uri.length()) { + String hex = uri.substring(i + 2, i + 10); + try { + int unicodeChar = Integer.parseInt(hex, 16); + if (Character.isSupplementaryCodePoint(unicodeChar)) { + builder.append(Character.highSurrogate(unicodeChar)); + builder.append(Character.lowSurrogate(unicodeChar)); + } else { + builder.append((char) unicodeChar); + } + i += 9; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid \\UXXXXXXXX escape sequence in URI: \\U" + hex); + } + } else { + throw new IllegalArgumentException("Incomplete \\UXXXXXXXX escape sequence in URI: " + uri.substring(i)); + } + break; + default: + builder.append(c).append(nextChar); + i++; + break; + } + } else { + builder.append(c); + } + } + return builder.toString(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractGraphSerializer.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractGraphSerializer.java index f031da378..fef457855 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractGraphSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractGraphSerializer.java @@ -1,23 +1,45 @@ package fr.inria.corese.core.next.impl.io.serialization.base; -import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; -import fr.inria.corese.core.next.impl.common.literal.RDF; -import fr.inria.corese.core.next.impl.io.serialization.option.*; -import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; -import fr.inria.corese.core.next.impl.exception.SerializationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; +import fr.inria.corese.core.next.impl.common.literal.RDF; +import fr.inria.corese.core.next.impl.exception.SerializationException; +import fr.inria.corese.core.next.impl.io.serialization.option.AbstractSerializerOption; +import fr.inria.corese.core.next.impl.io.serialization.option.AbstractTFamilyOption; +import fr.inria.corese.core.next.impl.io.serialization.option.BlankNodeStyleEnum; +import fr.inria.corese.core.next.impl.io.serialization.option.LiteralDatatypePolicyEnum; +import fr.inria.corese.core.next.impl.io.serialization.option.PrefixOrderingEnum; +import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; + /** * Abstract base class for RDF serializers based on TriG and Turtle syntax. * This class contains the common logic for serializing RDF models diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractLineBasedSerializer.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractLineBasedSerializer.java index 5f3ae0be4..f0af2a595 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractLineBasedSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/base/AbstractLineBasedSerializer.java @@ -1,15 +1,5 @@ package fr.inria.corese.core.next.impl.io.serialization.base; -import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; -import fr.inria.corese.core.next.impl.common.literal.RDF; -import fr.inria.corese.core.next.impl.io.serialization.option.AbstractSerializerOption; -import fr.inria.corese.core.next.impl.io.serialization.option.LiteralDatatypePolicyEnum; -import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; -import fr.inria.corese.core.next.impl.exception.SerializationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; @@ -18,6 +8,22 @@ import java.util.Objects; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; +import fr.inria.corese.core.next.impl.common.literal.RDF; +import fr.inria.corese.core.next.impl.exception.SerializationException; +import fr.inria.corese.core.next.impl.io.serialization.option.AbstractSerializerOption; +import fr.inria.corese.core.next.impl.io.serialization.option.LiteralDatatypePolicyEnum; +import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; + /** * Base class for line-based RDF serializers (N-Triples, N-Quads). * Contains all the common logic for writing statements line by line. diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/jsonld/TitaniumRDFDatasetSerializationAdapter.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/jsonld/TitaniumRDFDatasetSerializationAdapter.java index c2b33d39a..d02fce12a 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/jsonld/TitaniumRDFDatasetSerializationAdapter.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/jsonld/TitaniumRDFDatasetSerializationAdapter.java @@ -1,25 +1,43 @@ package fr.inria.corese.core.next.impl.io.serialization.jsonld; -import com.apicatalog.rdf.*; -import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.literal.CoreDatatype; -import fr.inria.corese.core.next.impl.common.util.IRIUtils; -import fr.inria.corese.core.next.impl.common.vocabulary.RDF; -import fr.inria.corese.core.next.impl.common.vocabulary.XSD; -import fr.inria.corese.core.next.impl.exception.SerializationException; +import static fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants.DEFAULT_GRAPH_IRI; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.XMLGregorianCalendar; import java.math.BigDecimal; import java.math.BigInteger; import java.time.Duration; import java.time.LocalDateTime; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAmount; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; -import static fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants.DEFAULT_GRAPH_IRI; +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import com.apicatalog.rdf.RdfDataset; +import com.apicatalog.rdf.RdfGraph; +import com.apicatalog.rdf.RdfLiteral; +import com.apicatalog.rdf.RdfNQuad; +import com.apicatalog.rdf.RdfResource; +import com.apicatalog.rdf.RdfTriple; +import com.apicatalog.rdf.RdfValue; + +import fr.inria.corese.core.next.api.BNode; +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.literal.CoreDatatype; +import fr.inria.corese.core.next.impl.common.util.IRIUtils; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.common.vocabulary.XSD; +import fr.inria.corese.core.next.impl.exception.SerializationException; /** * Adapter class from Model to RdfDataset for usage in the JSON-LD serialization process using the titanium library. diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/rdfxml/XmlSerializer.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/rdfxml/XmlSerializer.java index e61d61aaa..4c59eaff7 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/rdfxml/XmlSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/rdfxml/XmlSerializer.java @@ -1,21 +1,35 @@ package fr.inria.corese.core.next.impl.io.serialization.rdfxml; -import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; -import fr.inria.corese.core.next.impl.io.serialization.option.LiteralDatatypePolicyEnum; -import fr.inria.corese.core.next.impl.io.serialization.option.PrefixOrderingEnum; -import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; -import fr.inria.corese.core.next.impl.exception.SerializationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedWriter; import java.io.IOException; import java.io.UncheckedIOException; import java.io.Writer; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.inria.corese.core.next.api.IRI; +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.io.serialization.RDFSerializer; +import fr.inria.corese.core.next.impl.exception.SerializationException; +import fr.inria.corese.core.next.impl.io.serialization.option.LiteralDatatypePolicyEnum; +import fr.inria.corese.core.next.impl.io.serialization.option.PrefixOrderingEnum; +import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; + /** * Serializes a {@link Model} to RDF/XML format. * This class provides a method to write the statements of a model to a {@link Writer} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/trig/TriGSerializer.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/trig/TriGSerializer.java index 4fe2d9605..3e6a3db90 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/trig/TriGSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/trig/TriGSerializer.java @@ -1,5 +1,15 @@ package fr.inria.corese.core.next.impl.io.serialization.trig; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + import fr.inria.corese.core.next.api.IRI; import fr.inria.corese.core.next.api.Model; import fr.inria.corese.core.next.api.Resource; @@ -7,10 +17,6 @@ import fr.inria.corese.core.next.impl.io.serialization.base.AbstractGraphSerializer; import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; -import java.io.IOException; -import java.io.Writer; -import java.util.*; - /** * Serializes a {@link Model} to TriG format with comprehensive syntax support. * This class provides a method to write the declarations of a model to a {@link Writer} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/turtle/TurtleSerializer.java b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/turtle/TurtleSerializer.java index 0d7fe4918..326725de9 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/serialization/turtle/TurtleSerializer.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/serialization/turtle/TurtleSerializer.java @@ -1,16 +1,18 @@ package fr.inria.corese.core.next.impl.io.serialization.turtle; -import fr.inria.corese.core.next.api.*; -import fr.inria.corese.core.next.impl.io.serialization.base.AbstractGraphSerializer; -import fr.inria.corese.core.next.impl.io.serialization.option.AbstractTFamilyOption; -import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.Writer; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Statement; +import fr.inria.corese.core.next.impl.io.serialization.base.AbstractGraphSerializer; +import fr.inria.corese.core.next.impl.io.serialization.option.AbstractTFamilyOption; +import fr.inria.corese.core.next.impl.io.serialization.util.SerializationConstants; + /** * Serializes a {@link Model} to Turtle format with comprehensive syntax support. * This class provides a method to write the declarations of a model to a {@link Writer} diff --git a/src/main/java/fr/inria/corese/core/sparql/datatype/function/StringHelper.java b/src/main/java/fr/inria/corese/core/sparql/datatype/function/StringHelper.java index 87581a0bc..30d6bbb25 100644 --- a/src/main/java/fr/inria/corese/core/sparql/datatype/function/StringHelper.java +++ b/src/main/java/fr/inria/corese/core/sparql/datatype/function/StringHelper.java @@ -358,7 +358,6 @@ public static int indexOfWordIgnoreCaseAccentAndPlurial(String string1, String s * qualifier, scientific notation and numbers marked with a type * qualifier (e.g. 123L). * - * @see org.apache.commons.lang3.math.NumberUtils * @param str the string to check * @return true if the string denotes a correctly formatted number, false otherwise. */ diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/ParserFactoryTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ParserFactoryTest.java new file mode 100644 index 000000000..eff44aa9b --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ParserFactoryTest.java @@ -0,0 +1,112 @@ +package fr.inria.corese.core.next.impl.io.parser; + +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.io.parser.RDFParser; +import fr.inria.corese.core.next.api.io.parser.RDFParserOptions; +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.turtle.ANTLRTurtleParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for the ParserFactory class. + * This class verifies that the factory correctly instantiates the appropriate + * RDFParser implementation based on the provided RdfFormat. + */ +@ExtendWith(MockitoExtension.class) +class ParserFactoryTest { + + private ParserFactory parserFactory; + + @Mock + private Model mockModel; + + @Mock + private ValueFactory mockValueFactory; + + @Mock + private RDFParserOptions mockParserOptions; + + + @BeforeEach + void setUp() { + parserFactory = new ParserFactory(); + } + + @Test + @DisplayName("createRDFParser (with config) should return JSONLDParser for JSONLD format") + void testCreateRDFParserWithConfig_JSONLD() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.JSONLD, mockModel, mockValueFactory, mockParserOptions); + assertNotNull(parser); + assertTrue(parser instanceof JSONLDParser); + } + + @Test + @DisplayName("createRDFParser (with config) should return ANTLRTurtleParser for TURTLE format") + void testCreateRDFParserWithConfig_TURTLE() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.TURTLE, mockModel, mockValueFactory, mockParserOptions); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRTurtleParser); + } + + @Test + @DisplayName("createRDFParser (with config) should return ANTLRNTriplesParser for N-TRIPLES format") + void testCreateRDFParserWithConfig_NTRIPLES() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.NTRIPLES, mockModel, mockValueFactory, mockParserOptions); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRNTriplesParser); + } + + @Test + @DisplayName("createRDFParser (with config) should return ANTLRNQuadsParser for N-QUADS format") + void testCreateRDFParserWithConfig_NQUADS() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.NQUADS, mockModel, mockValueFactory, mockParserOptions); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRNQuadsParser); + } + + + @Test + @DisplayName("createRDFParser (without config) should return JSONLDParser for JSONLD format") + void testCreateRDFParserWithoutConfig_JSONLD() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.JSONLD, mockModel, mockValueFactory); + assertNotNull(parser); + assertTrue(parser instanceof JSONLDParser); + } + + @Test + @DisplayName("createRDFParser (without config) should return ANTLRTurtleParser for TURTLE format") + void testCreateRDFParserWithoutConfig_TURTLE() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.TURTLE, mockModel, mockValueFactory); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRTurtleParser); + } + + @Test + @DisplayName("createRDFParser (without config) should return ANTLRNTriplesParser for N-TRIPLES format") + void testCreateRDFParserWithoutConfig_NTRIPLES() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.NTRIPLES, mockModel, mockValueFactory); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRNTriplesParser); + } + + @Test + @DisplayName("createRDFParser (without config) should return ANTLRNQuadsParser for N-QUADS format") + void testCreateRDFParserWithoutConfig_NQUADS() { + RDFParser parser = parserFactory.createRDFParser(RDFFormat.NQUADS, mockModel, mockValueFactory); + assertNotNull(parser); + assertTrue(parser instanceof ANTLRNQuadsParser); + } + +} diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParserTest.java new file mode 100644 index 000000000..e0a26f63e --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/ANTLRNQuadsParserTest.java @@ -0,0 +1,197 @@ +package fr.inria.corese.core.next.impl.io.parser.nquads; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.base.io.RDFFormat; +import fr.inria.corese.core.next.impl.exception.ParsingErrorException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the ANTLRNQuadsParser class. + * These tests verify the parser's ability to correctly parse N-Quads + * and interact with the Model and ValueFactory, including error handling + * and unescaping of IRIs and literals, and named graphs. + */ +@ExtendWith(MockitoExtension.class) +class ANTLRNQuadsParserTest { + + @Mock + private Model mockModel; + + @Mock + private ValueFactory mockValueFactory; + + private ANTLRNQuadsParser parser; + + @Mock + private IRI mockSubjectIRI; + @Mock + private IRI mockPredicateIRI; + @Mock + private IRI mockObjectIRI; + @Mock + private IRI mockGraphIRI; + @Mock + private BNode mockSubjectBNode; + @Mock + private BNode mockObjectBNode; + @Mock + private BNode mockGraphBNode; + @Mock + private Literal mockSimpleLiteral; + @Mock + private Literal mockLangLiteral; + @Mock + private Literal mockTypedLiteral; + @Mock + private IRI mockDatatypeIRI; + @Mock + private IRI mockEscapedIRI; + @Mock + private Literal mockEscapedLiteral; + + + @BeforeEach + void setUp() { + parser = new ANTLRNQuadsParser(mockModel, mockValueFactory); + + lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { + String uri = invocation.getArgument(0); + if (uri.equals("http://example.org/subject")) return mockSubjectIRI; + if (uri.equals("http://example.org/predicate")) return mockPredicateIRI; + if (uri.equals("http://example.org/object")) return mockObjectIRI; + if (uri.equals("http://example.org/graph")) return mockGraphIRI; + if (uri.equals("http://www.w3.org/2001/XMLSchema#integer")) return mockDatatypeIRI; + if (uri.equals("http://example.org/escaped>uri")) return mockEscapedIRI; + if (uri.equals("http://example.org/s ubject")) return mock(IRI.class); + if (uri.equals("http://example.org/path/€")) return mock(IRI.class); + if (uri.equals("http://example.org/path>with\\ { + String label = invocation.getArgument(0); + if (label.equals("sub1")) return mockSubjectBNode; + if (label.equals("obj1")) return mockObjectBNode; + if (label.equals("graph1")) return mockGraphBNode; + return mock(BNode.class); + }); + + lenient().when(mockValueFactory.createLiteral(eq("simple string"))).thenReturn(mockSimpleLiteral); + lenient().when(mockValueFactory.createLiteral(eq("hello"), eq("en"))).thenReturn(mockLangLiteral); + lenient().when(mockValueFactory.createLiteral(eq("123"), any(IRI.class))).thenReturn(mockTypedLiteral); + lenient().when(mockValueFactory.createLiteral(eq("literal with \"quotes\" and \n newline"))).thenReturn(mockEscapedLiteral); + } + + @Test + @DisplayName("Test get RDF format returns NQUADS") + void testGetRDFFormat() { + assertEquals(RDFFormat.NQUADS, parser.getRDFFormat()); + } + + @Test + @DisplayName("Test parsing a basic quad with IRI graph") + void testParseBasicQuadWithIRIGraph() throws ParsingErrorException { + String nquad = " ."; + parser.parse(new StringReader(nquad)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI, mockGraphIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a basic quad with BNode graph") + void testParseBasicQuadWithBNodeGraph() throws ParsingErrorException { + String nquad = " _:graph1 ."; + parser.parse(new StringReader(nquad)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI, mockGraphBNode); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a quad with a literal object and IRI graph") + void testParseQuadWithLiteralObjectAndIRIGraph() throws ParsingErrorException { + String nquad = " \"simple string\" ."; + parser.parse(new StringReader(nquad)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockSimpleLiteral, mockGraphIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple (no graph) which should go to default graph") + void testParseTripleToDefaultGraph() throws ParsingErrorException { + String nquad = " ."; + parser.parse(new StringReader(nquad)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI); + verifyNoMoreInteractions(mockModel); + } + + + @Test + @DisplayName("Test parsing a quad with escaped characters in literal") + void testParseEscapedLiteral() throws ParsingErrorException { + String nquad = " \"literal with \\\"quotes\\\" and \\n newline\" ."; + parser.parse(new StringReader(nquad)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockEscapedLiteral, mockGraphIRI); + verifyNoMoreInteractions(mockModel); + } + + + @Test + @DisplayName("Test parsing a quad with Unicode escape in literal (\\uXXXX)") + void testParseUnicodeEscapeLiteralU() throws ParsingErrorException { + String nquad = " \"Hello\\u0020World\" ."; + Literal expectedLiteral = mock(Literal.class); + lenient().when(mockValueFactory.createLiteral(eq("Hello World"))).thenReturn(expectedLiteral); + + parser.parse(new StringReader(nquad)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, expectedLiteral, mockGraphIRI); + } + + @Test + @DisplayName("Test parsing a quad with Unicode escape in literal (\\UXXXXXXXX)") + void testParseUnicodeEscapeLiteralUx() throws ParsingErrorException { + String nquad = " \"Euro\" ."; + Literal expectedLiteral = mock(Literal.class); + lenient().when(mockValueFactory.createLiteral(eq("Euro"))).thenReturn(expectedLiteral); + + parser.parse(new StringReader(nquad)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, expectedLiteral, mockGraphIRI); + } + + @Test + @DisplayName("Test parsing a quad with Unicode escape in IRI (\\uXXXX) in graph") + void testParseUnicodeEscapeIRIUInGraph() throws ParsingErrorException { + String nquad = " ."; + IRI expectedGraphIRI = mock(IRI.class); + lenient().when(mockValueFactory.createIRI(eq("http://example.org/graphName"))).thenReturn(expectedGraphIRI); + + parser.parse(new StringReader(nquad)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI, expectedGraphIRI); + } + + @Test + @DisplayName("Test parsing a quad with Unicode escape in IRI (\\UXXXXXXXX) in graph") + void testParseUnicodeEscapeIRIUxInGraph() throws ParsingErrorException { + String nquad = " ."; + IRI expectedGraphIRI = mock(IRI.class); + lenient().when(mockValueFactory.createIRI(eq("http://example.org/graph"))).thenReturn(expectedGraphIRI); + + parser.parse(new StringReader(nquad)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI, expectedGraphIRI); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListenerTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListenerTest.java new file mode 100644 index 000000000..9961f68ba --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/nquads/NQuadsListenerTest.java @@ -0,0 +1,362 @@ +package fr.inria.corese.core.next.impl.io.parser.nquads; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.impl.parser.antlr.NQuadsParser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the NQuadsListener 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, + * including named graphs. + */ +@ExtendWith(MockitoExtension.class) +class NQuadsListenerTest { + + @Mock + private Model mockModel; + + @Mock + private ValueFactory mockValueFactory; + + @Mock + private IOOptions mockIOOptions; + + private NQuadsListener listener; + + + @Mock + private IRI mockIRI; + @Mock + private BNode mockBNode; + @Mock + private Literal mockLiteral; + @Mock + private IRI mockDatatypeIRI; + @Mock + private IRI mockGraphIRI; + @Mock + private BNode mockGraphBNode; + + @BeforeEach + void setUp() { + listener = new NQuadsListener(mockModel, mockValueFactory, mockIOOptions); + + lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { + String uri = invocation.getArgument(0); + if (uri.equals("http://example.org/test")) return mockIRI; + if (uri.equals("http://example.org/datatype")) return mockDatatypeIRI; + if (uri.equals("http://example.org/graph")) return mockGraphIRI; + if (uri.equals("http://example.org/escaped>uri")) return mock(IRI.class); + if (uri.equals("http://example.org/s ubject")) return mock(IRI.class); + if (uri.equals("http://example.org/path/€")) return mock(IRI.class); + if (uri.equals("http://example.org/path>with\\ { + String label = invocation.getArgument(0); + if (label.equals("b1")) return mockBNode; + if (label.equals("graph1")) return mockGraphBNode; + return mock(BNode.class); + }); + + lenient().when(mockValueFactory.createLiteral(anyString())).thenAnswer(invocation -> { + String value = invocation.getArgument(0); + if (value.equals("test literal")) return mockLiteral; + if (value.equals("literal with \"quotes\" and \n newline")) return mock(Literal.class); + if (value.equals("Hello World")) return mock(Literal.class); + if (value.equals("Euro€")) return mock(Literal.class); + return mock(Literal.class); + }); + lenient().when(mockValueFactory.createLiteral(anyString(), any(IRI.class))).thenReturn(mock(Literal.class)); + lenient().when(mockValueFactory.createLiteral(anyString(), anyString())).thenReturn(mock(Literal.class)); + } + + private TerminalNode mockTerminalNode(String text) { + TerminalNode node = mock(TerminalNode.class); + when(node.getText()).thenReturn(text); + return node; + } + + private T mockRuleContext(Class clazz) { + return mock(clazz); + } + + @Test + @DisplayName("enterStatement and exitStatement should add a quad to the model with IRI graph") + void testEnterExitStatementAddsToModelWithIRIGraph() { + NQuadsParser.StatementContext statementCtx = mockRuleContext(NQuadsParser.StatementContext.class); + NQuadsParser.SubjectContext subjectCtx = mockRuleContext(NQuadsParser.SubjectContext.class); + NQuadsParser.PredicateContext predicateCtx = mockRuleContext(NQuadsParser.PredicateContext.class); + NQuadsParser.ObjectContext objectCtx = mockRuleContext(NQuadsParser.ObjectContext.class); + NQuadsParser.GraphLabelContext graphLabelCtx = mockRuleContext(NQuadsParser.GraphLabelContext.class); + + + TerminalNode subjectIriRef = mockTerminalNode(""); + when(subjectCtx.IRIREF()).thenReturn(subjectIriRef); + + TerminalNode predicateIriRef = mockTerminalNode(""); + when(predicateCtx.IRIREF()).thenReturn(predicateIriRef); + + TerminalNode objectIriRef = mockTerminalNode(""); + when(objectCtx.IRIREF()).thenReturn(objectIriRef); + + TerminalNode graphIriRef = mockTerminalNode(""); + when(graphLabelCtx.IRIREF()).thenReturn(graphIriRef); + + + when(statementCtx.subject()).thenReturn(subjectCtx); + when(statementCtx.predicate()).thenReturn(predicateCtx); + when(statementCtx.object()).thenReturn(objectCtx); + when(statementCtx.graphLabel()).thenReturn(graphLabelCtx); + + IRI actualSubjectIRI = mock(IRI.class); + IRI actualPredicateIRI = mock(IRI.class); + IRI actualObjectIRI = mock(IRI.class); + IRI actualGraphIRI = mock(IRI.class); + when(mockValueFactory.createIRI("http://example.org/subject")).thenReturn(actualSubjectIRI); + when(mockValueFactory.createIRI("http://example.org/predicate")).thenReturn(actualPredicateIRI); + when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); + when(mockValueFactory.createIRI("http://example.org/graph")).thenReturn(actualGraphIRI); + + + listener.enterStatement(statementCtx); + listener.exitStatement(statementCtx); + + verify(mockModel).add(actualSubjectIRI, actualPredicateIRI, actualObjectIRI, actualGraphIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("enterStatement and exitStatement should add a triple to the model (default graph)") + void testEnterExitStatementAddsToModelDefaultGraph() { + NQuadsParser.StatementContext statementCtx = mockRuleContext(NQuadsParser.StatementContext.class); + NQuadsParser.SubjectContext subjectCtx = mockRuleContext(NQuadsParser.SubjectContext.class); + NQuadsParser.PredicateContext predicateCtx = mockRuleContext(NQuadsParser.PredicateContext.class); + NQuadsParser.ObjectContext objectCtx = mockRuleContext(NQuadsParser.ObjectContext.class); + + TerminalNode subjectIriRef = mockTerminalNode(""); + when(subjectCtx.IRIREF()).thenReturn(subjectIriRef); + + TerminalNode predicateIriRef = mockTerminalNode(""); + when(predicateCtx.IRIREF()).thenReturn(predicateIriRef); + + TerminalNode objectIriRef = mockTerminalNode(""); + when(objectCtx.IRIREF()).thenReturn(objectIriRef); + + when(statementCtx.subject()).thenReturn(subjectCtx); + when(statementCtx.predicate()).thenReturn(predicateCtx); + when(statementCtx.object()).thenReturn(objectCtx); + when(statementCtx.graphLabel()).thenReturn(null); + + IRI actualSubjectIRI = mock(IRI.class); + IRI actualPredicateIRI = mock(IRI.class); + IRI actualObjectIRI = mock(IRI.class); + when(mockValueFactory.createIRI("http://example.org/subject")).thenReturn(actualSubjectIRI); + when(mockValueFactory.createIRI("http://example.org/predicate")).thenReturn(actualPredicateIRI); + when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); + + + listener.enterStatement(statementCtx); + listener.exitStatement(statementCtx); + + verify(mockModel).add(actualSubjectIRI, actualPredicateIRI, actualObjectIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("extractSubject should return IRI for IRIREF context") + void testExtractSubjectIRI() { + NQuadsParser.SubjectContext ctx = mockRuleContext(NQuadsParser.SubjectContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + Resource result = listener.extractSubject(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractSubject should return BNode for BLANK_NODE_LABEL context") + void testExtractSubjectBNode() { + NQuadsParser.SubjectContext ctx = mockRuleContext(NQuadsParser.SubjectContext.class); + TerminalNode bNodeLabel = mockTerminalNode("_:b1"); + when(ctx.BLANK_NODE_LABEL()).thenReturn(bNodeLabel); + + Resource result = listener.extractSubject(ctx); + assertEquals(mockBNode, result); + verify(mockValueFactory).createBNode("b1"); + } + + @Test + @DisplayName("extractPredicate should return IRI for IRIREF context") + void testExtractPredicateIRI() { + NQuadsParser.PredicateContext ctx = mockRuleContext(NQuadsParser.PredicateContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + IRI result = listener.extractPredicate(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractObject should return IRI for IRIREF context") + void testExtractObjectIRI() { + NQuadsParser.ObjectContext ctx = mockRuleContext(NQuadsParser.ObjectContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + Value result = listener.extractObject(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractObject should return BNode for BLANK_NODE_LABEL context") + void testExtractObjectBNode() { + NQuadsParser.ObjectContext ctx = mockRuleContext(NQuadsParser.ObjectContext.class); + TerminalNode bNodeLabel = mockTerminalNode("_:b1"); + when(ctx.BLANK_NODE_LABEL()).thenReturn(bNodeLabel); + + Value result = listener.extractObject(ctx); + assertEquals(mockBNode, result); + verify(mockValueFactory).createBNode("b1"); + } + + @Test + @DisplayName("extractObject should return Literal for literal context (simple string)") + void testExtractObjectLiteralSimple() { + NQuadsParser.ObjectContext objCtx = mockRuleContext(NQuadsParser.ObjectContext.class); + NQuadsParser.LiteralContext litCtx = mockRuleContext(NQuadsParser.LiteralContext.class); + TerminalNode stringLiteral = mockTerminalNode("\"test literal\""); + + when(objCtx.literal()).thenReturn(litCtx); + when(litCtx.STRING_LITERAL_QUOTE()).thenReturn(stringLiteral); + when(litCtx.IRIREF()).thenReturn(null); + when(litCtx.LANGTAG()).thenReturn(null); + + Value result = listener.extractObject(objCtx); + assertEquals(mockLiteral, result); + verify(mockValueFactory).createLiteral("test literal"); + } + + @Test + @DisplayName("extractObject should return Literal for literal context (language-tagged)") + void testExtractObjectLiteralLang() { + NQuadsParser.ObjectContext objCtx = mockRuleContext(NQuadsParser.ObjectContext.class); + NQuadsParser.LiteralContext litCtx = mockRuleContext(NQuadsParser.LiteralContext.class); + TerminalNode stringLiteral = mockTerminalNode("\"hello\""); + TerminalNode langTag = mockTerminalNode("@en"); + + when(objCtx.literal()).thenReturn(litCtx); + when(litCtx.STRING_LITERAL_QUOTE()).thenReturn(stringLiteral); + when(litCtx.IRIREF()).thenReturn(null); + when(litCtx.LANGTAG()).thenReturn(langTag); + + Literal expectedLiteral = mock(Literal.class); + when(mockValueFactory.createLiteral("hello", "en")).thenReturn(expectedLiteral); + + Value result = listener.extractObject(objCtx); + assertEquals(expectedLiteral, result); + verify(mockValueFactory).createLiteral("hello", "en"); + } + + + @Test + @DisplayName("extractGraph should return IRI for IRIREF context") + void testExtractGraphIRI() { + NQuadsParser.GraphLabelContext ctx = mockRuleContext(NQuadsParser.GraphLabelContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + Resource result = listener.extractGraph(ctx); + assertEquals(mockGraphIRI, result); + verify(mockValueFactory).createIRI("http://example.org/graph"); + } + + @Test + @DisplayName("extractGraph should return BNode for BLANK_NODE_LABEL context") + void testExtractGraphBNode() { + NQuadsParser.GraphLabelContext ctx = mockRuleContext(NQuadsParser.GraphLabelContext.class); + TerminalNode bNodeLabel = mockTerminalNode("_:graph1"); + when(ctx.BLANK_NODE_LABEL()).thenReturn(bNodeLabel); + + Resource result = listener.extractGraph(ctx); + assertEquals(mockGraphBNode, result); + verify(mockValueFactory).createBNode("graph1"); + } + + + + + @Test + @DisplayName("unescapeLiteral should throw IllegalArgumentException for invalid \\UXXXXXXXX") + void testUnescapeLiteralInvalidUx() throws NoSuchMethodException { + String input = "\"Invalid\\U0000XXX\""; + java.lang.reflect.Method method = NQuadsListener.class.getDeclaredMethod("unescapeLiteral", String.class); + method.setAccessible(true); + assertThrows(IllegalArgumentException.class, + () -> listener.unescapeLiteral(input), + "Should throw for malformed \\UXXXXXXXX escape sequence"); + } + + + @Test + @DisplayName("unescapeUri should handle basic escape sequences") + void testUnescapeUriBasicEscapes() throws NoSuchMethodException, java.lang.reflect.InvocationTargetException, IllegalAccessException { + + String input = "http://example.org/path\\>with\\ listener.unescapeLiteral(input), + "Should throw unescapeUri should throw IllegalArgumentException for invalid \\uXXXX"); + + } + + +} diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParserTest.java new file mode 100644 index 000000000..16c6ce6ea --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/ANTLRNTriplesParserTest.java @@ -0,0 +1,209 @@ +package fr.inria.corese.core.next.impl.io.parser.ntriples; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.base.io.RDFFormat; +import fr.inria.corese.core.next.impl.exception.ParsingErrorException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the ANTLRNTriplesParser class. + * These tests verify the parser's ability to correctly parse N-Triples + * and interact with the Model and ValueFactory, including error handling + * and unescaping of IRIs and literals. + */ +@ExtendWith(MockitoExtension.class) +class ANTLRNTriplesParserTest { + + @Mock + private Model mockModel; + + @Mock + private ValueFactory mockValueFactory; + + private ANTLRNTriplesParser parser; + + @Mock + private IRI mockSubjectIRI; + @Mock + private IRI mockPredicateIRI; + @Mock + private IRI mockObjectIRI; + @Mock + private BNode mockSubjectBNode; + @Mock + private BNode mockObjectBNode; + @Mock + private Literal mockSimpleLiteral; + @Mock + private Literal mockLangLiteral; + @Mock + private Literal mockTypedLiteral; + @Mock + private IRI mockDatatypeIRI; + @Mock + private IRI mockEscapedIRI; + @Mock + private Literal mockEscapedLiteral; + + + @BeforeEach + void setUp() { + parser = new ANTLRNTriplesParser(mockModel, mockValueFactory); + + lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { + String uri = invocation.getArgument(0); + if (uri.equals("http://example.org/subject")) return mockSubjectIRI; + if (uri.equals("http://example.org/predicate")) return mockPredicateIRI; + if (uri.equals("http://example.org/object")) return mockObjectIRI; + if (uri.equals("http://www.w3.org/2001/XMLSchema#integer")) return mockDatatypeIRI; + if (uri.equals("http://example.org/escaped>uri")) return mockEscapedIRI; + return mock(IRI.class); + }); + + lenient().when(mockValueFactory.createBNode(anyString())).thenAnswer(invocation -> { + String label = invocation.getArgument(0); + if (label.equals("sub1")) return mockSubjectBNode; + if (label.equals("obj1")) return mockObjectBNode; + return mock(BNode.class); + }); + + lenient().when(mockValueFactory.createLiteral(eq("simple string"))).thenReturn(mockSimpleLiteral); + lenient().when(mockValueFactory.createLiteral(eq("hello"), eq("en"))).thenReturn(mockLangLiteral); + lenient().when(mockValueFactory.createLiteral(eq("123"), any(IRI.class))).thenReturn(mockTypedLiteral); + lenient().when(mockValueFactory.createLiteral(eq("literal with \"quotes\" and \n newline"))).thenReturn(mockEscapedLiteral); + } + + @Test + @DisplayName("Test get RDF format returns NTRIPLES") + void testGetRDFFormat() { + assertEquals(RDFFormat.NTRIPLES, parser.getRDFFormat()); + } + + @Test + @DisplayName("Test parsing a basic triple with IRIs") + void testParseBasicTriple() throws ParsingErrorException { + String ntriple = " ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple with a blank node subject") + void testParseBlankNodeSubject() throws ParsingErrorException { + String ntriple = "_:sub1 ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectBNode, mockPredicateIRI, mockObjectIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple with a blank node object") + void testParseBlankNodeObject() throws ParsingErrorException { + String ntriple = " _:obj1 ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectBNode); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple with a simple literal object") + void testParseSimpleLiteralObject() throws ParsingErrorException { + String ntriple = " \"simple string\" ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockSimpleLiteral); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple with a language-tagged literal object") + void testParseLangLiteralObject() throws ParsingErrorException { + String ntriple = " \"hello\"@en ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockLangLiteral); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("Test parsing a triple with a typed literal object") + void testParseTypedLiteralObject() throws ParsingErrorException { + String ntriple = " \"123\"^^ ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockTypedLiteral); + verifyNoMoreInteractions(mockModel); + } + + + @Test + @DisplayName("Test parsing a triple with escaped characters in literal") + void testParseEscapedLiteral() throws ParsingErrorException { + String ntriple = " \"literal with \\\"quotes\\\" and \\n newline\" ."; + parser.parse(new StringReader(ntriple)); + + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockEscapedLiteral); + verifyNoMoreInteractions(mockModel); + } + + + @Test + @DisplayName("Test parsing a triple with Unicode escape in literal (\\uXXXX)") + void testParseUnicodeEscapeLiteralUxxxx() throws ParsingErrorException { + String ntriple = " \"Hello\\u0020World\" ."; + + Literal expectedLiteral = mock(Literal.class); + lenient().when(mockValueFactory.createLiteral(eq("Hello World"))).thenReturn(expectedLiteral); + + parser.parse(new StringReader(ntriple)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, expectedLiteral); + } + + @Test + @DisplayName("Test parsing a triple with Unicode escape in literal (\\UXXXXXXXX)") + void testParseUnicodeEscapeLiteral() throws ParsingErrorException { + String ntriple = " \"Euro\\U000020AC\" ."; + + Literal expectedLiteral = mock(Literal.class); + lenient().when(mockValueFactory.createLiteral(eq("Euro€"))).thenReturn(expectedLiteral); + + parser.parse(new StringReader(ntriple)); + verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, expectedLiteral); + } + + @Test + @DisplayName("Test parsing a triple with Unicode escape in IRI (\\uXXXX)") + void testParseUnicodeEscapeIRIUxxxx() throws ParsingErrorException { + String ntriple = " ."; + IRI expectedSubjectIRI = mock(IRI.class); + lenient().when(mockValueFactory.createIRI(eq("http://example.org/s ubject"))).thenReturn(expectedSubjectIRI); + + parser.parse(new StringReader(ntriple)); + verify(mockModel).add(expectedSubjectIRI, mockPredicateIRI, mockObjectIRI); + } + + @Test + @DisplayName("Test parsing a triple with Unicode escape in IRI (\\UXXXXXXXX)") + void testParseUnicodeEscapeIRIU() throws ParsingErrorException { + String ntriple = " ."; + IRI expectedSubjectIRI = mock(IRI.class); + lenient().when(mockValueFactory.createIRI(eq("http://example.org/path/€"))).thenReturn(expectedSubjectIRI); + + parser.parse(new StringReader(ntriple)); + verify(mockModel).add(expectedSubjectIRI, mockPredicateIRI, mockObjectIRI); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListenerTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListenerTest.java new file mode 100644 index 000000000..b573df6d1 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/ntriples/NTriplesListenerTest.java @@ -0,0 +1,280 @@ +package fr.inria.corese.core.next.impl.io.parser.ntriples; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.impl.parser.antlr.NTriplesParser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the NTriplesListenerImpl 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. + */ +@ExtendWith(MockitoExtension.class) +class NTriplesListenerTest { + + @Mock + private Model mockModel; + + @Mock + private ValueFactory mockValueFactory; + + @Mock + private IOOptions mockIOOptions; + + private NTriplesListener listener; + + @Mock + private IRI mockIRI; + @Mock + private BNode mockBNode; + @Mock + private Literal mockLiteral; + @Mock + private IRI mockDatatypeIRI; + + @BeforeEach + void setUp() { + listener = new NTriplesListener(mockModel, mockValueFactory, mockIOOptions); + + lenient().when(mockValueFactory.createIRI(anyString())).thenAnswer(invocation -> { + String uri = invocation.getArgument(0); + if (uri.equals("http://example.org/test")) return mockIRI; + if (uri.equals("http://example.org/datatype")) return mockDatatypeIRI; + if (uri.equals("http://example.org/escaped>iri")) return mock(IRI.class); + if (uri.equals("http://example.org/s ubject")) return mock(IRI.class); + if (uri.equals("http://example.org/path/€")) return mock(IRI.class); + return mock(IRI.class); + }); + + lenient().when(mockValueFactory.createBNode(anyString())).thenAnswer(invocation -> { + String label = invocation.getArgument(0); + if (label.equals("b1")) return mockBNode; + return mock(BNode.class); + }); + + lenient().when(mockValueFactory.createLiteral(anyString())).thenAnswer(invocation -> { + String value = invocation.getArgument(0); + if (value.equals("test literal")) return mockLiteral; + if (value.equals("literal with \"quotes\" and \n newline")) return mock(Literal.class); + if (value.equals("Hello World")) return mock(Literal.class); + if (value.equals("Euro€")) return mock(Literal.class); + return mock(Literal.class); + }); + lenient().when(mockValueFactory.createLiteral(anyString(), any(IRI.class))).thenReturn(mock(Literal.class)); + lenient().when(mockValueFactory.createLiteral(anyString(), anyString())).thenReturn(mock(Literal.class)); + } + + private TerminalNode mockTerminalNode(String text) { + TerminalNode node = mock(TerminalNode.class); + when(node.getText()).thenReturn(text); + return node; + } + + private T mockRuleContext(Class clazz) { + return mock(clazz); + } + + @Test + @DisplayName("enterTriple and exitTriple should add a triple to the model") + void testEnterExitTripleAddsToModel() { + NTriplesParser.TripleContext tripleCtx = mockRuleContext(NTriplesParser.TripleContext.class); + NTriplesParser.SubjectContext subjectCtx = mockRuleContext(NTriplesParser.SubjectContext.class); + NTriplesParser.PredicateContext predicateCtx = mockRuleContext(NTriplesParser.PredicateContext.class); + NTriplesParser.ObjectContext objectCtx = mockRuleContext(NTriplesParser.ObjectContext.class); + + TerminalNode subjectIriRef = mockTerminalNode(""); + when(subjectCtx.IRIREF()).thenReturn(subjectIriRef); + + TerminalNode predicateIriRef = mockTerminalNode(""); + when(predicateCtx.IRIREF()).thenReturn(predicateIriRef); + + TerminalNode objectIriRef = mockTerminalNode(""); + when(objectCtx.IRIREF()).thenReturn(objectIriRef); + + when(tripleCtx.subject()).thenReturn(subjectCtx); + when(tripleCtx.predicate()).thenReturn(predicateCtx); + when(tripleCtx.object()).thenReturn(objectCtx); + + IRI actualSubjectIRI = mock(IRI.class); + IRI actualPredicateIRI = mock(IRI.class); + IRI actualObjectIRI = mock(IRI.class); + when(mockValueFactory.createIRI("http://example.org/subject")).thenReturn(actualSubjectIRI); + when(mockValueFactory.createIRI("http://example.org/predicate")).thenReturn(actualPredicateIRI); + when(mockValueFactory.createIRI("http://example.org/object")).thenReturn(actualObjectIRI); + + + listener.enterTriple(tripleCtx); + listener.exitTriple(tripleCtx); + + verify(mockModel).add(actualSubjectIRI, actualPredicateIRI, actualObjectIRI); + verifyNoMoreInteractions(mockModel); + } + + @Test + @DisplayName("extractSubject should return IRI for IRIREF context") + void testExtractSubjectIRI() { + NTriplesParser.SubjectContext ctx = mockRuleContext(NTriplesParser.SubjectContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + Resource result = listener.extractSubject(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractSubject should return BNode for BLANK_NODE_LABEL context") + void testExtractSubjectBNode() { + NTriplesParser.SubjectContext ctx = mockRuleContext(NTriplesParser.SubjectContext.class); + TerminalNode bNodeLabel = mockTerminalNode("_:b1"); + when(ctx.BLANK_NODE_LABEL()).thenReturn(bNodeLabel); + + Resource result = listener.extractSubject(ctx); + assertEquals(mockBNode, result); + verify(mockValueFactory).createBNode("b1"); + } + + @Test + @DisplayName("extractPredicate should return IRI for IRIREF context") + void testExtractPredicateIRI() { + NTriplesParser.PredicateContext ctx = mockRuleContext(NTriplesParser.PredicateContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + IRI result = listener.extractPredicate(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractObject should return IRI for IRIREF context") + void testExtractObjectIRI() { + NTriplesParser.ObjectContext ctx = mockRuleContext(NTriplesParser.ObjectContext.class); + TerminalNode iriRef = mockTerminalNode(""); + when(ctx.IRIREF()).thenReturn(iriRef); + + Value result = listener.extractObject(ctx); + assertEquals(mockIRI, result); + verify(mockValueFactory).createIRI("http://example.org/test"); + } + + @Test + @DisplayName("extractObject should return BNode for BLANK_NODE_LABEL context") + void testExtractObjectBNode() { + NTriplesParser.ObjectContext ctx = mockRuleContext(NTriplesParser.ObjectContext.class); + TerminalNode bNodeLabel = mockTerminalNode("_:b1"); + when(ctx.BLANK_NODE_LABEL()).thenReturn(bNodeLabel); + + Value result = listener.extractObject(ctx); + assertEquals(mockBNode, result); + verify(mockValueFactory).createBNode("b1"); + } + + @Test + @DisplayName("extractObject should return Literal for literal context (simple string)") + void testExtractObjectLiteralSimple() { + NTriplesParser.ObjectContext objCtx = mockRuleContext(NTriplesParser.ObjectContext.class); + NTriplesParser.LiteralContext litCtx = mockRuleContext(NTriplesParser.LiteralContext.class); + TerminalNode stringLiteral = mockTerminalNode("\"test literal\""); + + when(objCtx.literal()).thenReturn(litCtx); + when(litCtx.STRING_LITERAL_QUOTE()).thenReturn(stringLiteral); + when(litCtx.IRIREF()).thenReturn(null); + when(litCtx.LANGTAG()).thenReturn(null); + + Value result = listener.extractObject(objCtx); + assertEquals(mockLiteral, result); + verify(mockValueFactory).createLiteral("test literal"); + } + + @Test + @DisplayName("extractObject should return Literal for literal context (language-tagged)") + void testExtractObjectLiteralLang() { + NTriplesParser.ObjectContext objCtx = mockRuleContext(NTriplesParser.ObjectContext.class); + NTriplesParser.LiteralContext litCtx = mockRuleContext(NTriplesParser.LiteralContext.class); + TerminalNode stringLiteral = mockTerminalNode("\"hello\""); + TerminalNode langTag = mockTerminalNode("@en"); + + when(objCtx.literal()).thenReturn(litCtx); + when(litCtx.STRING_LITERAL_QUOTE()).thenReturn(stringLiteral); + when(litCtx.IRIREF()).thenReturn(null); + when(litCtx.LANGTAG()).thenReturn(langTag); + + Literal expectedLiteral = mock(Literal.class); + when(mockValueFactory.createLiteral("hello", "en")).thenReturn(expectedLiteral); + + Value result = listener.extractObject(objCtx); + assertEquals(expectedLiteral, result); + verify(mockValueFactory).createLiteral("hello", "en"); + } + + + @Test + @DisplayName("unescapeLiteral should throw IllegalArgumentException for invalid \\uXXXX") + void testUnescapeLiteralInvalidUx() throws NoSuchMethodException { + String input = "\"Invalid\\uXXXX\""; + java.lang.reflect.Method method = NTriplesListener.class.getDeclaredMethod("unescapeLiteral", String.class); + method.setAccessible(true); + + + assertThrows(IllegalArgumentException.class, + () -> listener.unescapeLiteral(input), + "Should throw unescapeLiteral should throw IllegalArgumentException for invalid \\uXXXX"); + } + + @Test + @DisplayName("unescapeLiteral should throw IllegalArgumentException for invalid \\UXXXXXXXX") + void testUnescapeLiteralInvalid() throws NoSuchMethodException { + String input = "\"Invalid\\U0000XXX\""; + java.lang.reflect.Method method = NTriplesListener.class.getDeclaredMethod("unescapeLiteral", String.class); + method.setAccessible(true); + + + assertThrows(IllegalArgumentException.class, + () -> listener.unescapeLiteral(input), + "Should throw unescapeLiteral should throw IllegalArgumentException for invalid \\UXXXXXXXX"); + } + + + @Test + @DisplayName("unescapeUri should handle basic escape sequences") + void testUnescapeUriBasicEscapes() throws NoSuchMethodException, java.lang.reflect.InvocationTargetException, IllegalAccessException { + + String input = "http://example.org/path\\>with\\