Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e3bc2b
Add JSON-LD serializer implementation
remiceres Aug 29, 2025
0dfc455
Integrate JSON-LD format into DefaultSerializerFactory
remiceres Aug 29, 2025
bfc5249
Add Turtle round-trip circular tests
remiceres Aug 29, 2025
a24df80
Add N-Triples round-trip circular tests
remiceres Aug 29, 2025
d60c46e
Add N-Quads round-trip circular tests
remiceres Aug 29, 2025
2d4f2fa
Add TriG round-trip circular tests
remiceres Aug 29, 2025
8e4fce0
Add RDF/XML round-trip circular tests (disabled)
remiceres Aug 29, 2025
5e787b2
Add JSON-LD round-trip circular tests
remiceres Aug 29, 2025
d01ba9e
TriG Named Graph Round-Trip Test Failures
abdessamad-abdoun Sep 1, 2025
6e82069
removing ignore tag
prbblrypier Sep 12, 2025
bf92bea
adding qname expension for datatype uri
prbblrypier Sep 12, 2025
d9be21d
1 - bnode correction in serializer
prbblrypier Sep 16, 2025
5eb13b0
1 - bnode correction in serializer
prbblrypier Sep 16, 2025
7136ddf
1 - bnode correction in serializer
prbblrypier Sep 16, 2025
760dc6f
1 - bnode correction in serializer
prbblrypier Sep 16, 2025
f91adbe
Merge remote-tracking branch 'origin/fix/turtle_round_trip_test_failu…
prbblrypier Sep 16, 2025
39e694d
Fixing turtle backslash duplication in serializer
MaillPierre Sep 23, 2025
0272190
Merge pull request #208 from corese-stack/fix/turtle_round_trip_test_…
MaillPierre Sep 23, 2025
aef3fe2
removing ignore tag
prbblrypier Sep 12, 2025
73ecec5
adding qname expension for datatype uri
prbblrypier Sep 12, 2025
4f0cd30
Merge remote-tracking branch 'origin/feature/185-parser-serializer-ro…
MaillPierre Sep 29, 2025
a2d0fdc
fixing blank node serialization
MaillPierre Sep 30, 2025
8e62438
fix default graph duplication
MaillPierre Sep 30, 2025
f2c789c
fixing lang tagged literal test
MaillPierre Sep 30, 2025
8d3f28d
Merge pull request #210 from corese-stack/feature/201_JSON-LDRound-Tr…
MaillPierre Oct 1, 2025
91f824f
fix the old test that was incorrect
MaillPierre Oct 1, 2025
337bbba
Merge branch 'fix/json_serializer_test_bn_and_default_graph' into fea…
MaillPierre Oct 1, 2025
b9fff29
removing ignore tag
prbblrypier Sep 12, 2025
2f858ca
adding qname expension for datatype uri
prbblrypier Sep 12, 2025
3583e39
Merge remote-tracking branch 'origin/feature/185-parser-serializer-ro…
MaillPierre Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ private void handleEndElement(String uri, String localName, String qName) {
IRI predicate = ctx.predicateStack.pop();
Resource subject = ctx.subjectStack.peek();
String datatypeUri = ctx.datatypeStack.isEmpty() ? null : ctx.datatypeStack.pop();
//emitLiteral(subject, predicate, text, datatypeUri);
String lang = ctx.langStack.isEmpty() ? null : ctx.langStack.peek();
emitter.emitLiteral(subject, predicate, text, datatypeUri, lang);
return;
Expand Down Expand Up @@ -213,7 +212,8 @@ private void updateLang(Attributes attrs) {
private void updateDatatype(Attributes attrs) {
String datatype = attrs.getValue(RDF.type.getNamespace(), "datatype");
if (datatype != null) {
ctx.datatypeStack.push(datatype);
String expanded = expandQNameFromQName(datatype);
ctx.datatypeStack.push(expanded);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ public static Optional<XSD> resolveDatatype(String datatypeUri) {
return Optional.empty();
}

/**
* Expands a QName string (e.g. "xsd:integer") into a full URI if known.
* Currently supports "xsd:" → XML Schema namespace.
*
* @param qname the QName string
* @return expanded full URI if a known prefix, otherwise returns qname unchanged
*/
public static String expandQNameFromQName(String qname) {
if (qname == null) return null;
if (qname.startsWith("xsd:")) {
return "http://www.w3.org/2001/XMLSchema#" + qname.substring("xsd:".length());
}
return qname;
}

/**
* Extracts a subject resource from RDF/XML attributes.
* Supports rdf:about, rdf:nodeID, rdf:ID.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,17 @@ private String resolveIRI(String raw) {
* @return the stripped string
*/
private String stripQuotes(String text) {
if (text == null || text.length() < 2)
if (text == null || text.length() < 2) {
return text;
}
if (text.startsWith("\"\"\"") && text.endsWith("\"\"\"") && text.length() >= 6) {
return text.substring(3, text.length() - 3);
}
if (text.startsWith("'''") && text.endsWith("'''") && text.length() >= 6) {
return text.substring(3, text.length() - 3);
}
if ((text.startsWith("\"") && text.endsWith("\"")) ||
(text.startsWith("'''") && text.endsWith("'''")) ||
(text.startsWith("\"\"\"") && text.endsWith("\"\"\""))) {
(text.startsWith("'") && text.endsWith("'"))) {
return text.substring(1, text.length() - 1);
}
return text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,18 +322,23 @@ protected void writeValue(Writer writer, Value value) throws IOException {
currentlyWritingBlankNodes.add(bNode);

boolean handled = false;
if (option instanceof AbstractSerializerOption && getTFamilyOption().useCollections() && bNode.isBNode()) {
handled = writeRDFList(writer, bNode);
}

if (!handled && option instanceof AbstractSerializerOption && getTFamilyOption().getBlankNodeStyle() == BlankNodeStyleEnum.ANONYMOUS && bNode.isBNode()) { // getBlankNodeStyle is on AbstractTFamilyConfig
List<Statement> properties = model.stream()
.filter(stmt -> stmt.getSubject().equals(bNode))
.toList();
boolean isSubject = model.stream().anyMatch(stmt -> stmt.getSubject().equals(bNode));

if (!isSubject && option instanceof AbstractSerializerOption) {
if (getTFamilyOption().useCollections() && bNode.isBNode()) {
handled = writeRDFList(writer, bNode);
}

if (!handled && getTFamilyOption().getBlankNodeStyle() == BlankNodeStyleEnum.ANONYMOUS && bNode.isBNode()) {
List<Statement> properties = model.stream()
.filter(stmt -> stmt.getSubject().equals(bNode))
.toList();

if (!properties.isEmpty()) {
writeInlineBlankNode(writer, properties);
handled = true;
if (!properties.isEmpty()) {
writeInlineBlankNode(writer, properties);
handled = true;
}
}
}

Expand All @@ -343,10 +348,12 @@ protected void writeValue(Writer writer, Value value) throws IOException {

currentlyWritingBlankNodes.remove(bNode);
} else {
throw new IllegalArgumentException("Unsupported value type for " + getFormatName() + " serialization: " + value.getClass().getName());
throw new IllegalArgumentException("Unsupported value type for " + getFormatName() +
" serialization: " + value.getClass().getName());
}
}


/**
* Writes an {@link IRI} to the writer.
* Attempts to use a prefixed name if possible, otherwise writes the full IRI in angle brackets.
Expand Down Expand Up @@ -661,48 +668,8 @@ protected Set<Resource> precomputeInlineBlankNodesAndLists() {
if (stmt.getSubject().isBNode()) {
Resource bNodeSubject = stmt.getSubject();
if (tFamilyConfig.useCollections() && isRDFListHead(bNodeSubject)) {
Resource current = bNodeSubject;
Set<Resource> listNodes = new HashSet<>();
Set<Resource> visitedInPrecomp = new HashSet<>();
boolean isList = true;
while (current != null && current.isBNode() && !visitedInPrecomp.contains(current)) {
visitedInPrecomp.add(current);
listNodes.add(current);
final Resource finalCurrentForLambda = current;
List<Statement> listProps = model.stream()
.filter(s -> s.getSubject().equals(finalCurrentForLambda))
.toList();

if (listProps.size() != 2) {
isList = false;
break;
}

Optional<Value> first = listProps.stream()
.filter(s -> s.getPredicate().stringValue().equals(SerializationConstants.RDF_FIRST))
.map(Statement::getObject)
.findFirst();

Optional<Value> rest = listProps.stream()
.filter(s -> s.getPredicate().stringValue().equals(SerializationConstants.RDF_REST))
.map(Statement::getObject)
.findFirst();

if (!first.isPresent() || !rest.isPresent()) {
isList = false;
break;
}

if (rest.get().stringValue().equals(SerializationConstants.RDF_NIL)) {
current = null;
} else if (rest.get().isBNode()) {
current = (Resource) rest.get();
} else {
isList = false;
break;
}
}
if (isList && current == null) {
Set<Resource> listNodes = detectListNodes(bNodeSubject);
if (!listNodes.isEmpty()) {
precomputed.addAll(listNodes);
}
}
Expand All @@ -716,7 +683,10 @@ protected Set<Resource> precomputeInlineBlankNodesAndLists() {
s.getPredicate().stringValue().equals(SerializationConstants.RDF_REST)
);

if (!properties.isEmpty() && !isPartOfList) {
boolean usedAsTopLevelSubject = model.stream()
.anyMatch(s -> s.getSubject().equals(bNodeSubject));

if (!properties.isEmpty() && !isPartOfList && !usedAsTopLevelSubject) {
precomputed.add(bNodeSubject);
}
}
Expand All @@ -725,6 +695,55 @@ protected Set<Resource> precomputeInlineBlankNodesAndLists() {
return precomputed;
}

/**
* Traverses an RDF list starting from the given head and collects all list nodes.
*
* @param head the blank node that may be the head of an RDF list
* @return the set of blank nodes that form the list, or an empty set if not a valid list
*/
private Set<Resource> detectListNodes(Resource head) {
Set<Resource> listNodes = new HashSet<>();
Set<Resource> visited = new HashSet<>();
Resource current = head;

while (current != null && current.isBNode() && !visited.contains(current)) {
visited.add(current);
listNodes.add(current);

final Resource finalCurrent = current;
List<Statement> props = model.stream()
.filter(s -> s.getSubject().equals(finalCurrent))
.toList();

if (props.size() != 2) {
return Collections.emptySet();
}

Optional<Value> first = props.stream()
.filter(s -> s.getPredicate().stringValue().equals(SerializationConstants.RDF_FIRST))
.map(Statement::getObject)
.findFirst();

Optional<Value> rest = props.stream()
.filter(s -> s.getPredicate().stringValue().equals(SerializationConstants.RDF_REST))
.map(Statement::getObject)
.findFirst();

if (!first.isPresent() || !rest.isPresent()) {
return Collections.emptySet();
}

if (rest.get().stringValue().equals(SerializationConstants.RDF_NIL)) {
current = null;
} else if (rest.get().isBNode()) {
current = (Resource) rest.get();
} else {
return Collections.emptySet();
}
}
return current == null ? listNodes : Collections.emptySet();
}

/**
* Checks if a given blank node is the head of an RDF list.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Adapter class from Model to RdfDataset for usage in the JSON-LD serialization process using the titanium library.
* @see <a href="https://github.com/filip26/titanium-rdf-api">Titanium RDF API</a>
*/
public class TitaniumRDFDatasetSerializationAdapter implements RdfDataset {

private final static Logger logger = LoggerFactory.getLogger(TitaniumRDFDatasetSerializationAdapter.class);
private Model model;

/**
Expand All @@ -58,17 +61,18 @@ public TitaniumRDFDatasetSerializationAdapter(Model model) {

@Override
public RdfGraph getDefaultGraph() {
return new RdfGraph() {
RdfGraph resultGraph = new RdfGraph() {
@Override
public boolean contains(RdfTriple triple) {
return model.contains(toResource(triple.getSubject()), toIRI(triple.getPredicate()), toValue(triple.getObject()));
return model.contains(toResource(triple.getSubject()), toIRI(triple.getPredicate()), toValue(triple.getObject()), (Resource) null);
}

@Override
public List<RdfTriple> toList() {
return model.stream().map(TitaniumRDFDatasetSerializationAdapter.this::toRdfTriple).toList();
return model.filter(null, null, null, (Resource) null).stream().map(TitaniumRDFDatasetSerializationAdapter.this::toRdfTriple).toList();
}
};
return resultGraph;
}

@Override
Expand Down Expand Up @@ -187,27 +191,15 @@ public RdfValue getObject() {
* @return the converted resource
*/
private RdfResource toRdfResource(Resource resource) {
if (resource != null && (! (resource.isBNode() || resource.isIRI()))) {
throw new SerializationException("Unknown resource type " + resource, "JSON-LD");
} else if (resource == null) {
if (resource == null) {
return null;
} else if (resource.isIRI()) {
return toRdfIRI((IRI) resource);
} else if (resource.isBNode()) {
return toRdfBlankNode((BNode) resource);
} else {
throw new SerializationException("Unknown resource type " + resource, "JSON-LD");
}
return new RdfResource() {
@Override
public boolean isIRI() {
return resource.isIRI();
}

@Override
public boolean isBlankNode() {
return resource.isBNode();
}

@Override
public String getValue() {
return resource.stringValue();
}
};
}

/**
Expand Down Expand Up @@ -258,7 +250,7 @@ public boolean isBlankNode() {
}
@Override
public String getValue() {
return bnode.stringValue();
return "_:" + bnode.stringValue();
}
};
}
Expand Down Expand Up @@ -290,7 +282,7 @@ public String getDatatype() {
) {
return literal.getDatatype().stringValue();
} else if (literal.getLanguage().isPresent()) {
return RDF.langString.getIRI().stringValue();
return "rdf:langString"; // Titanium JSONLD expect the langstring datatype to be in this format ...
} else {
return XSD.xsdString.getIRI().stringValue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,20 @@ protected String escapeLiteralString(String value) {
case '"':
sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.QUOTE);
break;
case '\\':
sb.append(SerializationConstants.BACK_SLASH).append(SerializationConstants.BACK_SLASH);
break;
default:
if (option.escapeUnicode() && (c <= 0x1F || c == 0x7F || (c >= 0x80 && c <= 0xFFFF))) {
if (Character.isISOControl(c) ||c == 0x7F) {
sb.append(String.format("\\u%04X", (int) c));
}
else if (option.escapeUnicode() && c >= 0x80 && c <= 0xFFFF) {
sb.append(String.format("\\u%04X", (int) c));
} else if (Character.isHighSurrogate(c)) {
int codePoint = value.codePointAt(i);
if (Character.isValidCodePoint(codePoint)) {
sb.append(String.format("\\U%08X", codePoint));
if (option.escapeUnicode()) {
sb.append(String.format("\\U%08X", codePoint));
} else {
sb.append(Character.toChars(codePoint));
}
i++;
} else {
sb.append(c);
Expand All @@ -178,6 +182,7 @@ protected String escapeMultilineLiteralString(String value) {
SerializationConstants.BACK_SLASH + SerializationConstants.QUOTE +
SerializationConstants.BACK_SLASH + SerializationConstants.QUOTE +
SerializationConstants.BACK_SLASH + SerializationConstants.QUOTE);

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import fr.inria.corese.core.next.impl.io.serialization.DefaultSerializerFactory;
import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory;
import fr.inria.corese.core.next.impl.temp.CoreseModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Circular tests for JSON-LD parser and serializer integration.
Expand All @@ -42,6 +44,8 @@
@DisplayName("JSON-LD Circular Integration Tests")
class JSONLDCircularTest {

private static final Logger logger = LoggerFactory.getLogger(JSONLDCircularTest.class);

private ValueFactory valueFactory;
private SerializerFactory serializerFactory;
private ParserFactory parserFactory;
Expand Down Expand Up @@ -69,7 +73,8 @@ void setUp() {
valueFactory = new CoreseAdaptedValueFactory();
serializerFactory = new DefaultSerializerFactory();
parserFactory = new ParserFactory();
defaultConfig = new TitaniumJSONLDProcessorOption.Builder().build();
defaultConfig = new TitaniumJSONLDProcessorOption.Builder()
.build();
}

/**
Expand Down Expand Up @@ -261,7 +266,7 @@ private Model createSpecialCharactersTestModel() {
private Model performRoundTrip(Model originalModel) throws Exception {
// Serialize to JSON-LD
RDFSerializer serializer = serializerFactory.createSerializer(
RDFFormat.JSONLD, originalModel, (SerializationOption) defaultConfig);
RDFFormat.JSONLD, originalModel, defaultConfig);

StringWriter writer = new StringWriter();
serializer.write(writer);
Expand Down
Loading