| package org.apache.taverna.robundle.manifest; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| |
| import static org.apache.jena.ontology.OntModelSpec.OWL_DL_MEM_RULE_INF; |
| import static org.apache.jena.rdf.model.ModelFactory.createOntologyModel; |
| import static org.apache.taverna.robundle.utils.PathHelper.relativizeFromBase; |
| import static org.apache.taverna.robundle.utils.RDFUtils.literalAsFileTime; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import org.apache.jena.riot.Lang; |
| import org.apache.jena.riot.RDFDataMgr; |
| import org.apache.jena.riot.RiotException; |
| import org.apache.taverna.robundle.Bundles; |
| |
| import org.apache.jena.ontology.DatatypeProperty; |
| import org.apache.jena.ontology.Individual; |
| import org.apache.jena.ontology.ObjectProperty; |
| import org.apache.jena.ontology.OntClass; |
| import org.apache.jena.ontology.OntModel; |
| import org.apache.jena.ontology.OntResource; |
| 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.util.iterator.ExtendedIterator; |
| |
| public class RDFToManifest { |
| public static class ClosableIterable<T> implements AutoCloseable, |
| Iterable<T> { |
| |
| 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; |
| } |
| } |
| |
| private static final String BUNDLE = "http://purl.org/wf4ever/bundle#"; |
| |
| private static final String BUNDLE_RDF = "/ontologies/bundle.owl"; |
| private static final String DCT = "http://purl.org/dc/terms/"; |
| private static final String FOAF_0_1 = "http://xmlns.com/foaf/0.1/"; |
| private static final String FOAF_RDF = "/ontologies/foaf.rdf"; |
| |
| private static Logger logger = Logger.getLogger(RDFToManifest.class |
| .getCanonicalName()); |
| private static final String OA = "http://www.w3.org/ns/oa#"; |
| private static final String OA_RDF = "/ontologies/oa.rdf"; |
| private static final String ORE = "http://www.openarchives.org/ore/terms/"; |
| private static final String PAV = "http://purl.org/pav/"; |
| private static final String PAV_RDF = "/ontologies/pav.rdf"; |
| private static final String PROV = "http://www.w3.org/ns/prov#"; |
| private static final String PROV_AQ_RDF = "/ontologies/prov-aq.rdf"; |
| private static final String PROV_O = "http://www.w3.org/ns/prov-o#"; |
| private static final String PROV_O_RDF = "/ontologies/prov-o.rdf"; |
| |
| private static final String RO = "http://purl.org/wf4ever/ro#"; |
| private static final String RO_OWL = "/ontologies/ro.owl"; |
| |
| private static <T> ClosableIterable<T> iterate(ExtendedIterator<T> iterator) { |
| return new ClosableIterable<T>(iterator); |
| } |
| |
| protected static Model jsonLdAsJenaModel(InputStream jsonIn, URI base) |
| throws IOException, RiotException { |
| Model model = ModelFactory.createDefaultModel(); |
| |
| ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); |
| try { |
| // TAVERNA-971: set context classloader so jarcache.json is consulted |
| // even through OSGi |
| Thread.currentThread().setContextClassLoader(RDFToManifest.class.getClassLoader()); |
| |
| // Now we can parse the JSON-LD without network access |
| RDFDataMgr.read(model, jsonIn, base.toASCIIString(), Lang.JSONLD); |
| } finally { |
| // Restore old context class loader (if any) |
| Thread.currentThread().setContextClassLoader(oldCl); |
| } |
| return model; |
| } |
| |
| protected static URI makeBaseURI() throws URISyntaxException { |
| return new URI("app", UUID.randomUUID().toString(), "/", (String) null); |
| } |
| |
| private ObjectProperty aggregates; |
| private OntClass aggregation; |
| private ObjectProperty authoredBy; |
| private DatatypeProperty authoredOn; |
| private OntModel bundle; |
| private ObjectProperty conformsTo; |
| private ObjectProperty createdBy; |
| private DatatypeProperty createdOn; |
| private ObjectProperty retrievedFrom; |
| private ObjectProperty retrievedBy; |
| private DatatypeProperty retrievedOn; |
| private OntModel dct; |
| private OntModel foaf; |
| private DatatypeProperty foafName; |
| private DatatypeProperty format; |
| private ObjectProperty hasAnnotation; |
| |
| private ObjectProperty hasBody; |
| |
| private ObjectProperty hasProvenance; |
| |
| private ObjectProperty hasProxy; |
| |
| private ObjectProperty hasTarget; |
| private ObjectProperty inFolder; |
| private ObjectProperty isDescribedBy; |
| private OntModel oa; |
| private OntModel ore; |
| private OntModel pav; |
| |
| private OntModel prov; |
| |
| private OntModel provaq; |
| |
| private ObjectProperty proxyFor; |
| |
| private ObjectProperty proxyIn; |
| |
| private OntClass standard; |
| |
| private OntModel roterms; |
| |
| private ObjectProperty alternateOf; |
| |
| private ObjectProperty bundledAs; |
| |
| private DatatypeProperty entryName; |
| |
| private OntModel ro; |
| |
| public RDFToManifest() { |
| loadOntologies(); |
| } |
| |
| private void checkNotNull(Object... possiblyNulls) { |
| int i = 0; |
| for (Object check : possiblyNulls) { |
| if (check == null) |
| throw new IllegalStateException("Could not load item #" + i); |
| i++; |
| } |
| } |
| |
| 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 List<Agent> getAgents(URI base, Individual in, |
| ObjectProperty property) { |
| List<Agent> creators = new ArrayList<>(); |
| for (Individual agent : listObjectProperties(in, property)) { |
| Agent a = new Agent(); |
| |
| // Check for any ORCIDs, note that "orcid" is mapped as |
| // prov:alternateOf in our modified bundle.jsonld |
| for (Individual alternate : listObjectProperties(agent, alternateOf)) { |
| if (alternate.isURIResource() && ( |
| alternate.getURI().startsWith("https://orcid.org/") || |
| alternate.getURI().startsWith("http://orcid.org/"))) { |
| // TODO: Check against https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier |
| a.setOrcid(URI.create(alternate.getURI())); |
| break; |
| } |
| } |
| if (agent.isURIResource()) { |
| URI agentURI = relativizeFromBase(agent.getURI(), base); |
| if ("orcid.org".equals(agentURI.getHost()) && a.getOrcid() == null) { |
| a.setOrcid(agentURI); |
| } else { |
| a.setUri(agentURI); |
| } |
| } |
| |
| RDFNode name = agent.getPropertyValue(foafName); |
| if (name != null && name.isLiteral()) |
| a.setName(name.asLiteral().getLexicalForm()); |
| creators.add(a); |
| } |
| return creators; |
| } |
| |
| protected OntModel getOntModel() { |
| OntModel ontModel = createOntologyModel(OWL_DL_MEM_RULE_INF); |
| 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; |
| } |
| |
| 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; |
| } |
| |
| protected synchronized void loadBundle() { |
| if (bundle != null) |
| return; |
| OntModel ontModel = loadOntologyFromClasspath(BUNDLE_RDF, BUNDLE); |
| hasProxy = ontModel.getObjectProperty(BUNDLE + "hasProxy"); |
| hasAnnotation = ontModel.getObjectProperty(BUNDLE + "hasAnnotation"); |
| inFolder = ontModel.getObjectProperty(BUNDLE + "inFolder"); |
| bundledAs = ontModel.getObjectProperty(BUNDLE + "bundledAs"); |
| checkNotNull(hasProxy, hasAnnotation, inFolder, bundledAs); |
| bundle = ontModel; |
| } |
| |
| protected synchronized void loadDCT() { |
| if (dct != 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"); |
| |
| // 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 loadFOAF() { |
| if (foaf != null) |
| return; |
| |
| OntModel ontModel = loadOntologyFromClasspath(FOAF_RDF, FOAF_0_1); |
| |
| // properties from foaf |
| foafName = ontModel.getDatatypeProperty(FOAF_0_1 + "name"); |
| checkNotNull(foafName); |
| |
| foaf = ontModel; |
| } |
| |
| protected synchronized void loadOA() { |
| if (oa != null) |
| return; |
| OntModel ontModel = loadOntologyFromClasspath(OA_RDF, OA); |
| hasTarget = ontModel.getObjectProperty(OA + "hasTarget"); |
| hasBody = ontModel.getObjectProperty(OA + "hasBody"); |
| checkNotNull(hasTarget, hasBody); |
| oa = ontModel; |
| } |
| |
| protected void loadOntologies() { |
| loadDCT(); |
| loadORE(); |
| loadFOAF(); |
| loadPROVO(); |
| loadPAV(); |
| loadPROVAQ(); |
| loadOA(); |
| loadRO(); |
| loadBundle(); |
| } |
| |
| protected OntModel loadOntologyFromClasspath(String classPathUri, String uri) { |
| OntModel ontModel = 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); |
| try { |
| inStream.close(); |
| } catch (IOException e) { |
| // Shouldn't happen |
| } |
| return 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"); |
| |
| aggregates = ontModel.getObjectProperty(ORE + "aggregates"); |
| proxyFor = ontModel.getObjectProperty(ORE + "proxyFor"); |
| proxyIn = ontModel.getObjectProperty(ORE + "proxyIn"); |
| isDescribedBy = ontModel.getObjectProperty(ORE + "isDescribedBy"); |
| |
| checkNotNull(aggregation, aggregates, proxyFor, proxyIn, isDescribedBy); |
| |
| ore = ontModel; |
| } |
| |
| 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"); |
| retrievedFrom = ontModel.getObjectProperty(PAV + "retrievedFrom"); |
| retrievedBy = ontModel.getObjectProperty(PAV + "retrievedBy"); |
| retrievedOn = ontModel.getDatatypeProperty(PAV + "retrievedOn"); |
| |
| checkNotNull(createdBy, createdOn, authoredBy, authoredOn, |
| retrievedFrom, retrievedBy, retrievedOn); |
| |
| pav = ontModel; |
| } |
| |
| 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; |
| } |
| |
| protected synchronized void loadPROVO() { |
| if (prov != null) |
| return; |
| OntModel ontModel = loadOntologyFromClasspath(PROV_O_RDF, PROV_O); |
| alternateOf = ontModel.getObjectProperty(PROV + "alternateOf"); |
| checkNotNull(ontModel, alternateOf); |
| |
| prov = ontModel; |
| } |
| |
| protected synchronized void loadRO() { |
| if (ro != null) |
| return; |
| OntModel ontModel = loadOntologyFromClasspath(RO_OWL, RO); |
| entryName = ontModel.getDatatypeProperty(RO + "entryName"); |
| checkNotNull(ontModel, entryName); |
| ro = ontModel; |
| } |
| |
| public void readTo(InputStream manifestResourceAsStream, Manifest manifest, |
| URI manifestResourceBaseURI) throws IOException, RiotException { |
| OntModel model = new RDFToManifest().getOntModel(); |
| model.add(jsonLdAsJenaModel(manifestResourceAsStream, |
| manifestResourceBaseURI)); |
| |
| URI root = manifestResourceBaseURI.resolve("/"); |
| Individual ro = findRO(model, root); |
| if (ro == null) |
| throw new IOException("root ResearchObject not found - " |
| + "Not a valid RO Bundle manifest"); |
| |
| // isDescribedBy URI |
| for (Individual manifestResource : listObjectProperties(ro, |
| isDescribedBy)) { |
| String uriStr = manifestResource.getURI(); |
| if (uriStr == null) { |
| logger.warning("Skipping manifest without URI: " |
| + manifestResource); |
| continue; |
| } |
| // URI relative = relativizeFromBase(uriStr, root); |
| Path path = manifest.getBundle().getFileSystem().provider() |
| .getPath(URI.create(uriStr)); |
| manifest.getManifest().add(path); |
| } |
| |
| // createdBy |
| List<Agent> creators = getAgents(root, ro, createdBy); |
| if (!creators.isEmpty()) { |
| manifest.setCreatedBy(creators.get(0)); |
| if (creators.size() > 1) { |
| logger.warning("Ignoring additional createdBy agents"); |
| } |
| } |
| |
| // createdOn |
| RDFNode created = ro.getPropertyValue(createdOn); |
| manifest.setCreatedOn(literalAsFileTime(created)); |
| |
| // history |
| List<Path> history = new ArrayList<Path> (); |
| for (Individual histItem : listObjectProperties (ro, hasProvenance)) { |
| history.add(Bundles.uriToBundlePath(manifest.getBundle(), relativizeFromBase(histItem.getURI(), root))); |
| } |
| manifest.setHistory(history); |
| |
| // authoredBy |
| List<Agent> authors = getAgents(root, ro, authoredBy); |
| if (!authors.isEmpty()) { |
| manifest.setAuthoredBy(authors); |
| } |
| |
| // authoredOn |
| RDFNode authored = ro.getPropertyValue(authoredOn); |
| manifest.setAuthoredOn(literalAsFileTime(authored)); |
| |
| // retrievedFrom |
| RDFNode retrievedNode = ro.getPropertyValue(retrievedFrom); |
| if (retrievedNode != null) { |
| try { |
| manifest.setRetrievedFrom(new URI(retrievedNode.asResource().getURI())); |
| } catch (URISyntaxException ex) { |
| logger.log(Level.WARNING, "Error creating URI for retrievedFrom: " + |
| retrievedNode.asResource().getURI(), ex); |
| } |
| } |
| |
| // retrievedBy |
| List<Agent> retrievers = getAgents(root, ro, retrievedBy); |
| if (!retrievers.isEmpty()) { |
| manifest.setRetrievedBy(retrievers.get(0)); |
| if (retrievers.size() > 1) { |
| logger.warning("Ignoring additional retrievedBy agents"); |
| } |
| } |
| |
| // retrievedOn |
| RDFNode retrieved = ro.getPropertyValue(retrievedOn); |
| manifest.setRetrievedOn(literalAsFileTime(retrieved)); |
| |
| // conformsTo |
| for (Individual standard : listObjectProperties(ro, |
| conformsTo)) { |
| if (standard.isURIResource()) { |
| URI uri; |
| try { |
| uri = new URI(standard.getURI()); |
| } catch (URISyntaxException ex) { |
| logger.log(Level.WARNING, "Invalid URI for conformsTo: " + |
| standard, ex); |
| continue; |
| } |
| if (! manifest.getConformsTo().contains(uri)) { |
| manifest.getConformsTo().add(uri); |
| } |
| } |
| } |
| |
| // Aggregates |
| 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, root)); |
| |
| Set<Individual> proxies = listObjectProperties(aggrResource, |
| hasProxy); |
| if (proxies.isEmpty()) { |
| // FIXME: Jena does not follow OWL properties paths from hasProxy |
| proxies = listObjectProperties(aggrResource, bundledAs); |
| } |
| if (!proxies.isEmpty()) { |
| // Should really only be one anyway |
| Individual proxy = proxies.iterator().next(); |
| |
| String proxyUri = null; |
| if (proxy.getURI() != null) { |
| proxyUri = proxy.getURI(); |
| } else if (proxy.getSameAs() != null) { |
| proxyUri = proxy.getSameAs().getURI(); |
| } |
| |
| Proxy proxyInManifest = meta.getOrCreateBundledAs(); |
| if (proxyUri != null) { |
| proxyInManifest.setURI(relativizeFromBase(proxyUri, root)); |
| } |
| |
| RDFNode eName = proxy.getPropertyValue(entryName); |
| if (eName != null && eName.isLiteral()) { |
| proxyInManifest.setFilename(eName.asLiteral().getString());; |
| } |
| RDFNode folder = proxy.getPropertyValue(inFolder); |
| if (folder != null && folder.isURIResource()) { |
| URI folderUri = URI.create(folder.asResource().getURI()); |
| if (! folderUri.resolve("/").equals(manifest.getBaseURI())) { |
| logger.warning("Invalid bundledAs folder, outside base URI of RO: " + folderUri); |
| continue; |
| } |
| Path folderPath = Paths.get(folderUri); |
| // Note: folder need NOT exist in zip file, so we don't need to do |
| // Files.createDirectories(folderPath); |
| proxyInManifest.setFolder(folderPath); |
| } |
| |
| } |
| |
| // createdBy |
| creators = getAgents(root, aggrResource, createdBy); |
| if (!creators.isEmpty()) { |
| meta.setCreatedBy(creators.get(0)); |
| if (creators.size() > 1) { |
| logger.warning("Ignoring additional createdBy agents for " |
| + meta); |
| } |
| } |
| |
| // createdOn |
| meta.setCreatedOn(literalAsFileTime(aggrResource |
| .getPropertyValue(createdOn))); |
| |
| // retrievedFrom |
| RDFNode retrievedAggrNode = aggrResource.getPropertyValue(retrievedFrom); |
| if (retrievedAggrNode != null) { |
| try { |
| meta.setRetrievedFrom(new URI(retrievedAggrNode.asResource().getURI())); |
| } catch (URISyntaxException ex) { |
| logger.log(Level.WARNING, "Error creating URI for retrievedFrom: " + |
| retrievedAggrNode.asResource().getURI(), ex); |
| } |
| } |
| |
| // retrievedBy |
| List<Agent> retrieversAggr = getAgents(root, aggrResource, retrievedBy); |
| if (!retrieversAggr.isEmpty()) { |
| meta.setRetrievedBy(retrieversAggr.get(0)); |
| if (retrieversAggr.size() > 1) { |
| logger.warning("Ignoring additional retrievedBy agents for " |
| + meta); |
| } |
| } |
| |
| // retrievedOn |
| RDFNode retrievedAggr = aggrResource.getPropertyValue(retrievedOn); |
| meta.setRetrievedOn(literalAsFileTime(retrievedAggr)); |
| |
| // conformsTo |
| for (Individual standard : listObjectProperties(aggrResource, |
| conformsTo)) { |
| if (standard.getURI() != null) { |
| meta.setConformsTo(relativizeFromBase(standard.getURI(), |
| root)); |
| } |
| } |
| |
| // format |
| RDFNode mediaType = aggrResource.getPropertyValue(format); |
| if (mediaType != null && mediaType.isLiteral()) { |
| meta.setMediatype(mediaType.asLiteral().getLexicalForm()); |
| } |
| } |
| |
| for (Individual ann : listObjectProperties(ro, hasAnnotation)) { |
| /* |
| * Normally just one body per annotation, but just in case we'll |
| * iterate and split them out (as our PathAnnotation can only keep a |
| * single setContent() at a time) |
| */ |
| for (Individual body : listObjectProperties( |
| model.getOntResource(ann), hasBody)) { |
| if (! body.isURIResource()) { |
| logger.warning("Can't find annotation body for anonymous " |
| + body); |
| continue; |
| } |
| PathAnnotation pathAnn = new PathAnnotation(); |
| pathAnn.setContent(relativizeFromBase(body.getURI(), root)); |
| |
| if (ann.getURI() != null) |
| pathAnn.setUri(relativizeFromBase(ann.getURI(), root)); |
| else if (ann.getSameAs() != null |
| && ann.getSameAs().getURI() != null) |
| pathAnn.setUri(relativizeFromBase(ann.getSameAs().getURI(), |
| root)); |
| |
| // Handle multiple about/hasTarget |
| for (Individual target : listObjectProperties(ann, hasTarget)) |
| if (target.getURI() != null) |
| pathAnn.getAboutList().add( |
| relativizeFromBase(target.getURI(), root)); |
| manifest.getAnnotations().add(pathAnn); |
| } |
| } |
| } |
| } |