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.impl.utils;
017
018import com.google.common.base.Predicate;
019import org.fcrepo.kernel.FedoraJcrTypes;
020import org.fcrepo.kernel.models.FedoraResource;
021import org.fcrepo.kernel.exception.RepositoryRuntimeException;
022import org.fcrepo.kernel.services.functions.AnyTypesPredicate;
023import org.fcrepo.kernel.services.functions.JcrPropertyFunctions;
024import org.slf4j.Logger;
025
026import javax.jcr.Node;
027import javax.jcr.Property;
028import javax.jcr.RepositoryException;
029import javax.jcr.Session;
030import javax.jcr.nodetype.NodeType;
031import javax.jcr.nodetype.PropertyDefinition;
032
033import static com.google.common.base.Preconditions.checkNotNull;
034import static javax.jcr.PropertyType.REFERENCE;
035import static javax.jcr.PropertyType.UNDEFINED;
036import static javax.jcr.PropertyType.WEAKREFERENCE;
037import static org.slf4j.LoggerFactory.getLogger;
038
039/**
040 * Convenience class with static methods for manipulating Fedora types in the
041 * JCR.
042 *
043 * @author ajs6f
044 * @since Feb 14, 2013
045 */
046public abstract class FedoraTypesUtils implements FedoraJcrTypes {
047
048    public static final String REFERENCE_PROPERTY_SUFFIX = "_ref";
049
050    private static final Logger LOGGER = getLogger(FedoraTypesUtils.class);
051
052    /**
053     * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.models.Container}.
054     */
055    public static Predicate<Node> isContainer =
056            new AnyTypesPredicate(FEDORA_CONTAINER);
057
058    /**
059     * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.models.NonRdfSourceDescription}
060     */
061    public static Predicate<Node> isNonRdfSourceDescription =
062            new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
063
064
065    /**
066     * Predicate for determining whether this {@link Node} is a Fedora
067     * binary.
068     */
069    public static Predicate<Node> isFedoraBinary =
070            new AnyTypesPredicate(FEDORA_BINARY);
071
072    /**
073     * Predicate for determining whether this {@link FedoraResource} has a frozen node
074     */
075    public static Predicate<FedoraResource> isFrozenNode =
076            new Predicate<FedoraResource>() {
077
078                @Override
079                public boolean apply(final FedoraResource f) {
080                    return f.hasType(FROZEN_NODE) || f.getPath().contains(JCR_FROZEN_NODE);
081                }
082     };
083
084    /**
085     * Predicate for determining whether this {@link Node} is a Fedora
086     * binary.
087     */
088    public static Predicate<Node> isBlankNode =
089            new AnyTypesPredicate(FEDORA_BLANKNODE);
090
091    /**
092     * Check if a property is a reference property.
093     */
094    public static Predicate<Property> isInternalReferenceProperty =
095        new Predicate<Property>() {
096
097            @Override
098            public boolean apply(final Property p) {
099                try {
100                    return (p.getType() == REFERENCE || p.getType() == WEAKREFERENCE)
101                        && p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX);
102                } catch (final RepositoryException e) {
103                    throw new RepositoryRuntimeException(e);
104                }
105            }
106        };
107
108    /**
109    * Check whether a property is an internal property that should be suppressed
110    * from external output.
111    */
112    public static Predicate<Property> isInternalProperty =
113        new Predicate<Property>() {
114
115            @Override
116            public boolean apply(final Property p) {
117                return JcrPropertyFunctions.isBinaryContentProperty.apply(p)
118                        || isProtectedAndShouldBeHidden.apply(p);
119            }
120        };
121
122    /**
123     * Check whether a property is protected (ie, cannot be modified directly) but
124     * is not one we've explicitly chosen to include.
125     */
126    public static Predicate<Property> isProtectedAndShouldBeHidden = new ProtectedAndHiddenCheck();
127
128    private static class ProtectedAndHiddenCheck implements  Predicate<Property> {
129
130        @Override
131        public boolean apply(final Property p) {
132            try {
133                if (!p.getDefinition().isProtected()) {
134                    return false;
135                } else if (p.getParent().isNodeType(FROZEN_NODE)) {
136                    // everything on a frozen node is protected
137                    // but we wish to display it anyway and there's
138                    // another mechanism in place to make clear that
139                    // things cannot be edited.
140                    return false;
141                } else {
142                    final String name = p.getName();
143                    for (String exposedName : EXPOSED_PROTECTED_JCR_TYPES) {
144                        if (name.equals(exposedName)) {
145                            return false;
146                        }
147                    }
148                    return true;
149                }
150            } catch (final RepositoryException e) {
151                throw new RepositoryRuntimeException(e);
152            }
153        }
154    }
155
156    /**
157     * Check if a node is "internal" and should not be exposed e.g. via the REST
158     * API
159     */
160    public static Predicate<Node> isInternalNode = new Predicate<Node>() {
161
162        @Override
163        public boolean apply(final Node n) {
164            checkNotNull(n, "null is neither internal nor not internal!");
165            try {
166                return n.isNodeType("mode:system");
167            } catch (final RepositoryException e) {
168                throw new RepositoryRuntimeException(e);
169            }
170        }
171    };
172
173    /**
174     * Get the JCR property type ID for a given property name. If unsure, mark
175     * it as UNDEFINED.
176     *
177     * @param node the JCR node to add the property on
178     * @param propertyName the property name
179     * @return a PropertyType value
180     * @throws RepositoryException if repository exception occurred
181     */
182    public static int getPropertyType(final Node node, final String propertyName)
183            throws RepositoryException {
184        LOGGER.debug("Getting type of property: {} from node: {}",
185                propertyName, node);
186        final PropertyDefinition def =
187                getDefinitionForPropertyName(node, propertyName);
188
189        if (def == null) {
190            return UNDEFINED;
191        }
192
193        return def.getRequiredType();
194    }
195
196    /**
197     * Determine if a given JCR property name is single- or multi- valued.
198     * If unsure, choose the least restrictive
199     * option (multivalued)
200     *
201     * @param node the JCR node to check
202     * @param propertyName the property name
203     *   (which may or may not already exist)
204     * @return true if the property is (or could be) multivalued
205     * @throws RepositoryException if repository exception occurred
206     */
207    public static boolean isMultivaluedProperty(final Node node,
208                                                final String propertyName)
209            throws RepositoryException {
210        final PropertyDefinition def =
211                getDefinitionForPropertyName(node, propertyName);
212
213        if (def == null) {
214            return true;
215        }
216
217        return def.isMultiple();
218    }
219
220    /**
221     * Get the property definition information (containing type and multi-value
222     * information)
223     *
224     * @param node the node to use for inferring the property definition
225     * @param propertyName the property name to retrieve a definition for
226     * @return a JCR PropertyDefinition, if available, or null
227     * @throws javax.jcr.RepositoryException if repository exception occurred
228     */
229    public static PropertyDefinition getDefinitionForPropertyName(final Node node,
230                                                                  final String propertyName)
231            throws RepositoryException {
232
233        final NodeType primaryNodeType = node.getPrimaryNodeType();
234        final PropertyDefinition[] propertyDefinitions = primaryNodeType.getPropertyDefinitions();
235        LOGGER.debug("Looking for property name: {}", propertyName);
236        for (final PropertyDefinition p : propertyDefinitions) {
237            LOGGER.debug("Checking property: {}", p.getName());
238            if (p.getName().equals(propertyName)) {
239                return p;
240            }
241        }
242
243        for (final NodeType nodeType : node.getMixinNodeTypes()) {
244            for (final PropertyDefinition p : nodeType.getPropertyDefinitions()) {
245                if (p.getName().equals(propertyName)) {
246                    return p;
247                }
248            }
249        }
250        return null;
251    }
252
253    /**
254     * When we add certain URI properties, we also want to leave a reference node
255     * @param propertyName the property name
256     * @return property name as a reference
257     */
258    public static String getReferencePropertyName(final String propertyName) {
259        return propertyName + REFERENCE_PROPERTY_SUFFIX;
260    }
261
262    /**
263     * Given an internal reference node property, get the original name
264     * @param refPropertyName the reference node property name
265     * @return original property name of the reference property
266     */
267    public static String getReferencePropertyOriginalName(final String refPropertyName) {
268        final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
269
270        if (i < 0) {
271            return refPropertyName;
272        }
273        return refPropertyName.substring(0, i);
274    }
275
276    /**
277     * Check if a property definition is a reference property
278     * @param node the given node
279     * @param propertyName the property name
280     * @return whether a property definition is a reference property
281     * @throws RepositoryException if repository exception occurred
282     */
283    public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
284        final PropertyDefinition propertyDefinition = getDefinitionForPropertyName(node, propertyName);
285
286        return propertyDefinition != null &&
287                (propertyDefinition.getRequiredType() == REFERENCE
288                        || propertyDefinition.getRequiredType() == WEAKREFERENCE);
289    }
290
291
292    /**
293     * Get the closest ancestor that current exists
294     *
295     * @param session the given session
296     * @param path the given path
297     * @return the closest ancestor that current exists
298     * @throws RepositoryException if repository exception occurred
299     */
300    public static Node getClosestExistingAncestor(final Session session,
301                                                  final String path) throws RepositoryException {
302        final String[] pathSegments = path.replaceAll("^/+", "").replaceAll("/+$", "").split("/");
303
304        final StringBuilder existingAncestorPath = new StringBuilder(path.length());
305        existingAncestorPath.append("/");
306
307        final int len = pathSegments.length;
308        for (int i = 0; i != len; ++i) {
309            final String pathSegment = pathSegments[i];
310
311            if (session.nodeExists(existingAncestorPath.toString() + pathSegment)) {
312                // Add to existingAncestorPath  ...
313                existingAncestorPath.append(pathSegment);
314                if (i != (len - 1)) {
315                existingAncestorPath.append("/");
316                }
317            } else {
318                if (i != 0) {
319                    existingAncestorPath.deleteCharAt(existingAncestorPath.length() - 1);
320                }
321                break;
322            }
323
324        }
325
326        return session.getNode(existingAncestorPath.toString());
327    }
328
329}