001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.kernel.modeshape.rdf; 019 020import static com.hp.hpl.jena.rdf.model.ModelFactory.createDefaultModel; 021import static javax.jcr.PropertyType.REFERENCE; 022import static javax.jcr.PropertyType.STRING; 023import static javax.jcr.PropertyType.UNDEFINED; 024import static javax.jcr.PropertyType.WEAKREFERENCE; 025import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_SKOLEM; 026import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_PAIRTREE; 027import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_RESOURCE; 028import static org.fcrepo.kernel.api.RdfLexicon.JCR_NAMESPACE; 029import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 030import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeToResource; 031import static org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter.getPropertyNameFromPredicate; 032import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getClosestExistingAncestor; 033import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode; 034import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getPropertyType; 035import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isReferenceProperty; 036import static org.modeshape.jcr.api.JcrConstants.NT_FOLDER; 037import static org.slf4j.LoggerFactory.getLogger; 038 039import java.util.HashMap; 040import java.util.Map; 041 042import javax.jcr.Node; 043import javax.jcr.PathNotFoundException; 044import javax.jcr.RepositoryException; 045import javax.jcr.Session; 046import javax.jcr.Value; 047import javax.jcr.ValueFormatException; 048import javax.jcr.ValueFactory; 049import javax.jcr.nodetype.NodeTypeManager; 050import javax.jcr.nodetype.NodeTypeTemplate; 051 052import com.google.common.annotations.VisibleForTesting; 053import com.hp.hpl.jena.rdf.model.AnonId; 054import com.hp.hpl.jena.rdf.model.Model; 055import com.hp.hpl.jena.rdf.model.Statement; 056 057import org.fcrepo.kernel.modeshape.services.AbstractService; 058import org.fcrepo.kernel.api.models.FedoraResource; 059import org.fcrepo.kernel.api.RdfLexicon; 060import org.fcrepo.kernel.api.exception.MalformedRdfException; 061import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 062import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 063import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 064import org.fcrepo.kernel.api.services.functions.HierarchicalIdentifierSupplier; 065import org.fcrepo.kernel.api.services.functions.UniqueValueSupplier; 066import org.fcrepo.kernel.modeshape.rdf.converters.ValueConverter; 067import org.fcrepo.kernel.modeshape.utils.NodePropertiesTools; 068 069import org.modeshape.jcr.api.JcrTools; 070import org.slf4j.Logger; 071 072import com.google.common.collect.BiMap; 073import com.google.common.collect.ImmutableBiMap; 074import com.hp.hpl.jena.rdf.model.RDFNode; 075import com.hp.hpl.jena.rdf.model.Resource; 076 077/** 078 * A set of helpful tools for converting JCR properties to RDF 079 * 080 * @author Chris Beer 081 * @author ajs6f 082 * @since May 10, 2013 083 */ 084public class JcrRdfTools { 085 086 private static final Logger LOGGER = getLogger(JcrRdfTools.class); 087 088 /** 089 * A map of JCR namespaces to Fedora's RDF namespaces 090 */ 091 public static BiMap<String, String> jcrNamespacesToRDFNamespaces = 092 ImmutableBiMap.of(JCR_NAMESPACE, 093 RdfLexicon.REPOSITORY_NAMESPACE); 094 095 /** 096 * A map of Fedora's RDF namespaces to the JCR equivalent 097 */ 098 public static BiMap<String, String> rdfNamespacesToJcrNamespaces = 099 jcrNamespacesToRDFNamespaces.inverse(); 100 101 private final IdentifierConverter<Resource, FedoraResource> idTranslator; 102 private final ValueConverter valueConverter; 103 104 private final Session session; 105 private final NodePropertiesTools nodePropertiesTools = new NodePropertiesTools(); 106 107 @VisibleForTesting 108 protected JcrTools jcrTools = new JcrTools(); 109 110 private final Map<AnonId, Resource> skolemizedBnodeMap; 111 112 private static final Model m = createDefaultModel(); 113 114 private static final UniqueValueSupplier pidMinter = new DefaultPathMinter(); 115 116 /** 117 * Constructor with even more context. 118 * 119 * @param idTranslator the id translator 120 * @param session the session 121 */ 122 public JcrRdfTools(final IdentifierConverter<Resource, FedoraResource> idTranslator, 123 final Session session) { 124 this.idTranslator = idTranslator; 125 this.session = session; 126 this.valueConverter = new ValueConverter(session, idTranslator); 127 this.skolemizedBnodeMap = new HashMap<>(); 128 } 129 130 /** 131 * Convert a Fedora RDF Namespace into its JCR equivalent 132 * 133 * @param rdfNamespaceUri a namespace from an RDF document 134 * @return the JCR namespace, or the RDF namespace if no matching JCR 135 * namespace is found 136 */ 137 public static String getJcrNamespaceForRDFNamespace( 138 final String rdfNamespaceUri) { 139 if (rdfNamespacesToJcrNamespaces.containsKey(rdfNamespaceUri)) { 140 return rdfNamespacesToJcrNamespaces.get(rdfNamespaceUri); 141 } 142 return rdfNamespaceUri; 143 } 144 145 /** 146 * Convert a JCR namespace into an RDF namespace fit for downstream 147 * consumption. 148 * 149 * @param jcrNamespaceUri a namespace from the JCR NamespaceRegistry 150 * @return an RDF namespace for downstream consumption. 151 */ 152 public static String getRDFNamespaceForJcrNamespace( 153 final String jcrNamespaceUri) { 154 if (jcrNamespacesToRDFNamespaces.containsKey(jcrNamespaceUri)) { 155 return jcrNamespacesToRDFNamespaces.get(jcrNamespaceUri); 156 } 157 return jcrNamespaceUri; 158 } 159 160 /** 161 * Create a JCR value from an RDFNode for a given JCR property 162 * @param node the JCR node we want a property for 163 * @param data an RDF Node (possibly with a DataType) 164 * @param propertyName name of the property to populate (used to use the right type for the value) 165 * @return the JCR value from an RDFNode for a given JCR property 166 * @throws RepositoryException if repository exception occurred 167 */ 168 public Value createValue(final Node node, 169 final RDFNode data, 170 final String propertyName) throws RepositoryException { 171 final ValueFactory valueFactory = node.getSession().getValueFactory(); 172 return createValue(valueFactory, data, getPropertyType(node, propertyName).orElse(UNDEFINED)); 173 } 174 175 /** 176 * Create a JCR value from an RDF node with the given JCR type 177 * @param valueFactory the given value factory 178 * @param data the rdf node data 179 * @param type the given JCR type 180 * @return created value 181 * @throws RepositoryException if repository exception occurred 182 */ 183 public Value createValue(final ValueFactory valueFactory, final RDFNode data, final int type) 184 throws RepositoryException { 185 assert (valueFactory != null); 186 187 188 if (type == UNDEFINED || type == STRING) { 189 return valueConverter.reverse().convert(data); 190 } else if (type == REFERENCE || type == WEAKREFERENCE) { 191 // reference to another node (by path) 192 if (!data.isURIResource()) { 193 throw new ValueFormatException("Reference properties can only refer to URIs, not literals"); 194 } 195 196 try { 197 final Node nodeFromGraphSubject = getJcrNode(idTranslator.convert(data.asResource())); 198 return valueFactory.createValue(nodeFromGraphSubject, type == WEAKREFERENCE); 199 } catch (final RepositoryRuntimeException e) { 200 throw new MalformedRdfException("Unable to find referenced node", e); 201 } 202 } else if (data.isResource()) { 203 LOGGER.debug("Using default JCR value creation for RDF resource: {}", 204 data); 205 return valueFactory.createValue(data.asResource().getURI(), type); 206 } else { 207 LOGGER.debug("Using default JCR value creation for RDF literal: {}", 208 data); 209 return valueFactory.createValue(data.asLiteral().getString(), type); 210 } 211 } 212 213 /** 214 * Add a mixin to a node 215 * @param resource the fedora resource 216 * @param mixinResource the mixin resource 217 * @param namespaces the namespace 218 * @throws RepositoryException if repository exception occurred 219 */ 220 public void addMixin(final FedoraResource resource, 221 final Resource mixinResource, 222 final Map<String,String> namespaces) 223 throws RepositoryException { 224 225 final Node node = getJcrNode(resource); 226 final Session session = node.getSession(); 227 final String mixinName = getPropertyNameFromPredicate(node, mixinResource, namespaces); 228 if (!repositoryHasType(session, mixinName)) { 229 final NodeTypeManager mgr = session.getWorkspace().getNodeTypeManager(); 230 final NodeTypeTemplate type = mgr.createNodeTypeTemplate(); 231 type.setName(mixinName); 232 type.setMixin(true); 233 type.setQueryable(true); 234 mgr.registerNodeType(type, false); 235 } 236 237 if (node.isNodeType(mixinName)) { 238 LOGGER.trace("Subject {} is already a {}; skipping", node, mixinName); 239 return; 240 } 241 242 if (node.canAddMixin(mixinName)) { 243 LOGGER.debug("Adding mixin: {} to node: {}.", mixinName, node.getPath()); 244 node.addMixin(mixinName); 245 } else { 246 throw new MalformedRdfException("Could not persist triple containing type assertion: " 247 + mixinResource.toString() 248 + " because no such mixin/type can be added to this node: " 249 + node.getPath() + "!"); 250 } 251 } 252 253 /** 254 * Add property to a node 255 * @param resource the fedora resource 256 * @param predicate the predicate 257 * @param value the value 258 * @param namespaces the namespace 259 * @throws RepositoryException if repository exception occurred 260 */ 261 public void addProperty(final FedoraResource resource, 262 final com.hp.hpl.jena.rdf.model.Property predicate, 263 final RDFNode value, 264 final Map<String,String> namespaces) throws RepositoryException { 265 266 final Node node = getJcrNode(resource); 267 268 if (isManagedPredicate.test(predicate)) { 269 270 throw new ServerManagedPropertyException("Could not persist triple containing predicate " 271 + predicate.toString() 272 + " to node " 273 + node.getPath()); 274 } 275 276 final String propertyName = 277 getPropertyNameFromPredicate(node, predicate, namespaces); 278 279 if (value.isURIResource() 280 && idTranslator.inDomain(value.asResource()) 281 && !isReferenceProperty(node, propertyName)) { 282 nodePropertiesTools.addReferencePlaceholders(idTranslator, node, propertyName, value.asResource()); 283 } else { 284 final Value v = createValue(node, value, propertyName); 285 nodePropertiesTools.appendOrReplaceNodeProperty(node, propertyName, v); 286 } 287 } 288 289 protected boolean repositoryHasType(final Session session, final String mixinName) throws RepositoryException { 290 return session.getWorkspace().getNodeTypeManager().hasNodeType(mixinName); 291 } 292 293 /** 294 * Remove a mixin from a node 295 * @param resource the resource 296 * @param mixinResource the mixin resource 297 * @param nsPrefixMap the prefix map 298 * @throws RepositoryException if repository exception occurred 299 */ 300 public void removeMixin(final FedoraResource resource, 301 final Resource mixinResource, 302 final Map<String, String> nsPrefixMap) throws RepositoryException { 303 304 final Node node = getJcrNode(resource); 305 final String mixinName = getPropertyNameFromPredicate(node, mixinResource, nsPrefixMap); 306 if (repositoryHasType(session, mixinName) && node.isNodeType(mixinName)) { 307 node.removeMixin(mixinName); 308 } 309 310 } 311 312 /** 313 * Remove a property from a node 314 * @param resource the fedora resource 315 * @param predicate the predicate 316 * @param objectNode the object node 317 * @param nsPrefixMap the prefix map 318 * @throws RepositoryException if repository exception occurred 319 */ 320 public void removeProperty(final FedoraResource resource, 321 final com.hp.hpl.jena.rdf.model.Property predicate, 322 final RDFNode objectNode, 323 final Map<String, String> nsPrefixMap) throws RepositoryException { 324 325 final Node node = getJcrNode(resource); 326 final String propertyName = getPropertyNameFromPredicate(node, predicate, nsPrefixMap); 327 328 if (isManagedPredicate.test(predicate)) { 329 330 throw new ServerManagedPropertyException("Could not remove triple containing predicate " 331 + predicate.toString() 332 + " to node " 333 + node.getPath()); 334 } 335 336 if (objectNode.isURIResource() 337 && idTranslator.inDomain(objectNode.asResource()) 338 && !isReferenceProperty(node, propertyName)) { 339 nodePropertiesTools.removeReferencePlaceholders(idTranslator, 340 node, 341 propertyName, 342 objectNode.asResource()); 343 } else { 344 final Value v = createValue(node, objectNode, propertyName); 345 nodePropertiesTools.removeNodeProperty(node, propertyName, v); 346 } 347 } 348 349 /** 350 * Convert an external statement into a persistable statement by skolemizing 351 * blank nodes, creating hash-uri subjects, etc 352 * 353 * @param idTranslator the property of idTranslator 354 * @param t the statement 355 * @return the persistable statement 356 * @throws RepositoryException if repository exception occurred 357 */ 358 public Statement skolemize(final IdentifierConverter<Resource, FedoraResource> idTranslator, final Statement t) 359 throws RepositoryException { 360 361 Statement skolemized = t; 362 363 if (t.getSubject().isAnon()) { 364 skolemized = m.createStatement(getSkolemizedResource(idTranslator, skolemized.getSubject()), 365 skolemized.getPredicate(), 366 skolemized.getObject()); 367 } else if (idTranslator.inDomain(t.getSubject()) && t.getSubject().getURI().contains("#")) { 368 findOrCreateHashUri(idTranslator, t.getSubject()); 369 } 370 371 if (t.getObject().isAnon()) { 372 skolemized = m.createStatement(skolemized.getSubject(), skolemized.getPredicate(), getSkolemizedResource 373 (idTranslator, skolemized.getObject())); 374 } else if (t.getObject().isResource() 375 && idTranslator.inDomain(t.getObject().asResource()) 376 && t.getObject().asResource().getURI().contains("#")) { 377 findOrCreateHashUri(idTranslator, t.getObject().asResource()); 378 } 379 380 return skolemized; 381 } 382 383 private void findOrCreateHashUri(final IdentifierConverter<Resource, FedoraResource> idTranslator, 384 final Resource s) throws RepositoryException { 385 final String absPath = idTranslator.asString(s); 386 387 if (!absPath.isEmpty() && !session.nodeExists(absPath)) { 388 final Node closestExistingAncestor = getClosestExistingAncestor(session, absPath); 389 390 final Node orCreateNode = jcrTools.findOrCreateNode(session, absPath, NT_FOLDER); 391 orCreateNode.addMixin(FEDORA_RESOURCE); 392 393 final Node parent = orCreateNode.getParent(); 394 395 if (!parent.getName().equals("#")) { 396 throw new AssertionError("Hash URI resource created with too much hierarchy: " + s); 397 } 398 399 // We require the closest node to be either "#" resource, or its parent. 400 if (!parent.equals(closestExistingAncestor) 401 && !parent.getParent().equals(closestExistingAncestor)) { 402 throw new PathNotFoundException("Unexpected request to create new resource " + s); 403 } 404 405 if (parent.isNew()) { 406 parent.addMixin(FEDORA_PAIRTREE); 407 } 408 } 409 } 410 411 private Resource getSkolemizedResource(final IdentifierConverter<Resource, FedoraResource> idTranslator, 412 final RDFNode resource) throws RepositoryException { 413 final AnonId id = resource.asResource().getId(); 414 415 if (!skolemizedBnodeMap.containsKey(id)) { 416 jcrTools.findOrCreateNode(session, skolemizedPrefix()); 417 final String pid = pidMinter.get(); 418 final String path = skolemizedPrefix() + pid; 419 final Node preexistingNode = getClosestExistingAncestor(session, path); 420 final Node orCreateNode = jcrTools.findOrCreateNode(session, path); 421 orCreateNode.addMixin(FEDORA_SKOLEM); 422 423 if (preexistingNode != null) { 424 AbstractService.tagHierarchyWithPairtreeMixin(preexistingNode, 425 orCreateNode); 426 } 427 428 final Resource skolemizedSubject = nodeToResource(idTranslator).convert(orCreateNode); 429 skolemizedBnodeMap.put(id, skolemizedSubject); 430 } 431 432 return skolemizedBnodeMap.get(id); 433 } 434 435 private static String skolemizedPrefix() { 436 return "/.well-known/genid/"; 437 } 438 439 private static class DefaultPathMinter implements HierarchicalIdentifierSupplier { } 440 441}