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