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