T3-1109 Avoid network-dependency for bundle context

jsonld dependency 0.3 instead of 0.2.99-mygrid
diff --git a/pom.xml b/pom.xml
index 2538cc8..1b0d3cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <scufl2.version>0.11.0</scufl2.version>
-        <jsonld.version>0.2.99-mygrid</jsonld.version>
+        <jsonld.version>0.3</jsonld.version>
         <!-- Raven: Jena and Jackson versions must match dependencies
         from jsonld *and* scufl2 --> 
         <jena.version>2.11.0</jena.version>
@@ -74,7 +74,12 @@
             <type>bundle</type>
         </dependency>
          -->
-       
+
+        <dependency>
+        	<groupId>uk.org.taverna.httpclientjarcache</groupId>
+        	<artifactId>httpclient-jarcache</artifactId>
+        	<version>0.0.1-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 
     <scm>
diff --git a/src/main/java/org/purl/wf4ever/robundle/manifest/Manifest.java b/src/main/java/org/purl/wf4ever/robundle/manifest/Manifest.java
index 99f365c..961aae0 100644
--- a/src/main/java/org/purl/wf4ever/robundle/manifest/Manifest.java
+++ b/src/main/java/org/purl/wf4ever/robundle/manifest/Manifest.java
@@ -12,7 +12,6 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileTime;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.GregorianCalendar;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
diff --git a/src/main/java/org/purl/wf4ever/robundle/manifest/RDFToManifest.java b/src/main/java/org/purl/wf4ever/robundle/manifest/RDFToManifest.java
index f1b02c3..9b24e2d 100644
--- a/src/main/java/org/purl/wf4ever/robundle/manifest/RDFToManifest.java
+++ b/src/main/java/org/purl/wf4ever/robundle/manifest/RDFToManifest.java
@@ -14,9 +14,14 @@
 import java.util.UUID;
 import java.util.logging.Logger;
 
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.cache.CachingHttpClient;
 import org.apache.jena.riot.RDFDataMgr;
 import org.apache.jena.riot.RiotException;
 
+import uk.org.taverna.httpclient.jarcache.JarCacheStorage;
+
+import com.github.jsonldjava.core.DocumentLoader;
 import com.github.jsonldjava.jena.JenaJSONLD;
 import com.hp.hpl.jena.datatypes.xsd.XSDDateTime;
 import com.hp.hpl.jena.ontology.DatatypeProperty;
@@ -33,409 +38,440 @@
 import com.hp.hpl.jena.util.iterator.ExtendedIterator;
 
 public class RDFToManifest {
-    private static Logger logger = Logger.getLogger(RDFToManifest.class.getCanonicalName());
-    
-    private static final String PROV = "http://www.w3.org/ns/prov#";
-    private static final String PROV_O = "http://www.w3.org/ns/prov-o#";
-    private static final String FOAF_0_1 = "http://xmlns.com/foaf/0.1/";
-    private static final String PAV = "http://purl.org/pav/";
+	
+	private static Logger logger = Logger.getLogger(RDFToManifest.class
+			.getCanonicalName());
 
-    private static final String DCT = "http://purl.org/dc/terms/";
-    //private static final String RO = "http://purl.org/wf4ever/ro#";
-    private static final String ORE = "http://www.openarchives.org/ore/terms/";
-    private static final String OA = "http://www.w3.org/ns/oa#";
-    private static final String FOAF_RDF = "/ontologies/foaf.rdf";
-    private static final String PAV_RDF = "/ontologies/pav.rdf";
-    private static final String PROV_O_RDF = "/ontologies/prov-o.rdf";
-    private static final String PROV_AQ_RDF = "/ontologies/prov-aq.rdf";
-    private OntModel ore;
-    private ObjectProperty aggregates;
-    private ObjectProperty proxyFor;
-    private ObjectProperty proxyIn;
+	static {
+		setCachedHttpClientInJsonLD();
+	}
+	
+	private static final String PROV = "http://www.w3.org/ns/prov#";
+	private static final String PROV_O = "http://www.w3.org/ns/prov-o#";
+	private static final String FOAF_0_1 = "http://xmlns.com/foaf/0.1/";
+	private static final String PAV = "http://purl.org/pav/";
 
-    private OntClass aggregation;
-    private OntModel foaf;
-    private DatatypeProperty foafName;
-    private OntModel pav;
-    private ObjectProperty createdBy;
-    private OntModel prov;
-    private OntModel provaq;
-    private ObjectProperty hasProvenance;
-    private OntModel dct;
-    private ObjectProperty conformsTo;
-    private OntClass standard;
-    private ObjectProperty authoredBy;
-    private DatatypeProperty createdOn;
-    private DatatypeProperty authoredOn;
+	private static final String DCT = "http://purl.org/dc/terms/";
+	// private static final String RO = "http://purl.org/wf4ever/ro#";
+	private static final String ORE = "http://www.openarchives.org/ore/terms/";
+	private static final String OA = "http://www.w3.org/ns/oa#";
+	private static final String FOAF_RDF = "/ontologies/foaf.rdf";
+	private static final String PAV_RDF = "/ontologies/pav.rdf";
+	private static final String PROV_O_RDF = "/ontologies/prov-o.rdf";
+	private static final String PROV_AQ_RDF = "/ontologies/prov-aq.rdf";
+	private OntModel ore;
+	private ObjectProperty aggregates;
+	private ObjectProperty proxyFor;
+	private ObjectProperty proxyIn;
 
-    private DatatypeProperty format;
+	private OntClass aggregation;
+	private OntModel foaf;
+	private DatatypeProperty foafName;
+	private OntModel pav;
+	private ObjectProperty createdBy;
+	private OntModel prov;
+	private OntModel provaq;
+	private ObjectProperty hasProvenance;
+	private OntModel dct;
+	private ObjectProperty conformsTo;
+	private OntClass standard;
+	private ObjectProperty authoredBy;
+	private DatatypeProperty createdOn;
+	private DatatypeProperty authoredOn;
 
-    private OntModel oa;
+	private DatatypeProperty format;
 
-    private ObjectProperty hasBody;
+	private OntModel oa;
 
-    private ObjectProperty hasTarget;
+	private ObjectProperty hasBody;
 
-    public RDFToManifest() {
-        loadOntologies();
-    }
-    
-    protected void loadOntologies() {
-        loadDCT();
-        loadORE();
-        loadFOAF();
-        loadPROVO();
-        loadPAV();
-        loadPROVAQ();
-        loadOA();
-    }
+	private ObjectProperty hasTarget;
 
+	public RDFToManifest() {
+		loadOntologies();
+	}
 
-    protected OntModel loadOntologyFromClasspath(String classPathUri, String uri) {
-        OntModel ontModel = ModelFactory.createOntologyModel();
+	protected void loadOntologies() {
+		loadDCT();
+		loadORE();
+		loadFOAF();
+		loadPROVO();
+		loadPAV();
+		loadPROVAQ();
+		loadOA();
+	}
 
-        // Load from classpath
-        InputStream inStream = getClass().getResourceAsStream(classPathUri);
-        if (inStream == null) {
-            throw new IllegalArgumentException("Can't load " + classPathUri);
-        }
-//        Ontology ontology = ontModel.createOntology(uri);
-        ontModel.read(inStream, uri);
-        return ontModel;
-    }
+	protected OntModel loadOntologyFromClasspath(String classPathUri, String uri) {
+		OntModel ontModel = ModelFactory.createOntologyModel();
 
+		// Load from classpath
+		InputStream inStream = getClass().getResourceAsStream(classPathUri);
+		if (inStream == null) {
+			throw new IllegalArgumentException("Can't load " + classPathUri);
+		}
+		// Ontology ontology = ontModel.createOntology(uri);
+		ontModel.read(inStream, uri);
+		return ontModel;
+	}
 
-    
-    
-    protected static Model jsonLdAsJenaModel(InputStream jsonIn, URI base) throws IOException,
-            RiotException {
-        JenaJSONLD.init();
-        Model model = ModelFactory.createDefaultModel();
-        RDFDataMgr.read(model, jsonIn, base.toASCIIString(), JenaJSONLD.JSONLD);
-        return model;
-        
-//        
-//        Object input = JSONUtils.fromInputStream(jsonIn);
-//        JSONLDTripleCallback callback = new JenaTripleCallback();        
-//        Model model = (Model)JSONLD.toRDF(input, callback, new Options(base.toASCIIString()));
-//        return model;
-    }
-    
-    private void checkNotNull(Object... possiblyNulls) {
-        int i=0;
-        for (Object check : possiblyNulls) {
-            if (check == null) {
-                throw new IllegalStateException("Could not load item #" + i);
-            }
-            i++;
-        }
-        
-    }
+	
+	
+	protected static Model jsonLdAsJenaModel(InputStream jsonIn, URI base)
+			throws IOException, RiotException {
+		JenaJSONLD.init();
+		Model model = ModelFactory.createDefaultModel();
+		RDFDataMgr.read(model, jsonIn, base.toASCIIString(), JenaJSONLD.JSONLD);
+		return model;
 
+		//
+		// Object input = JSONUtils.fromInputStream(jsonIn);
+		// JSONLDTripleCallback callback = new JenaTripleCallback();
+		// Model model = (Model)JSONLD.toRDF(input, callback, new
+		// Options(base.toASCIIString()));
+		// return model;
+	}
 
-    protected OntModel getOntModel() {
-        OntModel ontModel = ModelFactory.createOntologyModel();
-        ontModel.setNsPrefix("foaf", FOAF_0_1);
-        ontModel.setNsPrefix("prov", PROV);
-        ontModel.setNsPrefix("ore", ORE);
-        ontModel.setNsPrefix("pav", PAV);
-        ontModel.setNsPrefix("dct", DCT);
-//        ontModel.getDocumentManager().loadImports(foaf.getOntModel());
-        return ontModel;
-    }
-//    
-    protected synchronized void loadFOAF() {
-        if (foaf != null) {
-            return;
-        }
+	/**
+	 * Use a JarCacheStorage so that our JSON-LD @context can be loaded from our
+	 * classpath and not require network connectivity
+	 * 
+	 */
+	protected static void setCachedHttpClientInJsonLD() {
+		JarCacheStorage cacheStorage = new JarCacheStorage(
+				RDFToManifest.class.getClassLoader());
+		synchronized (DocumentLoader.class) {
+			HttpClient oldHttpClient = DocumentLoader.getHttpClient();
+			CachingHttpClient wrappedHttpClient = new CachingHttpClient(
+					oldHttpClient, cacheStorage, cacheStorage.getCacheConfig());
+			DocumentLoader.setHttpClient(wrappedHttpClient);
+		}
+	}
 
-        OntModel ontModel = loadOntologyFromClasspath(FOAF_RDF, FOAF_0_1);            
-        
-        // properties from foaf
-        foafName = ontModel.getDatatypeProperty(FOAF_0_1 + "name");
-        checkNotNull(foafName);
-                
-        foaf = ontModel;            
-    }
-    
+	private void checkNotNull(Object... possiblyNulls) {
+		int i = 0;
+		for (Object check : possiblyNulls) {
+			if (check == null) {
+				throw new IllegalStateException("Could not load item #" + i);
+			}
+			i++;
+		}
 
-    protected synchronized void loadPAV() {
-        if (pav != null) {
-            return;
-        }
+	}
 
-        OntModel ontModel = loadOntologyFromClasspath(PAV_RDF, PAV);            
-        // properties from foaf
-        createdBy = ontModel.getObjectProperty(PAV + "createdBy");
-        createdOn = ontModel.getDatatypeProperty(PAV + "createdOn");
-        authoredBy = ontModel.getObjectProperty(PAV + "authoredBy");
-        authoredOn = ontModel.getDatatypeProperty(PAV + "authoredOn");
-        checkNotNull(createdBy,createdOn, authoredBy, authoredOn);
-                
-        pav = ontModel;            
-    }
+	protected OntModel getOntModel() {
+		OntModel ontModel = ModelFactory.createOntologyModel();
+		ontModel.setNsPrefix("foaf", FOAF_0_1);
+		ontModel.setNsPrefix("prov", PROV);
+		ontModel.setNsPrefix("ore", ORE);
+		ontModel.setNsPrefix("pav", PAV);
+		ontModel.setNsPrefix("dct", DCT);
+		// ontModel.getDocumentManager().loadImports(foaf.getOntModel());
+		return ontModel;
+	}
 
-    protected synchronized void loadPROVO() {
-        if (prov != null) {
-            return;
-        }
-        OntModel ontModel = loadOntologyFromClasspath(PROV_O_RDF, PROV_O);            
-        
-        checkNotNull(ontModel);
-                
-        prov = ontModel;            
-    }
+	//
+	protected synchronized void loadFOAF() {
+		if (foaf != null) {
+			return;
+		}
 
+		OntModel ontModel = loadOntologyFromClasspath(FOAF_RDF, FOAF_0_1);
 
-    protected synchronized void loadPROVAQ() {
-        if (provaq != null) {
-            return;
-        }
-        OntModel ontModel = loadOntologyFromClasspath(PROV_AQ_RDF, PAV);            
-        
-        // properties from foaf
-        hasProvenance = ontModel.getObjectProperty(PROV + "has_provenance");
-        checkNotNull(hasProvenance);
-                
-        provaq = ontModel;            
-    }
+		// properties from foaf
+		foafName = ontModel.getDatatypeProperty(FOAF_0_1 + "name");
+		checkNotNull(foafName);
 
+		foaf = ontModel;
+	}
 
-    protected synchronized void loadDCT() {
-        if (dct != null) {
-            return;
-        }
+	protected synchronized void loadPAV() {
+		if (pav != null) {
+			return;
+		}
 
-        OntModel ontModel = loadOntologyFromClasspath("/ontologies/dcterms_od.owl", "http://purl.org/wf4ever/dcterms_od");            
-        
-        // properties from dct
-        standard = ontModel.getOntClass(DCT + "Standard");
-        conformsTo = ontModel.getObjectProperty(DCT + "conformsTo");
+		OntModel ontModel = loadOntologyFromClasspath(PAV_RDF, PAV);
+		// properties from foaf
+		createdBy = ontModel.getObjectProperty(PAV + "createdBy");
+		createdOn = ontModel.getDatatypeProperty(PAV + "createdOn");
+		authoredBy = ontModel.getObjectProperty(PAV + "authoredBy");
+		authoredOn = ontModel.getDatatypeProperty(PAV + "authoredOn");
+		checkNotNull(createdBy, createdOn, authoredBy, authoredOn);
 
-        // We'll cheat dc:format in
-        format = ontModel.createDatatypeProperty("http://purl.org/dc/elements/1.1/" + "format");
-        checkNotNull(standard, conformsTo, format);
-                
-        dct = ontModel;  
-        
-    }
-    
-    protected synchronized void loadOA() {
-        if (oa != null) {
-            return;
-        }
-        OntModel ontModel = loadOntologyFromClasspath("/ontologies/oa.rdf", OA);
-        hasTarget = ontModel.getObjectProperty(OA + "hasTarget");
-        hasBody = ontModel.getObjectProperty(OA + "hasBody");        
-        checkNotNull(hasTarget, hasBody);
-        oa = ontModel;
-    }
-    
-    protected synchronized void loadORE() {
-        if (ore != null) {
-            return;
-        }
-        OntModel ontModel = loadOntologyFromClasspath("/ontologies/ore-owl.owl", "http://purl.org/wf4ever/ore-owl");
-        aggregation = ontModel.getOntClass(ORE + "Aggregation");
+		pav = ontModel;
+	}
 
-        aggregates = ontModel.getObjectProperty(ORE + "aggregates");
-        proxyFor = ontModel.getObjectProperty(ORE + "proxyFor");
-        proxyIn = ontModel.getObjectProperty(ORE + "proxyIn");
-        
-        checkNotNull(aggregation, aggregates, proxyFor, proxyIn);
-        
-        
-        ore = ontModel;
-    }
-    
-    public static <T> ClosableIterable<T> iterate(ExtendedIterator<T> iterator) {
-        return new ClosableIterable<T>(iterator);
-    }
-    
-    public static class ClosableIterable<T> implements AutoCloseable, Iterable<T> {
+	protected synchronized void loadPROVO() {
+		if (prov != null) {
+			return;
+		}
+		OntModel ontModel = loadOntologyFromClasspath(PROV_O_RDF, PROV_O);
 
-        private ExtendedIterator<T> iterator;
+		checkNotNull(ontModel);
 
-        public ClosableIterable(ExtendedIterator<T> iterator) {
-            this.iterator = iterator;
-        }
+		prov = ontModel;
+	}
 
-        @Override
-        public void close() {
-            iterator.close();
-        }
+	protected synchronized void loadPROVAQ() {
+		if (provaq != null) {
+			return;
+		}
+		OntModel ontModel = loadOntologyFromClasspath(PROV_AQ_RDF, PAV);
 
-        @Override
-        public ExtendedIterator<T> iterator() {
-            return iterator;
-        }       
-    }
+		// properties from foaf
+		hasProvenance = ontModel.getObjectProperty(PROV + "has_provenance");
+		checkNotNull(hasProvenance);
 
-    public void readTo(InputStream resourceAsStream, Manifest manifest) throws IOException, RiotException {
-        OntModel model = new RDFToManifest().getOntModel();
-        URI base;
-        try {
-            base = makeBaseURI();
-        } catch (URISyntaxException e) {
-            throw new IllegalStateException("Can't make base URI of form app://{uuid}/", e);
-        }
-        
-        model.add(jsonLdAsJenaModel(resourceAsStream, base));
-        
-        Individual ro = findRO(model, base);
-        
+		provaq = ontModel;
+	}
 
+	protected synchronized void loadDCT() {
+		if (dct != null) {
+			return;
+		}
 
-        List<Agent> creators = getAgents(base, ro, createdBy);
-        if (! creators.isEmpty()) {
-            manifest.setCreatedBy(creators);            
-        }
-        RDFNode created = ro.getPropertyValue(createdOn);
-        manifest.setCreatedOn(literalAsFileTime(created));
+		OntModel ontModel = loadOntologyFromClasspath(
+				"/ontologies/dcterms_od.owl",
+				"http://purl.org/wf4ever/dcterms_od");
 
-        List<Agent> authors = getAgents(base, ro, authoredBy);
-        if (! authors.isEmpty()) {
-            manifest.setAuthoredBy(authors);
-        }
-        RDFNode authored = ro.getPropertyValue(authoredOn);
-        manifest.setAuthoredOn(literalAsFileTime(authored));
-        
-        
-        for (Individual aggrResource : listObjectProperties(ro, aggregates)) {
-            String uriStr = aggrResource.getURI();
-            //PathMetadata meta = new PathMetadata();
-            if (uriStr == null) {
-                logger.warning("Skipping aggregation without URI: " + aggrResource);
-                continue;
-            }
-            
-            PathMetadata meta = manifest.getAggregation(relativizeFromBase(uriStr, base));
-            
-            Resource proxy = aggrResource.getPropertyResourceValue(proxyFor);
-            if (proxy != null && proxy.getURI() != null) {
-                meta.setProxy(relativizeFromBase(proxy.getURI(), base));
-            }
+		// properties from dct
+		standard = ontModel.getOntClass(DCT + "Standard");
+		conformsTo = ontModel.getObjectProperty(DCT + "conformsTo");
 
+		// We'll cheat dc:format in
+		format = ontModel
+				.createDatatypeProperty("http://purl.org/dc/elements/1.1/"
+						+ "format");
+		checkNotNull(standard, conformsTo, format);
 
-            
-            creators = getAgents(base, aggrResource, createdBy);
-            if (! creators.isEmpty()) {
-                meta.setCreatedBy(creators);            
-            }
-            meta.setCreatedOn(literalAsFileTime(aggrResource.getPropertyValue(createdOn)));
+		dct = ontModel;
 
-            
-            for (Individual standard : listObjectProperties(aggrResource, conformsTo)) {
-                if (standard.getURI() != null) {
-                    meta.setConformsTo(relativizeFromBase(standard.getURI(), base));
-                }
-            }
-            
-            RDFNode mediaType = aggrResource.getPropertyValue(format);            
-            if (mediaType != null && mediaType.isLiteral()) {
-                meta.setMediatype(mediaType.asLiteral().getLexicalForm());
-            }
-            
-            
-        }
-        
-        try (ClosableIterable<Resource> annotations = iterate( model.listResourcesWithProperty(hasTarget) )) {
-            for (Resource ann : annotations) {
-                //System.out.println("Found annotation " + ann);
-                
-                // Normally just one body per annotation, but just in case we'll iterate
-                // and split them out
-                for (Individual body : listObjectProperties(model.getOntResource(ann), hasBody)) { 
-                    PathAnnotation pathAnn = new PathAnnotation();
-                    
-                    if (ann.getURI() != null) {
-                        pathAnn.setAnnotation(relativizeFromBase(ann.getURI(), base));
-                    }
-    
-                    Resource target = ann.getPropertyResourceValue(hasTarget);
-                    if (target != null && target.getURI() != null) {
-                        pathAnn.setAbout(relativizeFromBase(target.getURI(), base));
-                    }
-                    if (body.getURI() != null) {
-                        pathAnn.setContent(relativizeFromBase(body.getURI(), base));
-                    } else { 
-                        logger.warning("Can't find annotation body for anonymous " + body);
-                    }
-                    manifest.getAnnotations().add(pathAnn);
-                }
-            }            
-        }
-        
-//        model.write(System.out, "TURTLE");
-        
-    }
+	}
 
-    private FileTime literalAsFileTime(RDFNode rdfNode) {
-        if (rdfNode == null) { 
-            return null;
-        }
-        if (! rdfNode.isLiteral()) { 
-            logger.warning("Expected literal. not " + rdfNode);
-        }
-        Literal literal = rdfNode.asLiteral();
-        Object value = literal.getValue();
-        if (! (value instanceof XSDDateTime)) {
-            logger.warning("Literal not an XSDDateTime, but: " + value.getClass() + " " + value);
-            return null;
-        }        
-        XSDDateTime dateTime = (XSDDateTime) value;
-        long millis = dateTime.asCalendar().getTimeInMillis();                            
-        return FileTime.fromMillis(millis);
-    }
+	protected synchronized void loadOA() {
+		if (oa != null) {
+			return;
+		}
+		OntModel ontModel = loadOntologyFromClasspath("/ontologies/oa.rdf", OA);
+		hasTarget = ontModel.getObjectProperty(OA + "hasTarget");
+		hasBody = ontModel.getObjectProperty(OA + "hasBody");
+		checkNotNull(hasTarget, hasBody);
+		oa = ontModel;
+	}
 
-    private List<Agent> getAgents(URI base, Individual in, ObjectProperty property) {
-        List<Agent> creators = new ArrayList<>();
-        for (Individual agent : listObjectProperties(in, property)) {
-            Agent a = new Agent();
-            if (agent.getURI() != null) {
-                a.setUri(relativizeFromBase(agent.getURI(), base));
-            }
-                
-            RDFNode name = agent.getPropertyValue(foafName);
-            if (name != null && name.isLiteral()) {
-                a.setName(name.asLiteral().getLexicalForm());
-            }                
-            creators.add(a);
-        }
-        return creators;
-    }
+	protected synchronized void loadORE() {
+		if (ore != null) {
+			return;
+		}
+		OntModel ontModel = loadOntologyFromClasspath(
+				"/ontologies/ore-owl.owl", "http://purl.org/wf4ever/ore-owl");
+		aggregation = ontModel.getOntClass(ORE + "Aggregation");
 
-    protected static URI makeBaseURI() throws URISyntaxException {
-        return new URI("app", UUID.randomUUID().toString(), "/", (String)null);
-    }
+		aggregates = ontModel.getObjectProperty(ORE + "aggregates");
+		proxyFor = ontModel.getObjectProperty(ORE + "proxyFor");
+		proxyIn = ontModel.getObjectProperty(ORE + "proxyIn");
 
+		checkNotNull(aggregation, aggregates, proxyFor, proxyIn);
 
+		ore = ontModel;
+	}
 
+	public static <T> ClosableIterable<T> iterate(ExtendedIterator<T> iterator) {
+		return new ClosableIterable<T>(iterator);
+	}
 
-    private Set<Individual> listObjectProperties(OntResource ontResource,
-            ObjectProperty prop) {
-        LinkedHashSet<Individual> results = new LinkedHashSet<>();
-        try (ClosableIterable<RDFNode> props = iterate(ontResource.listPropertyValues(prop))) {
-            for (RDFNode node : props) {
-                if (! node.isResource() || ! node.canAs(Individual.class)) {
-                    continue;
-                }
-                results.add(node.as(Individual.class));
-            }
-        }
-        return results;
-    }
+	public static class ClosableIterable<T> implements AutoCloseable,
+			Iterable<T> {
 
-    private Individual findRO(OntModel model, URI base) {
-        try (ClosableIterable<? extends OntResource> instances = iterate(aggregation.listInstances())) {
-            for (OntResource o : instances) {
-//                System.out.println("Woo " + o);
-                return o.asIndividual();
-            }
-        }
-        // Fallback - resolve as "/"
-        // TODO: Ensure it's an Aggregation?
-        return model.getIndividual(base.toString());
-    }
+		private ExtendedIterator<T> iterator;
+
+		public ClosableIterable(ExtendedIterator<T> iterator) {
+			this.iterator = iterator;
+		}
+
+		@Override
+		public void close() {
+			iterator.close();
+		}
+
+		@Override
+		public ExtendedIterator<T> iterator() {
+			return iterator;
+		}
+	}
+
+	public void readTo(InputStream resourceAsStream, Manifest manifest)
+			throws IOException, RiotException {
+		OntModel model = new RDFToManifest().getOntModel();
+		URI base;
+		try {
+			base = makeBaseURI();
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException(
+					"Can't make base URI of form app://{uuid}/", e);
+		}
+
+		model.add(jsonLdAsJenaModel(resourceAsStream, base));
+
+		Individual ro = findRO(model, base);
+
+		List<Agent> creators = getAgents(base, ro, createdBy);
+		if (!creators.isEmpty()) {
+			manifest.setCreatedBy(creators);
+		}
+		RDFNode created = ro.getPropertyValue(createdOn);
+		manifest.setCreatedOn(literalAsFileTime(created));
+
+		List<Agent> authors = getAgents(base, ro, authoredBy);
+		if (!authors.isEmpty()) {
+			manifest.setAuthoredBy(authors);
+		}
+		RDFNode authored = ro.getPropertyValue(authoredOn);
+		manifest.setAuthoredOn(literalAsFileTime(authored));
+
+		for (Individual aggrResource : listObjectProperties(ro, aggregates)) {
+			String uriStr = aggrResource.getURI();
+			// PathMetadata meta = new PathMetadata();
+			if (uriStr == null) {
+				logger.warning("Skipping aggregation without URI: "
+						+ aggrResource);
+				continue;
+			}
+
+			PathMetadata meta = manifest.getAggregation(relativizeFromBase(
+					uriStr, base));
+
+			Resource proxy = aggrResource.getPropertyResourceValue(proxyFor);
+			if (proxy != null && proxy.getURI() != null) {
+				meta.setProxy(relativizeFromBase(proxy.getURI(), base));
+			}
+
+			creators = getAgents(base, aggrResource, createdBy);
+			if (!creators.isEmpty()) {
+				meta.setCreatedBy(creators);
+			}
+			meta.setCreatedOn(literalAsFileTime(aggrResource
+					.getPropertyValue(createdOn)));
+
+			for (Individual standard : listObjectProperties(aggrResource,
+					conformsTo)) {
+				if (standard.getURI() != null) {
+					meta.setConformsTo(relativizeFromBase(standard.getURI(),
+							base));
+				}
+			}
+
+			RDFNode mediaType = aggrResource.getPropertyValue(format);
+			if (mediaType != null && mediaType.isLiteral()) {
+				meta.setMediatype(mediaType.asLiteral().getLexicalForm());
+			}
+
+		}
+
+		try (ClosableIterable<Resource> annotations = iterate(model
+				.listResourcesWithProperty(hasTarget))) {
+			for (Resource ann : annotations) {
+				// System.out.println("Found annotation " + ann);
+
+				// Normally just one body per annotation, but just in case we'll
+				// iterate
+				// and split them out
+				for (Individual body : listObjectProperties(
+						model.getOntResource(ann), hasBody)) {
+					PathAnnotation pathAnn = new PathAnnotation();
+
+					if (ann.getURI() != null) {
+						pathAnn.setAnnotation(relativizeFromBase(ann.getURI(),
+								base));
+					}
+
+					Resource target = ann.getPropertyResourceValue(hasTarget);
+					if (target != null && target.getURI() != null) {
+						pathAnn.setAbout(relativizeFromBase(target.getURI(),
+								base));
+					}
+					if (body.getURI() != null) {
+						pathAnn.setContent(relativizeFromBase(body.getURI(),
+								base));
+					} else {
+						logger.warning("Can't find annotation body for anonymous "
+								+ body);
+					}
+					manifest.getAnnotations().add(pathAnn);
+				}
+			}
+		}
+
+		// model.write(System.out, "TURTLE");
+
+	}
+
+	private FileTime literalAsFileTime(RDFNode rdfNode) {
+		if (rdfNode == null) {
+			return null;
+		}
+		if (!rdfNode.isLiteral()) {
+			logger.warning("Expected literal. not " + rdfNode);
+		}
+		Literal literal = rdfNode.asLiteral();
+		Object value = literal.getValue();
+		if (!(value instanceof XSDDateTime)) {
+			logger.warning("Literal not an XSDDateTime, but: "
+					+ value.getClass() + " " + value);
+			return null;
+		}
+		XSDDateTime dateTime = (XSDDateTime) value;
+		long millis = dateTime.asCalendar().getTimeInMillis();
+		return FileTime.fromMillis(millis);
+	}
+
+	private List<Agent> getAgents(URI base, Individual in,
+			ObjectProperty property) {
+		List<Agent> creators = new ArrayList<>();
+		for (Individual agent : listObjectProperties(in, property)) {
+			Agent a = new Agent();
+			if (agent.getURI() != null) {
+				a.setUri(relativizeFromBase(agent.getURI(), base));
+			}
+
+			RDFNode name = agent.getPropertyValue(foafName);
+			if (name != null && name.isLiteral()) {
+				a.setName(name.asLiteral().getLexicalForm());
+			}
+			creators.add(a);
+		}
+		return creators;
+	}
+
+	protected static URI makeBaseURI() throws URISyntaxException {
+		return new URI("app", UUID.randomUUID().toString(), "/", (String) null);
+	}
+
+	private Set<Individual> listObjectProperties(OntResource ontResource,
+			ObjectProperty prop) {
+		LinkedHashSet<Individual> results = new LinkedHashSet<>();
+		try (ClosableIterable<RDFNode> props = iterate(ontResource
+				.listPropertyValues(prop))) {
+			for (RDFNode node : props) {
+				if (!node.isResource() || !node.canAs(Individual.class)) {
+					continue;
+				}
+				results.add(node.as(Individual.class));
+			}
+		}
+		return results;
+	}
+
+	private Individual findRO(OntModel model, URI base) {
+		try (ClosableIterable<? extends OntResource> instances = iterate(aggregation
+				.listInstances())) {
+			for (OntResource o : instances) {
+				// System.out.println("Woo " + o);
+				return o.asIndividual();
+			}
+		}
+		// Fallback - resolve as "/"
+		// TODO: Ensure it's an Aggregation?
+		return model.getIndividual(base.toString());
+	}
 
 }
diff --git a/src/main/resources/contexts/bundle.jsonld b/src/main/resources/contexts/bundle.jsonld
new file mode 100644
index 0000000..3a5c031
--- /dev/null
+++ b/src/main/resources/contexts/bundle.jsonld
@@ -0,0 +1,110 @@
+{
+  "@context": {
+    "ao": "http://purl.org/ao/",
+    "oa": "http://www.w3.org/ns/oa#",
+    "dc": "http://purl.org/dc/elements/1.1/",
+    "dct": "http://purl.org/dc/terms/",
+    "ore": "http://www.openarchives.org/ore/terms/",
+    "ro": "http://purl.org/wf4ever/ro#",
+    "roterms": "http://purl.org/wf4ever/roterms#",
+    "bundle": "http://purl.org/wf4ever/bundle#",
+    "prov": "http://www.w3.org/ns/prov#",
+    "pav": "http://purl.org/pav/",
+    "xsd": "http://www.w3.org/2001/XMLSchema#",
+    "foaf": "http://xmlns.com/foaf/0.1/",
+
+    "id": "@id",
+    "file": "@id",
+    "uri": "@id",
+    "annotation": "@id",
+
+    "manifest": {
+        "@id": "ore:isDescribedBy",
+        "@type": "@id"
+    },
+
+    "createdOn": {
+        "@id": "pav:createdOn",
+        "@type": "xsd:dateTime"
+    },
+    "createdBy": {
+        "@id": "pav:createdBy",
+        "@type": "@id"
+    },
+    "authoredOn": {
+        "@id": "pav:authoredOn",
+        "@type": "xsd:dateTime"
+    },
+    "authoredBy": {
+        "@id": "pav:authoredBy",
+        "@type": "@id"
+    },
+    "curatedOn": {
+        "@id": "pav:curatedOn",
+        "@type": "xsd:dateTime"
+    },
+    "curatedBy": {
+        "@id": "pav:curatedBy",
+        "@type": "@id"
+    },
+    "contributedOn": {
+        "@id": "pav:contributedOn",
+        "@type": "xsd:dateTime"
+    },
+    "contributedBy": {
+        "@id": "pav:contributedBy",
+        "@type": "@id"
+    },
+    "name": {
+        "@id": "foaf:name"
+    },
+    "orcid": {
+        "@id": "roterms:orcid",
+        "@type": "@id"
+    },
+    "history": {
+        "@id": "prov:has_provenance",
+        "@type": "@id"
+    },
+    "aggregates": {
+      "@id": "ore:aggregates",
+      "@type": "@id"
+    },
+    "mediatype": {
+        "@id": "dc:format"
+    },
+    "folder": {
+      "@id": "bundle:inFolder",
+      "@type": "@id"
+    },
+    "proxy": {
+      "@id": "bundle:hasProxy",
+      "@type": "@id"
+    },
+    "bundledAs": { 
+        "@id": "bundle:name",
+        "@type": "@id"
+    },
+    "conformsTo": { 
+        "@id": "dct:conformsTo",
+        "@type": "@id"
+    },
+    "annotations": {
+      "@id": "bundle:hasAnnotation",
+      "@type": "@id"
+    },
+    "content": {
+       "@id": "oa:hasBody",
+       "@type": "@id"
+    },
+    "about": {
+       "@id": "oa:hasTarget",
+       "@type": "@id" 
+    }
+
+
+  },
+  "http://purl.org/pav/retrievedFrom": {
+    "@id": "https://w3id.org/bundle/context"
+  }
+}
diff --git a/src/main/resources/jarcache.json b/src/main/resources/jarcache.json
new file mode 100644
index 0000000..34df158
--- /dev/null
+++ b/src/main/resources/jarcache.json
@@ -0,0 +1,8 @@
+[
+  {
+  	"Content-Location": "https://w3id.org/bundle/context",
+  	"X-Classpath": "contexts/bundle.jsonld",
+  	"Content-Type": "application/ld+json"
+  }
+]
+
diff --git a/src/test/java/org/purl/wf4ever/robundle/manifest/TestRDFToManifest.java b/src/test/java/org/purl/wf4ever/robundle/manifest/TestRDFToManifest.java
new file mode 100644
index 0000000..3747b46
--- /dev/null
+++ b/src/test/java/org/purl/wf4ever/robundle/manifest/TestRDFToManifest.java
@@ -0,0 +1,23 @@
+package org.purl.wf4ever.robundle.manifest;
+
+import static org.junit.Assert.*;
+
+import java.net.URL;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.github.jsonldjava.core.DocumentLoader;
+
+public class TestRDFToManifest {
+	private static final String CONTEXT = "https://w3id.org/bundle/context";
+
+	@Test
+	public void contextLoadedFromJarCache() throws Exception {
+		RDFToManifest.makeBaseURI(); // trigger static{} block
+		Map<String, Object> context = (Map<String, Object>) DocumentLoader.fromURL(new URL(CONTEXT));
+		Object retrievedFrom = context.get("http://purl.org/pav/retrievedFrom");
+		assertNotNull("Did not load context from cache: " + CONTEXT, retrievedFrom);
+				
+	}
+}