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}