/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.modeshape.rdf;
import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
import static org.apache.jena.rdf.model.ResourceFactory.createResource;
import static java.util.UUID.randomUUID;
import static javax.jcr.PropertyType.REFERENCE;
import static javax.jcr.PropertyType.STRING;
import static javax.jcr.PropertyType.UNDEFINED;
import static javax.jcr.PropertyType.WEAKREFERENCE;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_PAIRTREE;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_RESOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.jcrProperties;
import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.JCR_NAMESPACE;
import static org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter.getPropertyNameFromPredicate;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getClosestExistingAncestor;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getPropertyType;
import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isReferenceProperty;
import static org.modeshape.jcr.api.JcrConstants.NT_FOLDER;
import static org.slf4j.LoggerFactory.getLogger;
import java.util.HashMap;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import com.google.common.annotations.VisibleForTesting;
import org.apache.jena.rdf.model.AnonId;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Statement;
import org.fcrepo.kernel.api.models.FedoraResource;
import org.fcrepo.kernel.api.RdfLexicon;
import org.fcrepo.kernel.api.exception.MalformedRdfException;
import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
import org.fcrepo.kernel.api.exception.ServerManagedPropertyException;
import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
import org.fcrepo.kernel.modeshape.rdf.converters.ValueConverter;
import org.fcrepo.kernel.modeshape.utils.NodePropertiesTools;
import org.modeshape.jcr.api.JcrTools;
import org.slf4j.Logger;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
/**
* A set of helpful tools for converting JCR properties to RDF
*
* @author Chris Beer
* @author ajs6f
* @since May 10, 2013
*/
public class JcrRdfTools {
private static final Logger LOGGER = getLogger(JcrRdfTools.class);
/**
* A map of JCR namespaces to Fedora's RDF namespaces
*/
public static BiMap<String, String> jcrNamespacesToRDFNamespaces =
ImmutableBiMap.of(JCR_NAMESPACE,
RdfLexicon.REPOSITORY_NAMESPACE);
/**
* A map of Fedora's RDF namespaces to the JCR equivalent
*/
public static BiMap<String, String> rdfNamespacesToJcrNamespaces =
jcrNamespacesToRDFNamespaces.inverse();
private final IdentifierConverter<Resource, FedoraResource> idTranslator;
private final ValueConverter valueConverter;
private final Session session;
private final NodePropertiesTools nodePropertiesTools = new NodePropertiesTools();
@VisibleForTesting
protected JcrTools jcrTools = new JcrTools();
private final Map<AnonId, Resource> skolemizedBnodeMap;
private static final Model m = createDefaultModel();
/**
* Constructor with even more context.
*
* @param idTranslator the id translator
* @param session the session
*/
public JcrRdfTools(final IdentifierConverter<Resource, FedoraResource> idTranslator,
final Session session) {
this.idTranslator = idTranslator;
this.session = session;
this.valueConverter = new ValueConverter(session, idTranslator);
this.skolemizedBnodeMap = new HashMap<>();
}
/**
* Convert a Fedora RDF Namespace into its JCR equivalent
*
* @param rdfNamespaceUri a namespace from an RDF document
* @return the JCR namespace, or the RDF namespace if no matching JCR
* namespace is found
*/
public static String getJcrNamespaceForRDFNamespace(
final String rdfNamespaceUri) {
if (rdfNamespacesToJcrNamespaces.containsKey(rdfNamespaceUri)) {
return rdfNamespacesToJcrNamespaces.get(rdfNamespaceUri);
}
return rdfNamespaceUri;
}
/**
* Convert a JCR namespace into an RDF namespace fit for downstream
* consumption.
*
* @param jcrNamespaceUri a namespace from the JCR NamespaceRegistry
* @return an RDF namespace for downstream consumption.
*/
public static String getRDFNamespaceForJcrNamespace(
final String jcrNamespaceUri) {
if (jcrNamespacesToRDFNamespaces.containsKey(jcrNamespaceUri)) {
return jcrNamespacesToRDFNamespaces.get(jcrNamespaceUri);
}
return jcrNamespaceUri;
}
/**
* Create a JCR value from an RDFNode for a given JCR property
* @param node the JCR node we want a property for
* @param data an RDF Node (possibly with a DataType)
* @param propertyName name of the property to populate (used to use the right type for the value)
* @return the JCR value from an RDFNode for a given JCR property
* @throws RepositoryException if repository exception occurred
*/
public Value createValue(final Node node,
final RDFNode data,
final String propertyName) throws RepositoryException {
final ValueFactory valueFactory = node.getSession().getValueFactory();
return createValue(valueFactory, data, getPropertyType(node, propertyName).orElse(UNDEFINED));
}
/**
* Create a JCR value from an RDF node with the given JCR type
* @param valueFactory the given value factory
* @param data the rdf node data
* @param type the given JCR type
* @return created value
* @throws RepositoryException if repository exception occurred
*/
public Value createValue(final ValueFactory valueFactory, final RDFNode data, final int type)
throws RepositoryException {
assert (valueFactory != null);
if (type == UNDEFINED || type == STRING) {
return valueConverter.reverse().convert(data);
} else if (type == REFERENCE || type == WEAKREFERENCE) {
// reference to another node (by path)
if (!data.isURIResource()) {
throw new ValueFormatException("Reference properties can only refer to URIs, not literals");
}
try {
final Node nodeFromGraphSubject = getJcrNode(idTranslator.convert(data.asResource()));
return valueFactory.createValue(nodeFromGraphSubject, type == WEAKREFERENCE);
} catch (final RepositoryRuntimeException e) {
throw new MalformedRdfException("Unable to find referenced node", e);
}
} else if (data.isResource()) {
LOGGER.debug("Using default JCR value creation for RDF resource: {}",
data);
return valueFactory.createValue(data.asResource().getURI(), type);
} else {
LOGGER.debug("Using default JCR value creation for RDF literal: {}",
data);
return valueFactory.createValue(data.asLiteral().getString(), type);
}
}
/**
* Add a mixin to a node
* @param resource the fedora resource
* @param mixinResource the mixin resource
* @param namespaces the namespace
* @throws RepositoryException if repository exception occurred
*/
public void addMixin(final FedoraResource resource,
final Resource mixinResource,
final Map<String,String> namespaces)
throws RepositoryException {
final Node node = getJcrNode(resource);
final Session session = node.getSession();
final String mixinName = getPropertyNameFromPredicate(node, mixinResource, namespaces);
if (!repositoryHasType(session, mixinName)) {
final NodeTypeManager mgr = session.getWorkspace().getNodeTypeManager();
final NodeTypeTemplate type = mgr.createNodeTypeTemplate();
type.setName(mixinName);
type.setMixin(true);
type.setQueryable(true);
mgr.registerNodeType(type, false);
}
if (node.isNodeType(mixinName)) {
LOGGER.trace("Subject {} is already a {}; skipping", node, mixinName);
return;
}
if (node.canAddMixin(mixinName)) {
LOGGER.debug("Adding mixin: {} to node: {}.", mixinName, node.getPath());
node.addMixin(mixinName);
} else {
throw new MalformedRdfException("Could not persist triple containing type assertion: "
+ mixinResource.toString()
+ " because no such mixin/type can be added to this node: "
+ node.getPath() + "!");
}
}
/**
* Add property to a node
* @param resource the fedora resource
* @param predicate the predicate
* @param value the value
* @param namespaces the namespace
* @throws RepositoryException if repository exception occurred
*/
public void addProperty(final FedoraResource resource,
final org.apache.jena.rdf.model.Property predicate,
final RDFNode value,
final Map<String,String> namespaces) throws RepositoryException {
final Node node = getJcrNode(resource);
if (isManagedPredicate.test(predicate) || jcrProperties.contains(predicate)) {
throw new ServerManagedPropertyException("Could not persist triple containing predicate "
+ predicate.toString()
+ " to node "
+ node.getPath());
}
final String propertyName =
getPropertyNameFromPredicate(node, predicate, namespaces);
if (value.isURIResource()
&& idTranslator.inDomain(value.asResource())
&& !isReferenceProperty(node, propertyName)) {
nodePropertiesTools.addReferencePlaceholders(idTranslator, node, propertyName, value.asResource());
} else {
final Value v = createValue(node, value, propertyName);
nodePropertiesTools.appendOrReplaceNodeProperty(node, propertyName, v);
}
}
protected boolean repositoryHasType(final Session session, final String mixinName) throws RepositoryException {
return session.getWorkspace().getNodeTypeManager().hasNodeType(mixinName);
}
/**
* Remove a mixin from a node
* @param resource the resource
* @param mixinResource the mixin resource
* @param nsPrefixMap the prefix map
* @throws RepositoryException if repository exception occurred
*/
public void removeMixin(final FedoraResource resource,
final Resource mixinResource,
final Map<String, String> nsPrefixMap) throws RepositoryException {
final Node node = getJcrNode(resource);
final String mixinName = getPropertyNameFromPredicate(node, mixinResource, nsPrefixMap);
if (repositoryHasType(session, mixinName) && node.isNodeType(mixinName)) {
node.removeMixin(mixinName);
}
}
/**
* Remove a property from a node
* @param resource the fedora resource
* @param predicate the predicate
* @param objectNode the object node
* @param nsPrefixMap the prefix map
* @throws RepositoryException if repository exception occurred
*/
public void removeProperty(final FedoraResource resource,
final org.apache.jena.rdf.model.Property predicate,
final RDFNode objectNode,
final Map<String, String> nsPrefixMap) throws RepositoryException {
final Node node = getJcrNode(resource);
final String propertyName = getPropertyNameFromPredicate(node, predicate, nsPrefixMap);
if (isManagedPredicate.test(predicate) || jcrProperties.contains(predicate)) {
throw new ServerManagedPropertyException("Could not remove triple containing predicate "
+ predicate.toString()
+ " to node "
+ node.getPath());
}
if (objectNode.isURIResource()
&& idTranslator.inDomain(objectNode.asResource())
&& !isReferenceProperty(node, propertyName)) {
nodePropertiesTools.removeReferencePlaceholders(idTranslator,
node,
propertyName,
objectNode.asResource());
} else {
final Value v = createValue(node, objectNode, propertyName);
nodePropertiesTools.removeNodeProperty(node, propertyName, v);
}
}
/**
* Convert an external statement into a persistable statement by skolemizing
* blank nodes, creating hash-uri subjects, etc
*
* @param idTranslator the property of idTranslator
* @param t the statement
* @return the persistable statement
* @throws RepositoryException if repository exception occurred
*/
public Statement skolemize(final IdentifierConverter<Resource, FedoraResource> idTranslator, final Statement t,
final String topic) throws RepositoryException {
Statement skolemized = t;
if (t.getSubject().isAnon()) {
skolemized = m.createStatement(getSkolemizedResource(idTranslator, skolemized.getSubject(), topic),
skolemized.getPredicate(),
skolemized.getObject());
} else if (idTranslator.inDomain(t.getSubject()) && t.getSubject().getURI().contains("#")) {
findOrCreateHashUri(idTranslator, t.getSubject());
}
if (t.getObject().isAnon()) {
skolemized = m.createStatement(skolemized.getSubject(), skolemized.getPredicate(), getSkolemizedResource
(idTranslator, skolemized.getObject(), topic));
} else if (t.getObject().isResource()
&& idTranslator.inDomain(t.getObject().asResource())
&& t.getObject().asResource().getURI().contains("#")) {
findOrCreateHashUri(idTranslator, t.getObject().asResource());
}
return skolemized;
}
private void findOrCreateHashUri(final IdentifierConverter<Resource, FedoraResource> idTranslator,
final Resource s) throws RepositoryException {
final String absPath = idTranslator.asString(s);
if (!absPath.isEmpty() && !session.nodeExists(absPath)) {
final Node closestExistingAncestor = getClosestExistingAncestor(session, absPath);
final Node orCreateNode = jcrTools.findOrCreateNode(session, absPath, NT_FOLDER);
orCreateNode.addMixin(FEDORA_RESOURCE);
final Node parent = orCreateNode.getParent();
if (!parent.getName().equals("#")) {
throw new AssertionError("Hash URI resource created with too much hierarchy: " + s);
}
// We require the closest node to be either "#" resource, or its parent.
if (!parent.equals(closestExistingAncestor)
&& !parent.getParent().equals(closestExistingAncestor)) {
throw new PathNotFoundException("Unexpected request to create new resource " + s);
}
if (parent.isNew()) {
parent.addMixin(FEDORA_PAIRTREE);
}
}
}
private Resource getSkolemizedResource(final IdentifierConverter<Resource, FedoraResource> idTranslator,
final RDFNode resource, final String topic) throws RepositoryException {
final AnonId id = resource.asResource().getId();
if (!skolemizedBnodeMap.containsKey(id)) {
final String base = idTranslator.asString(createResource(topic));
final Resource skolemizedSubject = idTranslator.toDomain(base + "#" + blankNodeIdentifier());
findOrCreateHashUri(idTranslator, skolemizedSubject);
skolemizedBnodeMap.put(id, skolemizedSubject);
}
return skolemizedBnodeMap.get(id);
}
private String blankNodeIdentifier() {
return "genid" + randomUUID().toString();
}
}