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 the id translator
115     * @param session the 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 the JCR value from an RDFNode for a given JCR property
161     * @throws RepositoryException if repository exception occurred
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 the given value factory
173     * @param data the rdf node data
174     * @param type the given JCR type
175     * @return created value
176     * @throws RepositoryException if repository exception occurred
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 the fedora resource
211     * @param mixinResource the mixin resource
212     * @param namespaces the namespace
213     * @throws RepositoryException if repository exception occurred
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 the fedora resource
251     * @param predicate the predicate
252     * @param value the value
253     * @param namespaces the namespace
254     * @throws RepositoryException if repository exception occurred
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 the resource
291     * @param mixinResource the mixin resource
292     * @param nsPrefixMap the prefix map
293     * @throws RepositoryException if repository exception occurred
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 the fedora resource
310     * @param predicate the predicate
311     * @param objectNode the object node
312     * @param nsPrefixMap the prefix map
313     * @throws RepositoryException if repository exception occurred
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 the property of idTranslator
349     * @param t the statement
350     * @return the persistable statement
351     * @throws RepositoryException if repository exception occurred
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}