Skip to content

Apache Jena integration

This guide explains the functionalities of the jelly-jena module, which provides Jelly support for Apache Jena.

If you just want to add Jelly format support to Apache Jena / Apache Jena Fuseki, you can use the Jelly-JVM plugin JAR. See the dedicated guide for more information.

Base facilities

jelly-jena implements the eu.neverblink.jelly.core.JellyConverterFactory interface in eu.neverblink.jelly.convert.jena.JenaConverterFactory . This factory allows you to build encoders and decoders that convert between Jelly's RdfStreamFrames and Apache Jena's Triple and Quad objects. The eu.neverblink.jelly.core.proto.v1.RdfStreamFrame class is an object representation of Jelly's binary format.

The module also implements the eu.neverblink.jelly.core.utils.DatasetAdapter and eu.neverblink.jelly.core.utils.GraphAdapter interfaces in eu.neverblink.jelly.convert.jena.JenaAdapters . These adapters provide methods for Apache Jena's Model, Dataset, Graph, and DatasetGraph classes to convert them into an iterable of triples (GRAPH_ADAPTER.triples), quads (DATASET_ADAPTER.quads, MODEL_ADAPTER.quads), or named graphs (DATASET_ADAPTER.graphs, MODEL_ADAPTER.graphs). This is useful when working with Jelly on a lower level or when using the jelly-pekko-stream module.

Serialization and deserialization with RIOT

jelly-jena implements an RDF writer and reader for Apache Jena's RIOT library. This means you can use Jelly just like, for example, Turtle or RDF/XML. See the example below:

Example: JenaRiot.java (click to expand)

Source code on GitHub

JenaRiot.java
package eu.neverblink.jelly.examples;

import eu.neverblink.jelly.convert.jena.riot.*;
import eu.neverblink.jelly.core.*;
import eu.neverblink.jelly.core.proto.v1.RdfStreamOptions;
import eu.neverblink.jelly.examples.shared.Example;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URI;
import org.apache.jena.query.Dataset;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.riot.*;
import org.apache.jena.sparql.util.Context;

/**
 * Example of using Jelly's integration with Apache Jena's RIOT library for
 * writing and reading RDF graphs and datasets to/from disk.
 * <p>
 * See also: <a href="https://jena.apache.org/documentation/io/">Jena Documentation</a>
 */
public class JenaRiot implements Example {

    public static void main(String[] args) throws Exception {
        new JenaRiot().run(args);
    }

    @Override
    public void run(String[] args) throws Exception {
        // Load the RDF graph from an N-Triples file
        URI fileUri = new File(getClass().getResource("/weather.nt").toURI()).toURI();
        Model model = RDFDataMgr.loadModel(fileUri.toString());

        // Print the size of the model
        System.out.println("Loaded an RDF graph from N-Triples with size: " + model.size());

        // Write the model to a Jelly file using try-with-resources
        try (FileOutputStream out = new FileOutputStream("weather.jelly")) {
            // Note: by default this will use the JellyFormat.JELLY_SMALL_STRICT format variant
            RDFDataMgr.write(out, model, JellyLanguage.JELLY);
            System.out.println("Saved the model to a Jelly file");
        }

        // Load the RDF graph from a Jelly file
        Model model2 = RDFDataMgr.loadModel("weather.jelly", JellyLanguage.JELLY);

        // Print the size of the model
        System.out.println("Loaded an RDF graph from Jelly with size: " + model2.size());

        // ---------------------------------
        System.out.println("\n");

        // Try the same with an RDF dataset and some different settings
        URI trigFileUri = new File(getClass().getResource("/weather-graphs.trig").toURI()).toURI();
        Dataset dataset = RDFDataMgr.loadDataset(trigFileUri.toString());
        System.out.println(
            "Loaded an RDF dataset from a Trig file with " +
            dataset.asDatasetGraph().size() +
            " named graphs and " +
            dataset.asDatasetGraph().stream().count() +
            " quads"
        );

        try (FileOutputStream out = new FileOutputStream("weather-quads.jelly")) {
            // Write the dataset to a Jelly file, using the "BIG" settings
            // (better compression for big files, more memory usage)
            RDFDataMgr.write(out, dataset, JellyFormat.JELLY_BIG_STRICT);
            System.out.println("Saved the dataset to a Jelly file");
        }

        // Load the RDF dataset from a Jelly file
        Dataset dataset2 = RDFDataMgr.loadDataset("weather-quads.jelly", JellyLanguage.JELLY);
        System.out.println(
            "Loaded an RDF dataset from Jelly with " +
            dataset2.asDatasetGraph().size() +
            " named graphs and " +
            dataset2.asDatasetGraph().stream().count() +
            " quads"
        );

        // ---------------------------------
        System.out.println("\n");

        // Custom Jelly format – change any settings you like
        RDFFormat customFormat = new RDFFormat(
            JellyLanguage.JELLY,
            JellyFormatVariant.builder()
                .options(
                    JellyOptions.SMALL_STRICT.clone()
                        .setMaxPrefixTableSize(0) // disable the prefix table
                        .setStreamName("My weather stream") // add metadata to the stream
                )
                .frameSize(16) // make RdfStreamFrames with 16 rows each
                .build()
        );

        // Jena requires us to register the custom format – once for graphs and once for datasets,
        // as Jelly supports both.
        RDFWriterRegistry.register(customFormat, new JellyGraphWriterFactory());
        RDFWriterRegistry.register(customFormat, new JellyDatasetWriterFactory());

        try (FileOutputStream out = new FileOutputStream("weather-quads-custom.jelly")) {
            // Write the dataset to a Jelly file using the custom format
            RDFDataMgr.write(out, dataset, customFormat);
            System.out.println("Saved the dataset to a Jelly file with custom settings");
        }

        // Load the RDF dataset from a Jelly file with the custom format
        Dataset dataset3 = RDFDataMgr.loadDataset("weather-quads-custom.jelly", JellyLanguage.JELLY);
        System.out.println(
            "Loaded an RDF dataset from Jelly with custom settings with " +
            dataset3.asDatasetGraph().size() +
            " named graphs and " +
            dataset3.asDatasetGraph().stream().count() +
            " quads"
        );

        // ---------------------------------
        System.out.println("\n");

        // By default, the parser has limits on for example the maximum size of the lookup tables.
        // The default supported options are JellyOptions.defaultSupportedOptions.
        // You can change these limits by creating your own options object.
        RdfStreamOptions customOptions = JellyOptions.DEFAULT_SUPPORTED_OPTIONS.clone().setMaxNameTableSize(50); // set the maximum size of the name table to 50

        // Create a Context object with the custom options
        Context parserContext = RIOT.getContext().copy().set(JellyLanguage.SYMBOL_SUPPORTED_OPTIONS, customOptions);

        System.out.println("Trying to load the model with custom supported options...");
        Model model3 = ModelFactory.createDefaultModel();
        try {
            // The loading operation should fail because our allowed max name table size is too low
            RDFParser.create()
                .source("weather.jelly")
                .lang(JellyLanguage.JELLY)
                // Set the context object with the custom options
                .context(parserContext)
                .parse(model3);
        } catch (RdfProtoDeserializationError e) {
            // The stream uses a name table size of 128, which is larger than the maximum supported size of 50.
            // To read this stream, set maxNameTableSize to at least 128 in the supportedOptions for this decoder.
            System.out.println("Failed to load the model with custom options: " + e.getMessage());
        }
    }
}

Usage notes:

  • eu.neverblink.jelly.core.JellyOptions provides a few common presets for Jelly serialization options construct a JellyFormatVariant, as shown in the example above. You can also further customize the serialization options (e.g., dictionary size).
  • The RIOT writer (serializer) integration implements only the delimited variant of Jelly. It is used for writing Jelly to files on disk or sockets. Because of this, you cannot use RIOT to write non-delimited Jelly data (e.g., a single message to a Kafka stream). For this, you should use the jelly-pekko-stream module or the more low-level API: Low-level usage.
  • However, the RIOT parser (deserializer) integration will automatically detect if the parsed Jelly data is delimited or not. If it's non-delimited, the parser will assume that there is only one RdfStreamFrame in the file.
  • Jelly's parsers and writers are registered in the eu.neverblink.jelly.convert.jena.riot.JellyLanguage object (source code). This registration should happen automatically when you include the jelly-jena module in your project, using Jena's component initialization mechanism.

Streaming serialization with RIOT

jelly-jena also implements a streaming writer (StreamRDF API in Jena). Using it is similar to the regular RIOT writer, with a slightly different setup:

Example: JenaRiotStreaming.java (click to expand)

Source code on GitHub

JenaRiotStreaming.java
package eu.neverblink.jelly.examples;

import eu.neverblink.jelly.convert.jena.riot.*;
import eu.neverblink.jelly.core.JellyOptions;
import eu.neverblink.jelly.core.proto.v1.PhysicalStreamType;
import eu.neverblink.jelly.core.proto.v1.RdfStreamOptions;
import eu.neverblink.jelly.examples.shared.Example;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Objects;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFParser;
import org.apache.jena.riot.RIOT;
import org.apache.jena.riot.lang.StreamRDFCounting;
import org.apache.jena.riot.system.StreamRDF;
import org.apache.jena.riot.system.StreamRDFLib;
import org.apache.jena.riot.system.StreamRDFWriter;
import org.apache.jena.sparql.util.Context;

/**
 * Example of using Apache Jena's streaming IO API with Jelly.
 * <p>
 * See also: <a href="https://jena.apache.org/documentation/io/streaming-io.html">Jena Documentation</a>
 */
public class JenaRiotStreaming implements Example {

    public static void main(String[] args) throws Exception {
        new JenaRiotStreaming().run(args);
    }

    @Override
    public void run(String[] args) throws Exception {
        // Initialize a Jena StreamRDF to consume the statements
        StreamRDFCounting readerStream = StreamRDFLib.count();

        System.out.println("Reading a stream of triples from a Jelly file...");

        // Parse a Jelly file as a stream of triples
        File inputFileTriples = new File(
            Objects.requireNonNull(getClass().getResource("/jelly/weather.jelly")).toURI()
        );
        RDFParser.source(inputFileTriples.toURI().toString()).lang(JellyLanguage.JELLY).parse(readerStream);

        System.out.printf("Read %d triples%n", readerStream.countTriples());
        System.out.println();
        System.out.println("Reading a stream of quads from a Jelly file...");

        // Parse a different Jelly file as a stream of quads and send it to the same sink
        File inputFileQuads = new File(
            Objects.requireNonNull(getClass().getResource("/jelly/weather-quads.jelly")).toURI()
        );
        RDFParser.source(inputFileQuads.toURI().toString()).lang(JellyLanguage.JELLY).parse(readerStream);

        // Print the number of triples and quads
        //
        // The number of triples here is the sum of the triples from the first file and the triples
        // in the default graph of the second file. This is just how Jena handles it.
        System.out.printf(
            "Read %d triples (in total) and %d quads%n",
            readerStream.countTriples(),
            readerStream.countQuads()
        );

        // -------------------------------------
        System.out.println("\n");

        System.out.println("Writing a stream of 10 triples to a file...");

        // Try writing some triples to a file
        // We need to create an instance of RdfStreamOptions to pass to the writer:
        RdfStreamOptions options = JellyOptions.SMALL_STRICT.clone()
            // The stream writer does not know if we will be writing triples or quads – we
            // have to specify the physical stream type explicitly.
            .setPhysicalType(PhysicalStreamType.TRIPLES)
            .setStreamName("A stream of 10 triples");

        // To pass the options, we use Jena's Context mechanism
        Context context = RIOT.getContext()
            .copy()
            .set(JellyLanguage.SYMBOL_STREAM_OPTIONS, options)
            .set(JellyLanguage.SYMBOL_FRAME_SIZE, 128); // optional, default is 256

        // Create the writer using try-with-resources
        try (FileOutputStream out = new FileOutputStream("stream-riot.jelly")) {
            // Create the writer – remember to pass the context!
            StreamRDF writerStream = StreamRDFWriter.getWriterStream(out, JellyLanguage.JELLY, context);
            writerStream.start();

            for (int i = 1; i <= 10; i++) {
                writerStream.triple(
                    Triple.create(
                        NodeFactory.createBlankNode(),
                        NodeFactory.createURI("https://example.org/p"),
                        NodeFactory.createLiteralString("object " + i)
                    )
                );
            }

            writerStream.finish();
        }

        System.out.println("Done writing triples");

        // Load the RDF graph that we just saved using normal RIOT API
        Model model = RDFDataMgr.loadModel("stream-riot.jelly", JellyLanguage.JELLY);

        System.out.println("Loaded the stream from disk, contents:\n");
        model.write(System.out, "NT");
    }
}

See also