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