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