001/*
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.kernel.modeshape.utils;
017
018import java.util.Arrays;
019import java.util.Optional;
020import java.util.Set;
021import java.util.function.Predicate;
022
023import org.fcrepo.kernel.api.FedoraTypes;
024import org.fcrepo.kernel.api.models.FedoraResource;
025import org.fcrepo.kernel.modeshape.services.functions.AnyTypesPredicate;
026import org.modeshape.jcr.JcrRepository;
027import org.modeshape.jcr.cache.NodeKey;
028import org.slf4j.Logger;
029
030import javax.jcr.Node;
031import javax.jcr.Property;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.nodetype.NodeType;
035import javax.jcr.nodetype.PropertyDefinition;
036
037import static java.util.Arrays.stream;
038import static javax.jcr.PropertyType.REFERENCE;
039import static javax.jcr.PropertyType.WEAKREFERENCE;
040import static com.google.common.collect.ImmutableSet.of;
041import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
042import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_PRIMARY_TYPE;
043import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_NODE;
044import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
045import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATEDBY;
046import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_FROZEN_NODE;
047import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
048import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIEDBY;
049import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
050import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isBinaryContentProperty;
051import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck;
052import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
053import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE;
054import static org.modeshape.jcr.api.JcrConstants.JCR_MIXIN_TYPES;
055import static org.slf4j.LoggerFactory.getLogger;
056
057/**
058 * Convenience class with static methods for manipulating Fedora types in the
059 * JCR.
060 *
061 * @author ajs6f
062 * @since Feb 14, 2013
063 */
064public abstract class FedoraTypesUtils implements FedoraTypes {
065
066    public static final String REFERENCE_PROPERTY_SUFFIX = "_ref";
067
068    private static final Logger LOGGER = getLogger(FedoraTypesUtils.class);
069
070    private static Set<String> privateProperties = of(
071            "jcr:mime",
072            "jcr:mimeType",
073            "jcr:frozenUuid",
074            "jcr:uuid",
075            JCR_CONTENT,
076            JCR_PRIMARY_TYPE,
077            JCR_MIXIN_TYPES,
078            FROZEN_MIXIN_TYPES,
079            FROZEN_PRIMARY_TYPE);
080
081    private static Set<String> validJcrProperties = of(
082            JCR_CREATED,
083            JCR_CREATEDBY,
084            JCR_LASTMODIFIED,
085            JCR_LASTMODIFIEDBY);
086
087    /**
088     * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}.
089     */
090    public static Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER);
091
092    /**
093     * Predicate for determining whether this {@link Node} is a
094     * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}.
095     */
096    public static Predicate<Node> isNonRdfSourceDescription = new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
097
098
099    /**
100     * Predicate for determining whether this {@link Node} is a Fedora
101     * binary.
102     */
103    public static Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY);
104
105    /**
106     * Predicate for determining whether this {@link FedoraResource} has a frozen node
107     */
108    public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) ||
109            f.getPath().contains(JCR_FROZEN_NODE);
110
111    /**
112     * Predicate for determining whether this {@link Node} is a Fedora Skolem node.
113     */
114    public static Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM);
115
116    /**
117     * Check if a property is a reference property.
118     */
119    public static Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE ||
120            p.getType() == WEAKREFERENCE) &&
121            p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX));
122
123    /**
124     *  Check whether a type should be internal.
125     */
126    public static Predicate<String> hasInternalNamespace = type ->
127        type.startsWith("jcr:") || type.startsWith("mode:") || type.startsWith("nt:") ||
128            type.startsWith("mix:");
129
130    /**
131     * Predicate for determining whether a JCR property should be converted to the fedora namespace.
132     */
133    public static Predicate<String> isPublicJcrProperty = validJcrProperties::contains;
134
135    /**
136     * Check whether a property is protected (ie, cannot be modified directly) but
137     * is not one we've explicitly chosen to include.
138     */
139    private static Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> {
140        if (!p.getDefinition().isProtected()) {
141            return false;
142        } else if (p.getParent().isNodeType(FROZEN_NODE)) {
143            // everything on a frozen node is protected
144            // but we wish to display it anyway and there's
145            // another mechanism in place to make clear that
146            // things cannot be edited.
147            return false;
148        } else if (isPublicJcrProperty.test(p.getName())) {
149            return false;
150        }
151        return hasInternalNamespace.test(p.getName());
152    });
153
154    /**
155    * Check whether a property is an internal property that should be suppressed
156    * from external output.
157    */
158    public static Predicate<Property> isInternalProperty = isBinaryContentProperty
159                            .or(isProtectedAndShouldBeHidden::test)
160                            .or(uncheck(p -> privateProperties.contains(p.getName())));
161
162    /**
163     * Check if a node is "internal" and should not be exposed e.g. via the REST
164     * API
165     */
166    public static Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system"));
167
168    /**
169     * Check if a node is externally managed.
170     *
171     * Note: modeshape uses a source-workspace-identifier scheme
172     * to identify whether a node is externally-managed.
173     * Ordinary (non-external) nodes will have simple UUIDs
174     * as an identifier. These are never external nodes.
175     *
176     * External nodes will have a 7-character hex code
177     * identifying the "source", followed by another
178     * 7-character hex code identifying the "workspace", followed
179     * by a "/" and then the rest of the "identifier".
180     *
181     * Following that scheme, if a node's "source" key does not
182     * match the repository's configured store name, then it is an
183     * external node.
184     */
185    public static Predicate<Node> isExternalNode = uncheck(n ->  {
186        if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) {
187            return false;
188        } else if (n.getPrimaryNodeType().getName().equals(ROOT)) {
189            return false;
190        } else {
191            final NodeKey key = new NodeKey(n.getIdentifier());
192            final String source = NodeKey.keyForSourceName(
193                    ((JcrRepository)n.getSession().getRepository()).getConfiguration().getStoreName());
194            return !key.getSourceKey().equals(source);
195        }
196    });
197
198    /**
199     * Get the JCR property type ID for a given property name. If unsure, mark
200     * it as UNDEFINED.
201     *
202     * @param node the JCR node to add the property on
203     * @param propertyName the property name
204     * @return a PropertyType value
205     * @throws RepositoryException if repository exception occurred
206     */
207    public static Optional<Integer> getPropertyType(final Node node, final String propertyName)
208            throws RepositoryException {
209        LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node);
210        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType);
211    }
212
213    /**
214     * Determine if a given JCR property name is single- or multi- valued.
215     * If unsure, choose the least restrictive option (multivalued = true)
216     *
217     * @param node the JCR node to check
218     * @param propertyName the property name (which may or may not already exist)
219     * @return true if the property is multivalued
220     * @throws RepositoryException if repository exception occurred
221     */
222    public static boolean isMultivaluedProperty(final Node node, final String propertyName)
223            throws RepositoryException {
224        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true);
225    }
226
227    /**
228     * Get the property definition information (containing type and multi-value
229     * information)
230     *
231     * @param node the node to use for inferring the property definition
232     * @param propertyName the property name to retrieve a definition for
233     * @return a JCR PropertyDefinition, if available
234     * @throws javax.jcr.RepositoryException if repository exception occurred
235     */
236    public static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName)
237            throws RepositoryException {
238        LOGGER.debug("Looking for property name: {}", propertyName);
239        final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName());
240
241        final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions();
242        final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst();
243        return primaryCandidate.isPresent() ? primaryCandidate :
244                stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream)
245                        .filter(sameName).findFirst();
246    }
247
248    /**
249     * When we add certain URI properties, we also want to leave a reference node
250     * @param propertyName the property name
251     * @return property name as a reference
252     */
253    public static String getReferencePropertyName(final String propertyName) {
254        return propertyName + REFERENCE_PROPERTY_SUFFIX;
255    }
256
257    /**
258     * Given an internal reference node property, get the original name
259     * @param refPropertyName the reference node property name
260     * @return original property name of the reference property
261     */
262    public static String getReferencePropertyOriginalName(final String refPropertyName) {
263        final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
264        return i < 0 ? refPropertyName : refPropertyName.substring(0, i);
265    }
266
267    /**
268     * Check if a property definition is a reference property
269     * @param node the given node
270     * @param propertyName the property name
271     * @return whether a property definition is a reference property
272     * @throws RepositoryException if repository exception occurred
273     */
274    public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
275        final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName);
276
277        return propertyDefinition.isPresent() &&
278                (propertyDefinition.get().getRequiredType() == REFERENCE
279                        || propertyDefinition.get().getRequiredType() == WEAKREFERENCE);
280    }
281
282
283    /**
284     * Get the closest ancestor that current exists
285     *
286     * @param session the given session
287     * @param path the given path
288     * @return the closest ancestor that current exists
289     * @throws RepositoryException if repository exception occurred
290     */
291    public static Node getClosestExistingAncestor(final Session session, final String path)
292            throws RepositoryException {
293
294        String potentialPath = path.startsWith("/") ? path : "/" + path;
295        while (!potentialPath.isEmpty()) {
296            if (session.nodeExists(potentialPath)) {
297                return session.getNode(potentialPath);
298            }
299            potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/'));
300        }
301        return session.getRootNode();
302    }
303
304}