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.function.Predicate;
021
022import org.fcrepo.kernel.api.FedoraJcrTypes;
023import org.fcrepo.kernel.api.models.FedoraResource;
024import org.fcrepo.kernel.api.services.functions.AnyTypesPredicate;
025import org.modeshape.jcr.JcrRepository;
026import org.modeshape.jcr.cache.NodeKey;
027import org.slf4j.Logger;
028
029import javax.jcr.Node;
030import javax.jcr.Property;
031import javax.jcr.RepositoryException;
032import javax.jcr.Session;
033import javax.jcr.nodetype.NodeType;
034import javax.jcr.nodetype.PropertyDefinition;
035
036import static java.util.Arrays.stream;
037import static javax.jcr.PropertyType.REFERENCE;
038import static javax.jcr.PropertyType.WEAKREFERENCE;
039import static org.fcrepo.kernel.api.services.functions.JcrPropertyFunctions.isBinaryContentProperty;
040import static org.fcrepo.kernel.api.utils.UncheckedPredicate.uncheck;
041import static org.slf4j.LoggerFactory.getLogger;
042
043/**
044 * Convenience class with static methods for manipulating Fedora types in the
045 * JCR.
046 *
047 * @author ajs6f
048 * @since Feb 14, 2013
049 */
050public abstract class FedoraTypesUtils implements FedoraJcrTypes {
051
052    public static final String REFERENCE_PROPERTY_SUFFIX = "_ref";
053
054    private static final Logger LOGGER = getLogger(FedoraTypesUtils.class);
055
056    /**
057     * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}.
058     */
059    public static Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER);
060
061    /**
062     * Predicate for determining whether this {@link Node} is a
063     * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}.
064     */
065    public static Predicate<Node> isNonRdfSourceDescription = new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
066
067
068    /**
069     * Predicate for determining whether this {@link Node} is a Fedora
070     * binary.
071     */
072    public static Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY);
073
074    /**
075     * Predicate for determining whether this {@link FedoraResource} has a frozen node
076     */
077    public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) ||
078            f.getPath().contains(JCR_FROZEN_NODE);
079
080    /**
081     * Predicate for determining whether this {@link Node} is a Fedora Skolem node.
082     */
083    public static Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM);
084
085    /**
086     * Check if a property is a reference property.
087     */
088    public static Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE ||
089            p.getType() == WEAKREFERENCE) &&
090            p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX));
091
092    /**
093     * Check whether a property is protected (ie, cannot be modified directly) but
094     * is not one we've explicitly chosen to include.
095     */
096    public static Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> {
097        if (!p.getDefinition().isProtected()) {
098            return false;
099        } else if (p.getParent().isNodeType(FROZEN_NODE)) {
100            // everything on a frozen node is protected
101            // but we wish to display it anyway and there's
102            // another mechanism in place to make clear that
103            // things cannot be edited.
104            return false;
105        } else {
106            return !EXPOSED_PROTECTED_JCR_TYPES.stream().anyMatch(p.getName()::equals);
107        }
108    });
109
110    /**
111     * Check if a property is intentionally suppressed.
112     */
113    private static Predicate<Property> isSuppressedProperty = uncheck(p -> p.getName().equals("jcr:mimeType"));
114
115    /**
116    * Check whether a property is an internal property that should be suppressed
117    * from external output.
118    */
119    public static Predicate<Property> isInternalProperty = p -> isBinaryContentProperty.test(p) ||
120            isProtectedAndShouldBeHidden.test(p) || isSuppressedProperty.test(p);
121
122
123    /**
124     * Check if a node is "internal" and should not be exposed e.g. via the REST
125     * API
126     */
127    public static Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system"));
128
129    /**
130     * Check if a node is externally managed.
131     *
132     * Note: modeshape uses a source-workspace-identifier scheme
133     * to identify whether a node is externally-managed.
134     * Ordinary (non-external) nodes will have simple UUIDs
135     * as an identifier. These are never external nodes.
136     *
137     * External nodes will have a 7-character hex code
138     * identifying the "source", followed by another
139     * 7-character hex code identifying the "workspace", followed
140     * by a "/" and then the rest of the "identifier".
141     *
142     * Following that scheme, if a node's "source" key does not
143     * match the repository's configured store name, then it is an
144     * external node.
145     */
146    public static Predicate<Node> isExternalNode = uncheck(n ->  {
147        if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) {
148            return false;
149        } else if (n.getPrimaryNodeType().getName().equals(ROOT)) {
150            return false;
151        } else {
152            final NodeKey key = new NodeKey(n.getIdentifier());
153            final String source = NodeKey.keyForSourceName(
154                    ((JcrRepository)n.getSession().getRepository()).getConfiguration().getStoreName());
155            return !key.getSourceKey().equals(source);
156        }
157    });
158
159    /**
160     * Get the JCR property type ID for a given property name. If unsure, mark
161     * it as UNDEFINED.
162     *
163     * @param node the JCR node to add the property on
164     * @param propertyName the property name
165     * @return a PropertyType value
166     * @throws RepositoryException if repository exception occurred
167     */
168    public static Optional<Integer> getPropertyType(final Node node, final String propertyName)
169            throws RepositoryException {
170        LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node);
171        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType);
172    }
173
174    /**
175     * Determine if a given JCR property name is single- or multi- valued.
176     * If unsure, choose the least restrictive option (multivalued = true)
177     *
178     * @param node the JCR node to check
179     * @param propertyName the property name (which may or may not already exist)
180     * @return true if the property is multivalued
181     * @throws RepositoryException if repository exception occurred
182     */
183    public static boolean isMultivaluedProperty(final Node node, final String propertyName)
184            throws RepositoryException {
185        return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true);
186    }
187
188    /**
189     * Get the property definition information (containing type and multi-value
190     * information)
191     *
192     * @param node the node to use for inferring the property definition
193     * @param propertyName the property name to retrieve a definition for
194     * @return a JCR PropertyDefinition, if available
195     * @throws javax.jcr.RepositoryException if repository exception occurred
196     */
197    public static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName)
198            throws RepositoryException {
199        LOGGER.debug("Looking for property name: {}", propertyName);
200        final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName());
201
202        final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions();
203        final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst();
204        return primaryCandidate.isPresent() ? primaryCandidate :
205                stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream)
206                        .filter(sameName).findFirst();
207    }
208
209    /**
210     * When we add certain URI properties, we also want to leave a reference node
211     * @param propertyName the property name
212     * @return property name as a reference
213     */
214    public static String getReferencePropertyName(final String propertyName) {
215        return propertyName + REFERENCE_PROPERTY_SUFFIX;
216    }
217
218    /**
219     * Given an internal reference node property, get the original name
220     * @param refPropertyName the reference node property name
221     * @return original property name of the reference property
222     */
223    public static String getReferencePropertyOriginalName(final String refPropertyName) {
224        final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
225        return i < 0 ? refPropertyName : refPropertyName.substring(0, i);
226    }
227
228    /**
229     * Check if a property definition is a reference property
230     * @param node the given node
231     * @param propertyName the property name
232     * @return whether a property definition is a reference property
233     * @throws RepositoryException if repository exception occurred
234     */
235    public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
236        final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName);
237
238        return propertyDefinition.isPresent() &&
239                (propertyDefinition.get().getRequiredType() == REFERENCE
240                        || propertyDefinition.get().getRequiredType() == WEAKREFERENCE);
241    }
242
243
244    /**
245     * Get the closest ancestor that current exists
246     *
247     * @param session the given session
248     * @param path the given path
249     * @return the closest ancestor that current exists
250     * @throws RepositoryException if repository exception occurred
251     */
252    public static Node getClosestExistingAncestor(final Session session, final String path)
253            throws RepositoryException {
254
255        String potentialPath = path.startsWith("/") ? path : "/" + path;
256        while (!potentialPath.isEmpty()) {
257            if (session.nodeExists(potentialPath)) {
258                return session.getNode(potentialPath);
259            }
260            potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/'));
261        }
262        return session.getRootNode();
263    }
264
265}