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