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.ROOT; 049import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.VERSIONABLE; 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> protectedProperties = of( 082 JCR_CREATED, 083 JCR_CREATEDBY, 084 JCR_LASTMODIFIED); 085 086 private static Set<String> protectedTypes = of( 087 "mix:created", 088 "mix:lastModified", 089 "mix:referenceable", 090 "mix:simpleVersionable", 091 "nt:base", 092 "nt:folder", 093 "nt:hierarchyNode", 094 VERSIONABLE, 095 ROOT, 096 FROZEN_NODE); 097 098 /** 099 * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}. 100 */ 101 public static Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER); 102 103 /** 104 * Predicate for determining whether this {@link Node} is a 105 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}. 106 */ 107 public static Predicate<Node> isNonRdfSourceDescription = new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION); 108 109 110 /** 111 * Predicate for determining whether this {@link Node} is a Fedora 112 * binary. 113 */ 114 public static Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY); 115 116 /** 117 * Predicate for determining whether this {@link FedoraResource} has a frozen node 118 */ 119 public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) || 120 f.getPath().contains(JCR_FROZEN_NODE); 121 122 /** 123 * Predicate for determining whether this {@link Node} is a Fedora Skolem node. 124 */ 125 public static Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM); 126 127 /** 128 * Check if a property is a reference property. 129 */ 130 public static Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE || 131 p.getType() == WEAKREFERENCE) && 132 p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX)); 133 134 /** 135 * Check whether a property is protected (ie, cannot be modified directly) but 136 * is not one we've explicitly chosen to include. 137 */ 138 private static Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> { 139 if (!p.getDefinition().isProtected()) { 140 return false; 141 } else if (p.getParent().isNodeType(FROZEN_NODE)) { 142 // everything on a frozen node is protected 143 // but we wish to display it anyway and there's 144 // another mechanism in place to make clear that 145 // things cannot be edited. 146 return false; 147 } else { 148 return !protectedProperties.contains(p.getName()); 149 } 150 }); 151 152 /** 153 * Check whether a property is an internal property that should be suppressed 154 * from external output. 155 */ 156 public static Predicate<Property> isInternalProperty = isBinaryContentProperty 157 .or(isProtectedAndShouldBeHidden::test) 158 .or(uncheck(p -> privateProperties.contains(p.getName()))); 159 160 /** 161 * Check whether a type should be internal. 162 */ 163 public static Predicate<NodeType> isInternalType = uncheck(p -> protectedTypes.contains(p.getName())); 164 165 /** 166 * Check if a node is "internal" and should not be exposed e.g. via the REST 167 * API 168 */ 169 public static Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system")); 170 171 /** 172 * Check if a node is externally managed. 173 * 174 * Note: modeshape uses a source-workspace-identifier scheme 175 * to identify whether a node is externally-managed. 176 * Ordinary (non-external) nodes will have simple UUIDs 177 * as an identifier. These are never external nodes. 178 * 179 * External nodes will have a 7-character hex code 180 * identifying the "source", followed by another 181 * 7-character hex code identifying the "workspace", followed 182 * by a "/" and then the rest of the "identifier". 183 * 184 * Following that scheme, if a node's "source" key does not 185 * match the repository's configured store name, then it is an 186 * external node. 187 */ 188 public static Predicate<Node> isExternalNode = uncheck(n -> { 189 if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) { 190 return false; 191 } else if (n.getPrimaryNodeType().getName().equals(ROOT)) { 192 return false; 193 } else { 194 final NodeKey key = new NodeKey(n.getIdentifier()); 195 final String source = NodeKey.keyForSourceName( 196 ((JcrRepository)n.getSession().getRepository()).getConfiguration().getStoreName()); 197 return !key.getSourceKey().equals(source); 198 } 199 }); 200 201 /** 202 * Get the JCR property type ID for a given property name. If unsure, mark 203 * it as UNDEFINED. 204 * 205 * @param node the JCR node to add the property on 206 * @param propertyName the property name 207 * @return a PropertyType value 208 * @throws RepositoryException if repository exception occurred 209 */ 210 public static Optional<Integer> getPropertyType(final Node node, final String propertyName) 211 throws RepositoryException { 212 LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node); 213 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType); 214 } 215 216 /** 217 * Determine if a given JCR property name is single- or multi- valued. 218 * If unsure, choose the least restrictive option (multivalued = true) 219 * 220 * @param node the JCR node to check 221 * @param propertyName the property name (which may or may not already exist) 222 * @return true if the property is multivalued 223 * @throws RepositoryException if repository exception occurred 224 */ 225 public static boolean isMultivaluedProperty(final Node node, final String propertyName) 226 throws RepositoryException { 227 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true); 228 } 229 230 /** 231 * Get the property definition information (containing type and multi-value 232 * information) 233 * 234 * @param node the node to use for inferring the property definition 235 * @param propertyName the property name to retrieve a definition for 236 * @return a JCR PropertyDefinition, if available 237 * @throws javax.jcr.RepositoryException if repository exception occurred 238 */ 239 public static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName) 240 throws RepositoryException { 241 LOGGER.debug("Looking for property name: {}", propertyName); 242 final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName()); 243 244 final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions(); 245 final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst(); 246 return primaryCandidate.isPresent() ? primaryCandidate : 247 stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream) 248 .filter(sameName).findFirst(); 249 } 250 251 /** 252 * When we add certain URI properties, we also want to leave a reference node 253 * @param propertyName the property name 254 * @return property name as a reference 255 */ 256 public static String getReferencePropertyName(final String propertyName) { 257 return propertyName + REFERENCE_PROPERTY_SUFFIX; 258 } 259 260 /** 261 * Given an internal reference node property, get the original name 262 * @param refPropertyName the reference node property name 263 * @return original property name of the reference property 264 */ 265 public static String getReferencePropertyOriginalName(final String refPropertyName) { 266 final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX); 267 return i < 0 ? refPropertyName : refPropertyName.substring(0, i); 268 } 269 270 /** 271 * Check if a property definition is a reference property 272 * @param node the given node 273 * @param propertyName the property name 274 * @return whether a property definition is a reference property 275 * @throws RepositoryException if repository exception occurred 276 */ 277 public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException { 278 final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName); 279 280 return propertyDefinition.isPresent() && 281 (propertyDefinition.get().getRequiredType() == REFERENCE 282 || propertyDefinition.get().getRequiredType() == WEAKREFERENCE); 283 } 284 285 286 /** 287 * Get the closest ancestor that current exists 288 * 289 * @param session the given session 290 * @param path the given path 291 * @return the closest ancestor that current exists 292 * @throws RepositoryException if repository exception occurred 293 */ 294 public static Node getClosestExistingAncestor(final Session session, final String path) 295 throws RepositoryException { 296 297 String potentialPath = path.startsWith("/") ? path : "/" + path; 298 while (!potentialPath.isEmpty()) { 299 if (session.nodeExists(potentialPath)) { 300 return session.getNode(potentialPath); 301 } 302 potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/')); 303 } 304 return session.getRootNode(); 305 } 306 307}