001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.kernel.modeshape.utils; 019 020import java.util.Arrays; 021import java.util.Optional; 022import java.util.Set; 023import java.util.function.Function; 024import java.util.function.Predicate; 025 026import org.apache.jena.rdf.model.Resource; 027import org.fcrepo.kernel.api.FedoraTypes; 028import org.fcrepo.kernel.api.exception.AccessDeniedException; 029import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 030import org.fcrepo.kernel.api.models.FedoraResource; 031import org.fcrepo.kernel.modeshape.FedoraResourceImpl; 032import org.fcrepo.kernel.modeshape.services.functions.AnyTypesPredicate; 033import org.modeshape.jcr.JcrRepository; 034import org.modeshape.jcr.cache.NodeKey; 035import org.slf4j.Logger; 036 037import javax.jcr.NamespaceRegistry; 038import javax.jcr.Node; 039import javax.jcr.Property; 040import javax.jcr.RepositoryException; 041import javax.jcr.Session; 042import javax.jcr.nodetype.NodeType; 043import javax.jcr.nodetype.PropertyDefinition; 044 045import static java.util.Arrays.stream; 046import static java.util.Calendar.getInstance; 047import static java.util.Optional.empty; 048import static java.util.TimeZone.getTimeZone; 049import static javax.jcr.PropertyType.REFERENCE; 050import static javax.jcr.PropertyType.WEAKREFERENCE; 051import static com.google.common.collect.ImmutableSet.of; 052import static org.apache.jena.rdf.model.ResourceFactory.createResource; 053import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_LASTMODIFIED; 054import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER; 055import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER; 056import static org.fcrepo.kernel.api.FedoraTypes.LDP_INSERTED_CONTENT_RELATION; 057import static org.fcrepo.kernel.api.FedoraTypes.LDP_MEMBER_RESOURCE; 058import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES; 059import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_PRIMARY_TYPE; 060import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_NODE; 061import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED; 062import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATEDBY; 063import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_FROZEN_NODE; 064import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED; 065import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIEDBY; 066import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT; 067import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isBinaryContentProperty; 068import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry; 069import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck; 070import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 071import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE; 072import static org.modeshape.jcr.api.JcrConstants.JCR_MIXIN_TYPES; 073import static org.slf4j.LoggerFactory.getLogger; 074 075/** 076 * Convenience class with static methods for manipulating Fedora types in the 077 * JCR. 078 * 079 * @author ajs6f 080 * @since Feb 14, 2013 081 */ 082public abstract class FedoraTypesUtils implements FedoraTypes { 083 084 public static final String REFERENCE_PROPERTY_SUFFIX = "_ref"; 085 086 private static final Logger LOGGER = getLogger(FedoraTypesUtils.class); 087 088 private static Set<String> privateProperties = of( 089 "jcr:mime", 090 "jcr:mimeType", 091 "jcr:frozenUuid", 092 "jcr:uuid", 093 JCR_CONTENT, 094 JCR_PRIMARY_TYPE, 095 JCR_LASTMODIFIED, 096 JCR_MIXIN_TYPES, 097 FROZEN_MIXIN_TYPES, 098 FROZEN_PRIMARY_TYPE); 099 100 private static Set<String> validJcrProperties = of( 101 JCR_CREATED, 102 JCR_CREATEDBY, 103 JCR_LASTMODIFIEDBY); 104 105 /** 106 * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}. 107 */ 108 public static Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER); 109 110 /** 111 * Predicate for determining whether this {@link Node} is a 112 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}. 113 */ 114 public static Predicate<Node> isNonRdfSourceDescription = new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION); 115 116 117 /** 118 * Predicate for determining whether this {@link Node} is a Fedora 119 * binary. 120 */ 121 public static Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY); 122 123 /** 124 * Predicate for determining whether this {@link FedoraResource} has a frozen node 125 */ 126 public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) || 127 f.getPath().contains(JCR_FROZEN_NODE); 128 129 /** 130 * Predicate for determining whether this {@link Node} is a Fedora Skolem node. 131 */ 132 public static Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM); 133 134 /** 135 * Check if a property is a reference property. 136 */ 137 public static Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE || 138 p.getType() == WEAKREFERENCE) && 139 p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX)); 140 141 /** 142 * Check whether a type should be internal. 143 */ 144 public static Predicate<String> hasInternalNamespace = type -> 145 type.startsWith("jcr:") || type.startsWith("mode:") || type.startsWith("nt:") || 146 type.startsWith("mix:"); 147 148 /** 149 * Predicate for determining whether a JCR property should be converted to the fedora namespace. 150 */ 151 public static Predicate<String> isPublicJcrProperty = validJcrProperties::contains; 152 153 /** 154 * Check whether a property is protected (ie, cannot be modified directly) but 155 * is not one we've explicitly chosen to include. 156 */ 157 private static Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> { 158 if (!p.getDefinition().isProtected()) { 159 return false; 160 } else if (p.getParent().isNodeType(FROZEN_NODE)) { 161 // everything on a frozen node is protected 162 // but we wish to display it anyway and there's 163 // another mechanism in place to make clear that 164 // things cannot be edited. 165 return false; 166 } else if (isPublicJcrProperty.test(p.getName())) { 167 return false; 168 } 169 return hasInternalNamespace.test(p.getName()); 170 }); 171 172 /** 173 * Check whether a property is an internal property that should be suppressed 174 * from external output. 175 */ 176 public static Predicate<Property> isInternalProperty = isBinaryContentProperty 177 .or(isProtectedAndShouldBeHidden::test) 178 .or(uncheck(p -> privateProperties.contains(p.getName()))); 179 180 /** 181 * Check if a node is "internal" and should not be exposed e.g. via the REST 182 * API 183 */ 184 public static Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system")); 185 186 /** 187 * Check if a node is externally managed. 188 * 189 * Note: modeshape uses a source-workspace-identifier scheme 190 * to identify whether a node is externally-managed. 191 * Ordinary (non-external) nodes will have simple UUIDs 192 * as an identifier. These are never external nodes. 193 * 194 * External nodes will have a 7-character hex code 195 * identifying the "source", followed by another 196 * 7-character hex code identifying the "workspace", followed 197 * by a "/" and then the rest of the "identifier". 198 * 199 * Following that scheme, if a node's "source" key does not 200 * match the repository's configured store name, then it is an 201 * external node. 202 */ 203 public static Predicate<Node> isExternalNode = uncheck(n -> { 204 if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) { 205 return false; 206 } else if (n.getPrimaryNodeType().getName().equals(ROOT)) { 207 return false; 208 } else { 209 final NodeKey key = new NodeKey(n.getIdentifier()); 210 final String source = NodeKey.keyForSourceName( 211 ((JcrRepository)n.getSession().getRepository()).getConfiguration().getName()); 212 return !key.getSourceKey().equals(source); 213 } 214 }); 215 216 /** 217 * Get the JCR property type ID for a given property name. If unsure, mark 218 * it as UNDEFINED. 219 * 220 * @param node the JCR node to add the property on 221 * @param propertyName the property name 222 * @return a PropertyType value 223 * @throws RepositoryException if repository exception occurred 224 */ 225 public static Optional<Integer> getPropertyType(final Node node, final String propertyName) 226 throws RepositoryException { 227 LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node); 228 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType); 229 } 230 231 /** 232 * Determine if a given JCR property name is single- or multi- valued. 233 * If unsure, choose the least restrictive option (multivalued = true) 234 * 235 * @param node the JCR node to check 236 * @param propertyName the property name (which may or may not already exist) 237 * @return true if the property is multivalued 238 * @throws RepositoryException if repository exception occurred 239 */ 240 public static boolean isMultivaluedProperty(final Node node, final String propertyName) 241 throws RepositoryException { 242 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true); 243 } 244 245 /** 246 * Get the property definition information (containing type and multi-value 247 * information) 248 * 249 * @param node the node to use for inferring the property definition 250 * @param propertyName the property name to retrieve a definition for 251 * @return a JCR PropertyDefinition, if available 252 * @throws javax.jcr.RepositoryException if repository exception occurred 253 */ 254 public static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName) 255 throws RepositoryException { 256 LOGGER.debug("Looking for property name: {}", propertyName); 257 final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName()); 258 259 final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions(); 260 final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst(); 261 return primaryCandidate.isPresent() ? primaryCandidate : 262 stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream) 263 .filter(sameName).findFirst(); 264 } 265 266 /** 267 * When we add certain URI properties, we also want to leave a reference node 268 * @param propertyName the property name 269 * @return property name as a reference 270 */ 271 public static String getReferencePropertyName(final String propertyName) { 272 return propertyName + REFERENCE_PROPERTY_SUFFIX; 273 } 274 275 /** 276 * Given an internal reference node property, get the original name 277 * @param refPropertyName the reference node property name 278 * @return original property name of the reference property 279 */ 280 public static String getReferencePropertyOriginalName(final String refPropertyName) { 281 final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX); 282 return i < 0 ? refPropertyName : refPropertyName.substring(0, i); 283 } 284 285 /** 286 * Check if a property definition is a reference property 287 * @param node the given node 288 * @param propertyName the property name 289 * @return whether a property definition is a reference property 290 * @throws RepositoryException if repository exception occurred 291 */ 292 public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException { 293 final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName); 294 295 return propertyDefinition.isPresent() && 296 (propertyDefinition.get().getRequiredType() == REFERENCE 297 || propertyDefinition.get().getRequiredType() == WEAKREFERENCE); 298 } 299 300 301 /** 302 * Get the closest ancestor that current exists 303 * 304 * @param session the given session 305 * @param path the given path 306 * @return the closest ancestor that current exists 307 * @throws RepositoryException if repository exception occurred 308 */ 309 public static Node getClosestExistingAncestor(final Session session, final String path) 310 throws RepositoryException { 311 312 String potentialPath = path.startsWith("/") ? path : "/" + path; 313 while (!potentialPath.isEmpty()) { 314 if (session.nodeExists(potentialPath)) { 315 return session.getNode(potentialPath); 316 } 317 potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/')); 318 } 319 return session.getRootNode(); 320 } 321 322 /** 323 * Retrieve the underlying JCR Node from the FedoraResource 324 * 325 * @param resource the Fedora resource 326 * @return the JCR Node 327 */ 328 public static Node getJcrNode(final FedoraResource resource) { 329 if (resource instanceof FedoraResourceImpl) { 330 return ((FedoraResourceImpl)resource).getNode(); 331 } 332 throw new IllegalArgumentException("FedoraResource is of the wrong type"); 333 } 334 335 /** 336 * Given a JCR Node, fetch the parent's ldp:insertedContentRelation value, if 337 * one exists. 338 * 339 * @param node the JCR Node 340 * @return the ldp:insertedContentRelation Resource, if one exists. 341 */ 342 public static Optional<Resource> ldpInsertedContentProperty(final Node node) { 343 return getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE) && 344 parent.isNodeType(LDP_INDIRECT_CONTAINER) && parent.hasProperty(LDP_INSERTED_CONTENT_RELATION))) 345 .map(UncheckedFunction.uncheck(parent -> 346 createResource(parent.getProperty(LDP_INSERTED_CONTENT_RELATION).getString()))); 347 } 348 349 /** 350 * Using a JCR session, return a function that maps an RDF Resource to a corresponding property name. 351 * 352 * @param session The JCR session 353 * @return a Function that maps a Resource to an Optional-wrapped String 354 */ 355 public static Function<Resource, Optional<String>> resourceToProperty(final Session session) { 356 return resource -> { 357 try { 358 final NamespaceRegistry registry = getNamespaceRegistry(session); 359 return Optional.of(registry.getPrefix(resource.getNameSpace()) + ":" + resource.getLocalName()); 360 } catch (final RepositoryException ex) { 361 LOGGER.debug("Could not resolve resource namespace ({}): {}", resource.toString(), ex.getMessage()); 362 } 363 return empty(); 364 }; 365 } 366 367 368 /** 369 * Update the fedora:lastModified date of the parent's ldp:membershipResource if that node is a direct 370 * or indirect container, provided the LDP constraints are valid. 371 * 372 * @param node The JCR node 373 */ 374 public static void touchLdpMembershipResource(final Node node) { 375 getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE))).ifPresent(parent -> { 376 try { 377 final Optional<String> hasInsertedContentProperty = ldpInsertedContentProperty(node) 378 .flatMap(resourceToProperty(node.getSession())).filter(uncheck(node::hasProperty)); 379 if (parent.isNodeType(LDP_DIRECT_CONTAINER) || 380 (parent.isNodeType(LDP_INDIRECT_CONTAINER) && hasInsertedContentProperty.isPresent())) { 381 touch(parent.getProperty(LDP_MEMBER_RESOURCE).getNode()); 382 } 383 } catch (final javax.jcr.AccessDeniedException ex) { 384 throw new AccessDeniedException(ex); 385 } catch (final RepositoryException ex) { 386 throw new RepositoryRuntimeException(ex); 387 } 388 }); 389 } 390 391 /** 392 * Update the fedora:lastModified date of the node. 393 * 394 * @param node The JCR node 395 */ 396 public static void touch(final Node node) { 397 try { 398 node.setProperty(FEDORA_LASTMODIFIED, getInstance(getTimeZone("UTC"))); 399 } catch (final javax.jcr.AccessDeniedException ex) { 400 throw new AccessDeniedException(ex); 401 } catch (final RepositoryException ex) { 402 throw new RepositoryRuntimeException(ex); 403 } 404 } 405 406 /** 407 * Get the JCR Node that corresponds to the containing node in the repository. 408 * This may be the direct parent node, but it may also be a more distant ancestor. 409 * 410 * @param node the JCR node 411 * @return the containing node, if one is present 412 */ 413 public static Optional<Node> getContainingNode(final Node node) { 414 try { 415 if (node.getDepth() == 0) { 416 return empty(); 417 } 418 419 final Node parent = node.getParent(); 420 if (parent.isNodeType(FEDORA_PAIRTREE) || parent.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) { 421 return getContainingNode(parent); 422 } 423 return Optional.of(parent); 424 } catch (final RepositoryException ex) { 425 throw new RepositoryRuntimeException(ex); 426 } 427 } 428}