All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zone.cogni.semanticz.jsonldshaper.Rdf2JsonLd Maven / Gradle / Ivy

package zone.cogni.semanticz.jsonldshaper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.github.jsonldjava.core.JsonLdOptions;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.riot.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import zone.cogni.semanticz.jsonldshaper.utils.RdfUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Rdf2JsonLd {

    private static final Logger log = LoggerFactory.getLogger(Rdf2JsonLd.class);

    public static RDFWriterBuilder calculateJsonldWriter(Model shacl) {
        return calculateJsonldWriter(shacl, true);
    }

    public static RDFWriterBuilder calculateJsonldWriter(Model shacl, boolean adaptDefaultNamespace) {
        String jsonLdContext = calculateJsonldContextNode(shacl, adaptDefaultNamespace).toString();
        JsonLDWriteContext context = new JsonLDWriteContext();
        context.setJsonLDContext(jsonLdContext);
        setContextOptions(context);

        return RDFWriter.create()
                        .format(RDFFormat.JSONLD_COMPACT_PRETTY)
                        .context(context);
    }

    private static void setContextOptions(JsonLDWriteContext context) {
        JsonLdOptions opts = new JsonLdOptions();

        opts.setCompactArrays(false);
        opts.setUseNativeTypes(false);
        opts.setUseRdfType(false);

        context.setOptions(opts);
    }

    private static ObjectNode calculateJsonldContextNode(Model shacl, boolean adaptDefaultNamespace) {
        ObjectNode contextNode = JsonNodeFactory.instance.objectNode();

        addDatatypeProperties(shacl, contextNode);
        addResourceProperties(shacl, contextNode);
        addClassProperties(shacl, contextNode);
        addPrefixElements(shacl, contextNode, adaptDefaultNamespace);
        addCommonContextElements(contextNode);

        return contextNode;
    }

    private static void addPrefixElements(Model shacl, ObjectNode contextNode, boolean adaptDefaultNamespace) {
        shacl.getNsPrefixMap()
             .forEach((prefix, namespace) -> {
                 if (StringUtils.isEmpty(prefix) && adaptDefaultNamespace) {
                     contextNode.set("@vocab", getTextNode(namespace));
                 } else {
                     contextNode.set(prefix, getTextNode(namespace));
                 }
             });
    }

    private static void addCommonContextElements(ObjectNode contextNode) {
        contextNode.set("data", getTextNode("@graph"));
    }

    private static List getClassProperties(Model shacl) {
        String propertiesQuery = getClassPropertiesQuery(shacl);
        String inversePropertiesQuery = getClassInversePropertiesQuery(shacl);

        return Stream.of(propertiesQuery, inversePropertiesQuery)
                     .map(query -> getShortUriReferences(shacl, query))
                     .flatMap(Collection::stream)
                     .distinct()
                     .collect(Collectors.toList());
    }

    private static String getClassInversePropertiesQuery(Model shacl) {
        StringBuilder builder = new StringBuilder();
        appendPrefixedToQuery(shacl, builder);
        builder.append("\n" +
                "select ?property { \n" +
                "  ?s sh:path [sh:inversePath ?property]. \n" +
                "  filter exists { \n" +
                "    ?s sh:class ?class. \n" +
                "    filter (?class not in (sh:and, sh:not, sh:or, sh:xone)) \n" +
                "  }\n" +
                "}" +
                "order by ?property");
        return builder.toString();
    }

    private static String getClassPropertiesQuery(Model shacl) {
        StringBuilder builder = new StringBuilder();
        appendPrefixedToQuery(shacl, builder);
        builder.append("\n" +
                "select ?property { \n" +
                "  ?s sh:path ?property. \n" +
                "  filter(!isBlank(?property)). \n" +
                "  filter exists { \n" +
                "    ?s sh:class ?class. \n" +
                "    filter (?class not in (sh:and, sh:not, sh:or, sh:xone)) \n" +
                "  }\n" +
                "}" +
                "order by ?property");
        return builder.toString();
    }

    private static void addClassProperties(Model shacl, ObjectNode contextNode) {
        addShortUriReference(contextNode, shacl, getClassProperties(shacl));
    }

    private static void addResourceProperties(Model shacl, ObjectNode contextNode) {
        addShortUriReference(contextNode, shacl, getResourceProperties(shacl));
    }

    private static List getResourceProperties(Model shacl) {
        String query = getResourcePropertiesQuery(shacl);
        return getShortUriReferences(shacl, query);
    }

    private static void addShortUriReference(ObjectNode contextNode,
                                             Model shacl,
                                             List shortUriReferences) {
        shortUriReferences
                .forEach(classProperty -> {
                    String curie = shacl.shortForm(classProperty);
                    ObjectNode classPropertyNode = JsonNodeFactory.instance.objectNode();
                    classPropertyNode.set("@id", getTextNode(curie));
                    classPropertyNode.set("@type", getTextNode("@id"));

                    contextNode.set(curie, classPropertyNode);
                });
    }

    private static List getShortUriReferences(Model shacl, String query) {
        List> rows = RdfUtils.convertToListOfMaps(RdfUtils.select(shacl, query));
        return RdfUtils.convertSingleColumnUriToStringList(rows);
    }

    private static String getResourcePropertiesQuery(Model shacl) {
        StringBuilder builder = new StringBuilder();
        appendPrefixedToQuery(shacl, builder);
        builder.append("\n" +
                "select ?property { \n" +
                "  ?s sh:path ?property. \n" +
                "  filter(isIRI(?property)) \n" + // TODO: handle blank nodes (inverse path)
                "  ?s sh:nodeKind sh:IRI. \n" +
                "  filter not exists { \n" +
                "    ?s sh:class ?class. \n" +
                "  }\n" +
                "}");
        return builder.toString();
    }

    private static void addDatatypeProperties(Model shacl, ObjectNode contextNode) {
        getDatatypeProperties(shacl)
                .forEach(tuple -> {
                    String property = tuple.getLeft();
                    String datatype = tuple.getRight();

                    if (StringUtils.isEmpty(property)) return;

                    String curie = shacl.shortForm(property);
                    ObjectNode classPropertyNode = JsonNodeFactory.instance.objectNode();
                    classPropertyNode.set("@id", getTextNode(curie));

                    String curieDatatype = shacl.shortForm(datatype);
                    if (curieDatatype.equals("rdf:langString")) {
                        // langString gets a nested structure
                        classPropertyNode.set("@container", getTextNode("@language"));
                    } else if (!Set.of("xsd:string", "xsd:integer", "xsd:boolean").contains(curieDatatype)) {
                        // note: this seems not to be required! else we get some weird output
                        classPropertyNode.set("@type", getTextNode(curieDatatype));
                    }

                    contextNode.set(curie, classPropertyNode);
                });

    }

    private static List> getDatatypeProperties(Model model) {
        String query = getDatatypePropertiesQuery(model);

        return RdfUtils.convertToListOfMaps(RdfUtils.select(model, query)).stream()
                       .map(Rdf2JsonLd::datatypeRowToTuple)
                       .collect(Collectors.toList());
    }

    private static Pair datatypeRowToTuple(Map row) {
        String property = row.get("property").asResource().getURI();
        String datatype = row.get("datatype").asResource().getURI();
        return Pair.of(property, datatype);
    }

    private static TextNode getTextNode(String curieDatatype) {
        return JsonNodeFactory.instance.textNode(curieDatatype);
    }

    private static String getDatatypePropertiesQuery(Model shacl) {
        StringBuilder builder = new StringBuilder();
        appendPrefixedToQuery(shacl, builder);
        builder.append("\n" +
                "select ?property ?datatype { \n" +
                "  ?s sh:path ?property. \n" +
                "  ?s sh:datatype ?datatype. \n" +
                // note: xsd:string cannot be added as a @type or it will not work as we want
                "  filter (?datatype not in (sh:and, sh:not, sh:or, sh:xone, xsd:string)) \n" +
                "}" +
                "order by ?property");
        return builder.toString();
    }

    private static void appendPrefixedToQuery(Model shacl, StringBuilder builder) {
        shacl.getNsPrefixMap()
             .forEach((prefix, namespace) -> {
                 builder.append("PREFIX ").append(prefix).append(": <").append(namespace).append("> \n");
             });
    }


    private static Model jsonLdToModel(InputStream jsonLdBytes) {
        Model result = ModelFactory.createDefaultModel();

        RDFParser.source(jsonLdBytes)
                 .forceLang(Lang.JSONLD)
                 .build()
                 .parse(result);

        if (result.isEmpty()) {
            throw new RuntimeException("Conversion from json-ld to model failed. Resulting model is empty.");
        }

        return result;
    }

    private static Model jsonLdToModel(JsonNode jsonLd) {
        ByteArrayInputStream inputStream = getInputStreamFromJson(jsonLd);
        return jsonLdToModel(inputStream);
    }

    private static ByteArrayInputStream getInputStreamFromJson(JsonNode jsonLd) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            byte[] bytes = mapper.writeValueAsBytes(jsonLd);

            return new ByteArrayInputStream(bytes);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Error while converting json to bytes", e);
        }
    }

    public static ObjectNode modelToJsonLd(Model data, Model shacl) {
        RDFWriterBuilder jsonLdWriter = Rdf2JsonLd.calculateJsonldWriter(shacl);
        return modelToJsonLd(data, jsonLdWriter);
    }

    public static ObjectNode modelToJsonLd(Model data, RDFWriterBuilder jsonLdWriter) {
        long start = System.nanoTime();
        ByteArrayOutputStream jsonLdBytes = new ByteArrayOutputStream();
        jsonLdWriter.source(data).output(jsonLdBytes);
        ObjectMapper mapper = new ObjectMapper();

        try {
            ObjectNode result = mapper.readValue(jsonLdBytes.toByteArray(), ObjectNode.class);

            if (log.isTraceEnabled())
                log.trace("Model to JSON-LD in {}ns", System.nanoTime() - start);
            return result;
        } catch (IOException e) {
            throw new RuntimeException("Couldn't parse json bytes: ", e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy