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 =
127        new Predicate<Property>() {
128
129            @Override
130            public boolean apply(final Property p) {
131                try {
132                    if (!p.getDefinition().isProtected()) {
133                        return false;
134                    } else if (p.getParent().isNodeType(FROZEN_NODE)) {
135                        // everything on a frozen node is protected
136                        // but we wish to display it anyway and there's
137                        // another mechanism in place to make clear that
138                        // things cannot be edited.
139                        return false;
140                    } else {
141                        final String name = p.getName();
142                        for (String exposedName : EXPOSED_PROTECTED_JCR_TYPES) {
143                            if (name.equals(exposedName)) {
144                                return false;
145                            }
146                        }
147                        return true;
148                    }
149                } catch (final RepositoryException e) {
150                    throw new RepositoryRuntimeException(e);
151                }
152            }
153        };
154
155    /**
156     * Check if a node is "internal" and should not be exposed e.g. via the REST
157     * API
158     */
159    public static Predicate<Node> isInternalNode = new Predicate<Node>() {
160
161        @Override
162        public boolean apply(final Node n) {
163            checkNotNull(n, "null is neither internal nor not internal!");
164            try {
165                return n.isNodeType("mode:system");
166            } catch (final RepositoryException e) {
167                throw new RepositoryRuntimeException(e);
168            }
169        }
170    };
171
172    /**
173     * Get the JCR property type ID for a given property name. If unsure, mark
174     * it as UNDEFINED.
175     *
176     * @param node the JCR node to add the property on
177     * @param propertyName the property name
178     * @return a PropertyType value
179     * @throws RepositoryException
180     */
181    public static int getPropertyType(final Node node, final String propertyName)
182            throws RepositoryException {
183        LOGGER.debug("Getting type of property: {} from node: {}",
184                propertyName, node);
185        final PropertyDefinition def =
186                getDefinitionForPropertyName(node, propertyName);
187
188        if (def == null) {
189            return UNDEFINED;
190        }
191
192        return def.getRequiredType();
193    }
194
195    /**
196     * Determine if a given JCR property name is single- or multi- valued.
197     * If unsure, choose the least restrictive
198     * option (multivalued)
199     *
200     * @param node the JCR node to check
201     * @param propertyName the property name
202     *   (which may or may not already exist)
203     * @return true if the property is (or could be) multivalued
204     * @throws RepositoryException
205     */
206    public static boolean isMultivaluedProperty(final Node node,
207                                                final String propertyName)
208            throws RepositoryException {
209        final PropertyDefinition def =
210                getDefinitionForPropertyName(node, propertyName);
211
212        if (def == null) {
213            return true;
214        }
215
216        return def.isMultiple();
217    }
218
219    /**
220     * Get the property definition information (containing type and multi-value
221     * information)
222     *
223     * @param node the node to use for inferring the property definition
224     * @param propertyName the property name to retrieve a definition for
225     * @return a JCR PropertyDefinition, if available, or null
226     * @throws javax.jcr.RepositoryException
227     */
228    public static PropertyDefinition getDefinitionForPropertyName(final Node node,
229                                                                  final String propertyName)
230            throws RepositoryException {
231
232        final NodeType primaryNodeType = node.getPrimaryNodeType();
233        final PropertyDefinition[] propertyDefinitions = primaryNodeType.getPropertyDefinitions();
234        LOGGER.debug("Looking for property name: {}", propertyName);
235        for (final PropertyDefinition p : propertyDefinitions) {
236            LOGGER.debug("Checking property: {}", p.getName());
237            if (p.getName().equals(propertyName)) {
238                return p;
239            }
240        }
241
242        for (final NodeType nodeType : node.getMixinNodeTypes()) {
243            for (final PropertyDefinition p : nodeType.getPropertyDefinitions()) {
244                if (p.getName().equals(propertyName)) {
245                    return p;
246                }
247            }
248        }
249        return null;
250    }
251
252    /**
253     * When we add certain URI properties, we also want to leave a reference node
254     * @param propertyName
255     * @return property name as a reference
256     */
257    public static String getReferencePropertyName(final String propertyName) {
258        return propertyName + REFERENCE_PROPERTY_SUFFIX;
259    }
260
261    /**
262     * Given an internal reference node property, get the original name
263     * @param refPropertyName
264     * @return original property name of the reference property
265     */
266    public static String getReferencePropertyOriginalName(final String refPropertyName) {
267        final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
268
269        if (i < 0) {
270            return refPropertyName;
271        }
272        return refPropertyName.substring(0, i);
273    }
274
275    /**
276     * Check if a property definition is a reference property
277     * @param node
278     * @param propertyName
279     * @return
280     * @throws RepositoryException
281     */
282    public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
283        final PropertyDefinition propertyDefinition = getDefinitionForPropertyName(node, propertyName);
284
285        return propertyDefinition != null &&
286                (propertyDefinition.getRequiredType() == REFERENCE
287                        || propertyDefinition.getRequiredType() == WEAKREFERENCE);
288    }
289
290
291    /**
292     * Get the closest ancestor that current exists
293     *
294     * @param session
295     * @param path
296     * @return
297     * @throws RepositoryException
298     */
299    public static Node getClosestExistingAncestor(final Session session,
300                                                  final String path) throws RepositoryException {
301        final String[] pathSegments = path.replaceAll("^/+", "").replaceAll("/+$", "").split("/");
302
303        final StringBuilder existingAncestorPath = new StringBuilder(path.length());
304        existingAncestorPath.append("/");
305
306        final int len = pathSegments.length;
307        for (int i = 0; i != len; ++i) {
308            final String pathSegment = pathSegments[i];
309
310            if (session.nodeExists(existingAncestorPath.toString() + pathSegment)) {
311                // Add to existingAncestorPath  ...
312                existingAncestorPath.append(pathSegment);
313                if (i != (len - 1)) {
314                existingAncestorPath.append("/");
315                }
316            } else {
317                if (i != 0) {
318                    existingAncestorPath.deleteCharAt(existingAncestorPath.length() - 1);
319                }
320                break;
321            }
322
323        }
324
325        return session.getNode(existingAncestorPath.toString());
326    }
327
328}