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