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