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}