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