001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.kernel.modeshape.utils;
019
020import java.net.URI;
021import java.util.Arrays;
022import java.util.Calendar;
023import java.util.Optional;
024import java.util.Set;
025import java.util.function.Function;
026import java.util.function.Predicate;
027
028import org.apache.jena.rdf.model.Resource;
029import org.fcrepo.kernel.api.FedoraTypes;
030import org.fcrepo.kernel.api.exception.AccessDeniedException;
031import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
032import org.fcrepo.kernel.api.models.FedoraResource;
033import org.fcrepo.kernel.modeshape.FedoraResourceImpl;
034import org.fcrepo.kernel.modeshape.services.functions.AnyTypesPredicate;
035import org.modeshape.jcr.JcrRepository;
036import org.modeshape.jcr.cache.NodeKey;
037import org.slf4j.Logger;
038
039import javax.jcr.NamespaceRegistry;
040import javax.jcr.Node;
041import javax.jcr.Property;
042import javax.jcr.RepositoryException;
043import javax.jcr.Session;
044import javax.jcr.nodetype.NodeType;
045import javax.jcr.nodetype.PropertyDefinition;
046
047import static java.util.Arrays.stream;
048import static java.util.Calendar.getInstance;
049import static java.util.Optional.empty;
050import static java.util.TimeZone.getTimeZone;
051import static javax.jcr.PropertyType.REFERENCE;
052import static javax.jcr.PropertyType.WEAKREFERENCE;
053import static com.google.common.collect.ImmutableSet.of;
054import static org.apache.jena.rdf.model.ResourceFactory.createResource;
055import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
056import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
057import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_PRIMARY_TYPE;
058import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_NODE;
059import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
060import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATEDBY;
061import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_FROZEN_NODE;
062import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
063import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIEDBY;
064import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
065import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isBinaryContentProperty;
066import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
067import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck;
068import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
069import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE;
070import static org.modeshape.jcr.api.JcrConstants.JCR_MIXIN_TYPES;
071import static org.slf4j.LoggerFactory.getLogger;
072
073/**
074 * Convenience class with static methods for manipulating Fedora types in the
075 * JCR.
076 *
077 * @author ajs6f
078 * @since Feb 14, 2013
079 */
080public abstract class FedoraTypesUtils implements FedoraTypes {
081
082    private static final String REFERENCE_PROPERTY_SUFFIX = "_ref";
083
084    private static final Logger LOGGER = getLogger(FedoraTypesUtils.class);
085
086    private static final Set<String> privateProperties = of(
087            "jcr:mime",
088            "jcr:mimeType",
089            "jcr:frozenUuid",
090            "jcr:uuid",
091            JCR_CONTENT,
092            JCR_PRIMARY_TYPE,
093            JCR_LASTMODIFIED,
094            JCR_LASTMODIFIEDBY,
095            JCR_CREATED,
096            JCR_CREATEDBY,
097            JCR_MIXIN_TYPES,
098            FROZEN_MIXIN_TYPES,
099            FROZEN_PRIMARY_TYPE,
100            MEMENTO_DATETIME);
101
102    private static final Set<String> validJcrProperties = of(
103            JCR_CREATED,
104            JCR_CREATEDBY,
105            JCR_LASTMODIFIED,
106            JCR_LASTMODIFIEDBY);
107
108    /**
109     * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}.
110     */
111    public static final Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER);
112
113    /**
114     * Predicate for determining whether this {@link Node} is a
115     * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}.
116     */
117    public static final Predicate<Node> isNonRdfSourceDescription =
118            new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
119
120
121    /**
122     * Predicate for determining whether this {@link Node} is a Fedora
123     * binary.
124     */
125    public static final Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY);
126
127    /**
128     * Predicate for determining whether this {@link FedoraResource} has a frozen node
129     */
130    public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) ||
131            f.getPath().contains(JCR_FROZEN_NODE);
132
133    /**
134     * Predicate for determining whether this {@link Node} is a Fedora Skolem node.
135     */
136    public static final Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM);
137
138    /**
139     * Predicate for determining whether this {@link Node} is a Memento.
140     */
141    public static final Predicate<Node> isMemento = new AnyTypesPredicate(MEMENTO);
142
143
144    /**
145     * Predicate for determining whether this {@link Node} is an Web ACL.
146     */
147    public static final Predicate<Node> isAcl = new AnyTypesPredicate(FEDORA_WEBAC_ACL);
148
149    /**
150     * Check if a property is a reference property.
151     */
152    public static final Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE ||
153            p.getType() == WEAKREFERENCE) &&
154            p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX));
155
156    /**
157     *  Check whether a type should be internal.
158     */
159    public static final Predicate<String> hasInternalNamespace = type ->
160        type.startsWith("jcr:") || type.startsWith("mode:") || type.startsWith("nt:") ||
161            type.startsWith("mix:");
162
163    /**
164     * Predicate for determining whether a JCR property should be converted to the fedora namespace.
165     */
166    private static final Predicate<String> isPublicJcrProperty = validJcrProperties::contains;
167
168    /**
169     * Check whether a property is protected (ie, cannot be modified directly) but
170     * is not one we've explicitly chosen to include.
171     */
172    private static final Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> {
173        if (!p.getDefinition().isProtected()) {
174            return false;
175        } else if (p.getParent().isNodeType(FROZEN_NODE)) {
176            // everything on a frozen node is protected
177            // but we wish to display it anyway and there's
178            // another mechanism in place to make clear that
179            // things cannot be edited.
180            return false;
181        } else if (isPublicJcrProperty.test(p.getName())) {
182            return false;
183        }
184        return hasInternalNamespace.test(p.getName());
185    });
186
187    /**
188    * Check whether a property is an internal property that should be suppressed
189    * from external output.
190    */
191    public static final Predicate<Property> isInternalProperty = isBinaryContentProperty
192                            .or(isProtectedAndShouldBeHidden::test)
193                            .or(uncheck(p -> privateProperties.contains(p.getName())));
194
195    /**
196     * Check whether a type is an internal type that should be suppressed from external output.
197     */
198    public static final Predicate<URI> isInternalType = t -> t.toString().equals(MEMENTO_TYPE);
199
200    /**
201     * A functional predicate to check whether a property is a JCR property that should be exposed.
202     * Historically we exposed JCR properties when they seemed to match a fedora property we wanted to track,
203     * but when control over the property became a requirement, we introduced the direct storage
204     * of fedora properties that when present should overrule the JCR property.
205     */
206    public static class IsExposedJCRPropertyPredicate implements Predicate<Property> {
207
208        private final FedoraResource subject;
209
210        /**
211         * Constructs this functional predicate for testing properties on the given
212         * resource.
213         * @param resource the resource whose properties can be tested by this predicate
214         */
215        public IsExposedJCRPropertyPredicate(final FedoraResource resource) {
216            subject = resource;
217        }
218
219        @Override
220        public boolean test(final Property prop) {
221            try {
222                return (prop.getName().equals(JCR_LASTMODIFIED) && !subject.hasProperty(FEDORA_LASTMODIFIED))
223                        || (prop.getName().equals(JCR_LASTMODIFIEDBY) && !subject.hasProperty(FEDORA_LASTMODIFIEDBY))
224                        || (prop.getName().equals(JCR_CREATED) && !subject.hasProperty(FEDORA_CREATED))
225                        || (prop.getName().equals(JCR_CREATEDBY) && !subject.hasProperty(FEDORA_CREATEDBY));
226            } catch (final RepositoryException e) {
227                throw new RepositoryRuntimeException(e);
228            }
229        }
230    }
231
232    /**
233     * Check if a node is "internal" and should not be exposed e.g. via the REST
234     * API
235     */
236    public static final Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system"));
237
238    /**
239     * Check if a node is externally managed.
240     *
241     * Note: modeshape uses a source-workspace-identifier scheme
242     * to identify whether a node is externally-managed.
243     * Ordinary (non-external) nodes will have simple UUIDs
244     * as an identifier. These are never external nodes.
245     *
246     * External nodes will have a 7-character hex code
247     * identifying the "source", followed by another
248     * 7-character hex code identifying the "workspace", followed
249     * by a "/" and then the rest of the "identifier".
250     *
251     * Following that scheme, if a node's "source" key does not
252     * match the repository's configured store name, then it is an
253     * external node.
254     */
255    public static final Predicate<Node> isExternalNode = uncheck(n ->  {
256        if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) {
257            return false;
258        } else if (n.getPrimaryNodeType().getName().equals(ROOT)) {
259            return false;
260        } else {
261            final NodeKey key = new NodeKey(n.getIdentifier());
262            final String source = NodeKey.keyForSourceName(
263                    ((JcrRepository)n.getSession().getRepository()).getConfiguration().getName());
264            return !key.getSourceKey().equals(source);
265        }
266    });
267
268    /**
269     * Get the JCR property type ID for a given property name. If unsure, mark
270     * it as UNDEFINED.
271     *
272     * @param node the JCR node to add the property on
273     * @param propertyName the property name
274     * @return a PropertyType value
275     * @throws RepositoryException if repository exception occurred
276     */
277    public static Optional<Integer> getPropertyType(final Node node, final String propertyName)
278            throws RepositoryException {
279        LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node);
280        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType);
281    }
282
283    /**
284     * Determine if a given JCR property name is single- or multi- valued.
285     * If unsure, choose the least restrictive option (multivalued = true)
286     *
287     * @param node the JCR node to check
288     * @param propertyName the property name (which may or may not already exist)
289     * @return true if the property is multivalued
290     * @throws RepositoryException if repository exception occurred
291     */
292    public static boolean isMultivaluedProperty(final Node node, final String propertyName)
293            throws RepositoryException {
294        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true);
295    }
296
297    /**
298     * Get the property definition information (containing type and multi-value
299     * information)
300     *
301     * @param node the node to use for inferring the property definition
302     * @param propertyName the property name to retrieve a definition for
303     * @return a JCR PropertyDefinition, if available
304     * @throws javax.jcr.RepositoryException if repository exception occurred
305     */
306    private static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName)
307            throws RepositoryException {
308        LOGGER.debug("Looking for property name: {}", propertyName);
309        final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName());
310
311        final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions();
312        final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst();
313        return primaryCandidate.isPresent() ? primaryCandidate :
314                stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream)
315                        .filter(sameName).findFirst();
316    }
317
318    /**
319     * When we add certain URI properties, we also want to leave a reference node
320     * @param propertyName the property name
321     * @return property name as a reference
322     */
323    public static String getReferencePropertyName(final String propertyName) {
324        return propertyName + REFERENCE_PROPERTY_SUFFIX;
325    }
326
327    /**
328     * Given an internal reference node property, get the original name
329     * @param refPropertyName the reference node property name
330     * @return original property name of the reference property
331     */
332    public static String getReferencePropertyOriginalName(final String refPropertyName) {
333        final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
334        return i < 0 ? refPropertyName : refPropertyName.substring(0, i);
335    }
336
337    /**
338     * Check if a property definition is a reference property
339     * @param node the given node
340     * @param propertyName the property name
341     * @return whether a property definition is a reference property
342     * @throws RepositoryException if repository exception occurred
343     */
344    public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
345        final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName);
346
347        return propertyDefinition.isPresent() &&
348                (propertyDefinition.get().getRequiredType() == REFERENCE
349                        || propertyDefinition.get().getRequiredType() == WEAKREFERENCE);
350    }
351
352
353    /**
354     * Get the closest ancestor that current exists
355     *
356     * @param session the given session
357     * @param path the given path
358     * @return the closest ancestor that current exists
359     * @throws RepositoryException if repository exception occurred
360     */
361    public static Node getClosestExistingAncestor(final Session session, final String path)
362            throws RepositoryException {
363
364        String potentialPath = path.startsWith("/") ? path : "/" + path;
365        while (!potentialPath.isEmpty()) {
366            if (session.nodeExists(potentialPath)) {
367                return session.getNode(potentialPath);
368            }
369            potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/'));
370        }
371        return session.getRootNode();
372    }
373
374    /**
375     * Retrieve the underlying JCR Node from the FedoraResource
376     *
377     * @param resource the Fedora resource
378     * @return the JCR Node
379     */
380    public static Node getJcrNode(final FedoraResource resource) {
381        if (resource instanceof FedoraResourceImpl) {
382            return ((FedoraResourceImpl)resource).getNode();
383        }
384        throw new IllegalArgumentException("FedoraResource is of the wrong type");
385    }
386
387    /**
388     * Given a JCR Node, fetch the parent's ldp:insertedContentRelation value, if
389     * one exists.
390     *
391     * @param node the JCR Node
392     * @return the ldp:insertedContentRelation Resource, if one exists.
393     */
394    public static Optional<Resource> ldpInsertedContentProperty(final Node node) {
395        return getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE) &&
396                parent.isNodeType(LDP_INDIRECT_CONTAINER) && parent.hasProperty(LDP_INSERTED_CONTENT_RELATION)))
397            .map(UncheckedFunction.uncheck(parent ->
398                        createResource(parent.getProperty(LDP_INSERTED_CONTENT_RELATION).getString())));
399    }
400
401    /**
402     * Using a JCR session, return a function that maps an RDF Resource to a corresponding property name.
403     *
404     * @param session The JCR session
405     * @return a Function that maps a Resource to an Optional-wrapped String
406     */
407    public static Function<Resource, Optional<String>> resourceToProperty(final Session session) {
408        return resource -> {
409            try {
410                final NamespaceRegistry registry = getNamespaceRegistry(session);
411                return Optional.of(registry.getPrefix(resource.getNameSpace()) + ":" + resource.getLocalName());
412            } catch (final RepositoryException ex) {
413                LOGGER.debug("Could not resolve resource namespace ({}): {}", resource.toString(), ex.getMessage());
414            }
415            return empty();
416        };
417    }
418
419    /**
420     * Update the fedora:lastModified date and fedora:lastModifiedBy of the parent's ldp:membershipResource if that
421     * node is a direct or indirect container, provided the LDP constraints are valid.
422     *
423     * @param node The JCR node
424     */
425    public static void touchLdpMembershipResource(final Node node) {
426        touchLdpMembershipResource(node, null, null);
427    }
428
429    /**
430     * Update the fedora:lastModified date and fedora:lastModifiedBy of the parent's ldp:membershipResource if that
431     * node is a direct or indirect container, provided the LDP constraints are valid.
432     *
433     * @param node The JCR node
434     * @param date the date the modification was supposed to have occurred or null to indicate now
435     * @param user the user who performed the action or null to indicate the user associated with the current session
436     */
437    public static void touchLdpMembershipResource(final Node node, final Calendar date, final String user) {
438        getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE))).ifPresent(parent -> {
439            try {
440                final Optional<String> hasInsertedContentProperty = ldpInsertedContentProperty(node)
441                        .flatMap(resourceToProperty(node.getSession())).filter(uncheck(node::hasProperty));
442                if (parent.isNodeType(LDP_DIRECT_CONTAINER) ||
443                        (parent.isNodeType(LDP_INDIRECT_CONTAINER) && hasInsertedContentProperty.isPresent())) {
444                    touch(parent.getProperty(LDP_MEMBER_RESOURCE).getNode(), date, user);
445                }
446            } catch (final javax.jcr.AccessDeniedException ex) {
447                throw new AccessDeniedException(ex);
448            } catch (final RepositoryException ex) {
449                throw new RepositoryRuntimeException(ex);
450            }
451        });
452    }
453
454    /**
455     * Updates the LAST_MODIFIED_DATE and LAST_MODIFIED_BY properties to now, and the current user
456     * respectively.
457     *
458     * @param node The JCR node
459     *
460     */
461    public static void touch(final Node node) {
462        touch(node, null, null, null, null);
463    }
464
465    /**
466     * Updates the LAST_MODIFIED_DATE and LAST_MODIFIED_BY properties to the provided values.
467     *
468     * @param node The JCR node
469     * @param modified the modification date, or null if not explicitly set
470     * @param modifyingUser the userID who modified this resource or null if not explicitly set
471     *
472     */
473    private static void touch(final Node node, final Calendar modified, final String modifyingUser) {
474        touch(node, null, null, modified, modifyingUser);
475    }
476
477    /**
478     * Updates the LAST_MODIFIED_DATE, LAST_MODIFIED_BY, CREATED_DATE and CREATED_BY properties to the provided values.
479     *
480     * @param node The JCR node
481     * @param created the date the resource was created, or null if not explicitly set
482     * @param creatingUser the userID of created this resource or null if not explicitly set
483     * @param modified the modification date, or null if not explicitly set
484     * @param modifyingUser the userID who modified this resource or null if not explicitly set
485     *
486     */
487    public static void touch(final Node node, final Calendar created, final String creatingUser,
488                             final Calendar modified, final String modifyingUser) {
489        try {
490            if (created != null) {
491                node.setProperty(FEDORA_CREATED, created);
492            }
493
494            if (creatingUser != null) {
495                node.setProperty(FEDORA_CREATEDBY, creatingUser);
496            }
497
498            if (modified != null) {
499                node.setProperty(FEDORA_LASTMODIFIED, modified);
500            } else {
501                node.setProperty(FEDORA_LASTMODIFIED, getInstance(getTimeZone("UTC")));
502            }
503
504            if (modifyingUser != null) {
505                node.setProperty(FEDORA_LASTMODIFIEDBY, modifyingUser);
506            } else {
507                // revert to the modeshape-managed property
508                if (node.hasProperty(FEDORA_LASTMODIFIEDBY)) {
509                    node.getProperty(FEDORA_LASTMODIFIEDBY).remove();
510                }
511            }
512        } catch (final javax.jcr.AccessDeniedException ex) {
513            throw new AccessDeniedException(ex);
514        } catch (final RepositoryException ex) {
515            throw new RepositoryRuntimeException(ex);
516        }
517    }
518
519    /**
520     * Get the JCR Node that corresponds to the containing node in the repository.
521     * This may be the direct parent node, but it may also be a more distant ancestor.
522     *
523     * @param node the JCR node
524     * @return the containing node, if one is present
525     */
526    public static Optional<Node> getContainingNode(final Node node) {
527        try {
528            if (node.getDepth() == 0) {
529                return empty();
530            }
531
532            // check ancestors recursively only either of the following two cases applies:
533            // 1. the PARENT is a FEDORA_PAIRTREE
534            // 2. the PARENT is FEDORA_NON_RDF_SOURCE_DESCRIPTION
535            final Node parent = node.getParent();
536            if (parent.isNodeType(FEDORA_PAIRTREE) || parent.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) {
537                return getContainingNode(parent);
538            }
539            return Optional.of(parent);
540        } catch (final RepositoryException ex) {
541            throw new RepositoryRuntimeException(ex);
542        }
543    }
544}