2

I am trying out OpenRDF Alibaba (associated with Sesame) as a tool to map Java objects to RDF triples and back again. Currently, I'm looking at how it handles object graphs.

I have two objects, Inner and Outer. Outer has a reference to Inner. When I persist an Outer-instance, it seems that the Inner-instance is aways represented as b-node, even if I've persisted the Inner-instance with an assigned IRI previously.

What do I have to do to be able to successfully assign the Inner-instance's IRI myself, instead of getting b-nodes created?

Extra credit question: how can I make the resource IRI a property on the Java object, rather than having it be parallel to but disconnected from the object it identifies?


Code:

Inner:

package alibabaeval.domain;

import org.openrdf.annotations.Iri;

@Iri("http://example.com/innerType")
public class Inner {

    @Iri("http://example.com/innerType/data")
    private String data;

    public Inner(String data) {
        this.data = data;
    }


    // if this is missing, an unhelpful ClassCastException will be thrown on retrieval
    public Inner() {
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

Outer:

package alibabaeval.domain;

import org.openrdf.annotations.Iri;

@Iri("http://example.com/outerType")
public class Outer {

    @Iri("http://example.com/outerType/data")
    private String outerData;

    @Iri("http://example.com/outerType/innerObject")
    private Inner innerObject;

    public Outer(String outerData) {
        this.outerData = outerData;
    }

    // if this is missing, an unhelpful ClassCastException will be thrown on retrieval
    public Outer() {
    }

    public String getOuterData() {
        return outerData;
    }

    public void setOuterData(String outerData) {
        this.outerData = outerData;
    }

    public Inner getInnerObject() {
        return innerObject;
    }

    public void setInnerObject(Inner innerObject) {
        this.innerObject = innerObject;
    }
}

Test Program:

package alibabaeval;

import org.junit.Test;
import org.openrdf.model.URI;
import org.openrdf.model.ValueFactory;
import org.openrdf.query.QueryLanguage;
import org.openrdf.repository.Repository;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.ObjectRepository;
import org.openrdf.repository.object.config.ObjectRepositoryFactory;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFWriter;
import org.openrdf.rio.Rio;
import org.openrdf.sail.memory.MemoryStore;

import alibabaeval.domain.Inner;
import alibabaeval.domain.Outer;

public class AlibabaEval {

    public static void main(String[] args) throws Exception {
        Repository store = new SailRepository(new MemoryStore());
        store.initialize();

        // wrap in an object repository
        ObjectRepositoryFactory factory = new ObjectRepositoryFactory();
        ObjectRepository repository = factory.createRepository(store);

        // add a stuff to the repository
        ObjectConnection con = repository.getConnection();
        ValueFactory vf = con.getValueFactory();

        Inner inner = new Inner("some inner data");
        URI innerId = vf.createURI("http://example.com/inners/inner1");
        con.addObject(innerId, inner);

        URI outerId = vf.createURI("http://example.com/outers/outer1");
        Outer outer = new Outer("some outer data");
        outer.setInnerObject(inner);
        con.addObject(outerId, outer);

        // look at the triples that were created
        System.out.println("\n\n\nGenerated triples:");
        RDFWriter writer = Rio.createWriter(RDFFormat.NTRIPLES, System.out);
        con.prepareGraphQuery(QueryLanguage.SPARQL, "CONSTRUCT { ?s ?p ?o } WHERE {?s ?p ?o }").evaluate(writer);

        // close everything down
        con.close();
        repository.shutDown();
    }
}

Output:

I created only two object instances, and persisted them both separately. Alibaba seemed to ignore that, and created a second copy of the Inner-instance as a b-node for the reference from the Outer-instance.

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/me/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-jdk14/1.7.7/25d160723ea37a6cb84e87cd70773ff02997e857/slf4j-jdk14-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/me/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-log4j12/1.7.12/485f77901840cf4e8bf852f2abb9b723eb8ec29/slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.JDK14LoggerFactory]
Jan 08, 2016 6:00:21 PM org.openrdf.repository.object.managers.helpers.Scanner scan
INFO: Scanning C:\workspace\AlibabaTest\bin for concepts
Jan 08, 2016 6:00:22 PM org.openrdf.repository.object.ObjectRepository compileSchema
INFO: Compiling schema
Jan 08, 2016 6:00:22 PM org.openrdf.repository.object.composition.ClassResolver setBaseClassRoles
WARNING: Concept will only be mergable: class alibabaeval.domain.Inner
Jan 08, 2016 6:00:22 PM org.openrdf.repository.object.composition.ClassResolver setBaseClassRoles
WARNING: Concept will only be mergable: class alibabaeval.domain.Outer



Generated triples:
<http://example.com/inners/inner1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/innerType> .
<http://example.com/inners/inner1> <http://example.com/innerType/data> "some inner data" .
<http://example.com/outers/outer1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/outerType> .
_:node1a8hqu4aqx1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/innerType> .
_:node1a8hqu4aqx1 <http://example.com/innerType/data> "some inner data" .
<http://example.com/outers/outer1> <http://example.com/outerType/innerObject> _:node1a8hqu4aqx1 .
<http://example.com/outers/outer1> <http://example.com/outerType/data> "some outer data" .
Jeen Broekstra
  • 21,642
  • 4
  • 51
  • 73
Kaypro II
  • 3,210
  • 8
  • 30
  • 41
  • This is just a guess (I haven't used Alibaba in a while so I'm rusty), but I think the problem is that the id for `Inner` gets added to the store, but your actual `Inner` POJO does not get updated. You may need to add a line `inner = con.getObject(Inner.class, innerId);` after the `addObject`. – Jeen Broekstra Jan 09 '16 at 03:40
  • So it's required to round-trip every object through the persistence layer (to assign an ID to it) and replace nested instances in-place (in order to persist a non-basic object graph)? I'll give it a try when I go back to work, though I hope there's a less complicated way of doing that. – Kaypro II Jan 09 '16 at 07:51
  • Adding `inner = con.getObject(Inner.class, innerId);` and adding *that* instance to outer got rid of the b-nodes. Thanks! – Kaypro II Jan 11 '16 at 17:43
  • Glad to hear it solved it. I "upgraded" my comment to an answer. Also to adress your performance fear: it shouldn't be too bad as Alibaba does in memory caching of recently accessed objects. – Jeen Broekstra Jan 11 '16 at 22:26
  • Performance was only part of it. The other part was persisting an unmanaged object graph with all the correct IDs (like one unmarshalled from a webservice into POJOs). I can see at least one way to write the code to do that mechanically, though I was hoping Alibaba could do it for me. Is there any way to "set" the resource on an unmanaged object so it will be picked up by the single-parameter con.addObject(Object instance)? – Kaypro II Jan 12 '16 at 17:24
  • I am not sure. You might have better luck asking this on the Sesame User Group instead, there's more experienced Alibaba users (and its main developer) lurking there. – Jeen Broekstra Jan 12 '16 at 19:23

2 Answers2

1

Now that I'm a little more familiar with Alibaba and after taking another read of the documentation, I see a better answer.

Alibaba will use the value returned by RDFObject.getResource() on a detached object. Here's an implementation of inner that does this:

package alibabaeval.domain;

import org.openrdf.annotations.Iri;
import org.openrdf.model.Resource;
import org.openrdf.model.impl.URIImpl;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.RDFObject;

@Iri("http://example.com/#innerType")
public class Inner implements RDFObject {

    @Iri("http://example.com/innerType/data")
    private String data;

    private Resource detachedId;

    public Inner() {

    }

    @Override
    public ObjectConnection getObjectConnection() {
        // don't really care about this one right now
        return null;
    }

    @Override
    public Resource getResource() {
        // only run on detached object, is hidden by a proxy on managed objects
        return detachedId;
    }

    public Resource getDetachedId() {
        return detachedId;
    }

    public void setDetachedId(Resource detachedId) {
        this.detachedId = detachedId;
    }

    public void setDetachedId(String detachedId) {
        this.detachedId = new URIImpl(detachedId);
    }

    public void setResource(Resource resource) {
        detachedId = resource;
    }

    public Inner(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

This allows you to do this:

    Inner inner = new Inner("some inner data");
    inner.setDetachedId("http://example.com/inners/inner1");

    URI outerId = vf.createURI("http://example.com/outers/outer1");
    Outer outer = new Outer("some outer data");
    outer.setInnerObject(inner);
    con.addObject(outerId, outer);

And get these triples:

<http://example.com/inners/inner1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/#innerType> .
<http://example.com/inners/inner1> <http://example.com/innerType/data> "some inner data" .
<http://example.com/outers/outer1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.com/#outerType> .
<http://example.com/outers/outer1> <http://example.com/outerType/data> "some outer data" .
<http://example.com/outers/outer1> <http://example.com/outerType/innerObject> <http://example.com/inners/inner1> .

This technique also works with the single-parameter ObjectConnection.addObject(Object instance) method.

This also only works for detached objects. Instances managed by Alibaba will proxy the RDFObject.getResource() method, so you no longer have control over what it returns.

Kaypro II
  • 3,210
  • 8
  • 30
  • 41
0

The problem is that the id for Inner gets added to the store, but your actual Inner POJO does not get updated. To fix this, simply add a line inner = con.getObject(Inner.class, innerId); after the call to addObject.

FWIW the additional roundtrip penalty for this is not particularly severe as Alibaba does recent-access object caching - so it will not need to go all the way to the persistence layer for this lookup.

As for how to get the identifying resource back from the object itself: if you make sure your POJO implements the RDFObject interface, you can call getResource to retrieve the associated id. Your implementation of the getResource method can simply return null, by the way, as Alibaba will override the implementation in its generated object.

Jeen Broekstra
  • 21,642
  • 4
  • 51
  • 73