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.net.URI; 021import java.util.Arrays; 022import java.util.Calendar; 023import java.util.Optional; 024import java.util.Set; 025import java.util.function.Function; 026import java.util.function.Predicate; 027 028import org.apache.jena.rdf.model.Resource; 029import org.fcrepo.kernel.api.FedoraTypes; 030import org.fcrepo.kernel.api.exception.AccessDeniedException; 031import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 032import org.fcrepo.kernel.api.models.FedoraResource; 033import org.fcrepo.kernel.modeshape.FedoraResourceImpl; 034import org.fcrepo.kernel.modeshape.services.functions.AnyTypesPredicate; 035import org.modeshape.jcr.JcrRepository; 036import org.modeshape.jcr.cache.NodeKey; 037import org.slf4j.Logger; 038 039import javax.jcr.NamespaceRegistry; 040import javax.jcr.Node; 041import javax.jcr.Property; 042import javax.jcr.RepositoryException; 043import javax.jcr.Session; 044import javax.jcr.nodetype.NodeType; 045import javax.jcr.nodetype.PropertyDefinition; 046 047import static java.util.Arrays.stream; 048import static java.util.Calendar.getInstance; 049import static java.util.Optional.empty; 050import static java.util.TimeZone.getTimeZone; 051import static javax.jcr.PropertyType.REFERENCE; 052import static javax.jcr.PropertyType.WEAKREFERENCE; 053import static com.google.common.collect.ImmutableSet.of; 054import static org.apache.jena.rdf.model.ResourceFactory.createResource; 055import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE; 056import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES; 057import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_PRIMARY_TYPE; 058import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_NODE; 059import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED; 060import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATEDBY; 061import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_FROZEN_NODE; 062import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED; 063import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIEDBY; 064import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT; 065import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isBinaryContentProperty; 066import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry; 067import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck; 068import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 069import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE; 070import static org.modeshape.jcr.api.JcrConstants.JCR_MIXIN_TYPES; 071import static org.slf4j.LoggerFactory.getLogger; 072 073/** 074 * Convenience class with static methods for manipulating Fedora types in the 075 * JCR. 076 * 077 * @author ajs6f 078 * @since Feb 14, 2013 079 */ 080public abstract class FedoraTypesUtils implements FedoraTypes { 081 082 private static final String REFERENCE_PROPERTY_SUFFIX = "_ref"; 083 084 private static final Logger LOGGER = getLogger(FedoraTypesUtils.class); 085 086 private static final Set<String> privateProperties = of( 087 "jcr:mime", 088 "jcr:mimeType", 089 "jcr:frozenUuid", 090 "jcr:uuid", 091 JCR_CONTENT, 092 JCR_PRIMARY_TYPE, 093 JCR_LASTMODIFIED, 094 JCR_LASTMODIFIEDBY, 095 JCR_CREATED, 096 JCR_CREATEDBY, 097 JCR_MIXIN_TYPES, 098 FROZEN_MIXIN_TYPES, 099 FROZEN_PRIMARY_TYPE, 100 MEMENTO_DATETIME); 101 102 private static final Set<String> validJcrProperties = of( 103 JCR_CREATED, 104 JCR_CREATEDBY, 105 JCR_LASTMODIFIED, 106 JCR_LASTMODIFIEDBY); 107 108 /** 109 * Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}. 110 */ 111 public static final Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER); 112 113 /** 114 * Predicate for determining whether this {@link Node} is a 115 * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}. 116 */ 117 public static final Predicate<Node> isNonRdfSourceDescription = 118 new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION); 119 120 121 /** 122 * Predicate for determining whether this {@link Node} is a Fedora 123 * binary. 124 */ 125 public static final Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY); 126 127 /** 128 * Predicate for determining whether this {@link FedoraResource} has a frozen node 129 */ 130 public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) || 131 f.getPath().contains(JCR_FROZEN_NODE); 132 133 /** 134 * Predicate for determining whether this {@link Node} is a Fedora Skolem node. 135 */ 136 public static final Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM); 137 138 /** 139 * Predicate for determining whether this {@link Node} is a Memento. 140 */ 141 public static final Predicate<Node> isMemento = new AnyTypesPredicate(MEMENTO); 142 143 144 /** 145 * Predicate for determining whether this {@link Node} is an Web ACL. 146 */ 147 public static final Predicate<Node> isAcl = new AnyTypesPredicate(FEDORA_WEBAC_ACL); 148 149 /** 150 * Check if a property is a reference property. 151 */ 152 public static final Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE || 153 p.getType() == WEAKREFERENCE) && 154 p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX)); 155 156 /** 157 * Check whether a type should be internal. 158 */ 159 public static final Predicate<String> hasInternalNamespace = type -> 160 type.startsWith("jcr:") || type.startsWith("mode:") || type.startsWith("nt:") || 161 type.startsWith("mix:"); 162 163 /** 164 * Predicate for determining whether a JCR property should be converted to the fedora namespace. 165 */ 166 private static final Predicate<String> isPublicJcrProperty = validJcrProperties::contains; 167 168 /** 169 * Check whether a property is protected (ie, cannot be modified directly) but 170 * is not one we've explicitly chosen to include. 171 */ 172 private static final Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> { 173 if (!p.getDefinition().isProtected()) { 174 return false; 175 } else if (p.getParent().isNodeType(FROZEN_NODE)) { 176 // everything on a frozen node is protected 177 // but we wish to display it anyway and there's 178 // another mechanism in place to make clear that 179 // things cannot be edited. 180 return false; 181 } else if (isPublicJcrProperty.test(p.getName())) { 182 return false; 183 } 184 return hasInternalNamespace.test(p.getName()); 185 }); 186 187 /** 188 * Check whether a property is an internal property that should be suppressed 189 * from external output. 190 */ 191 public static final Predicate<Property> isInternalProperty = isBinaryContentProperty 192 .or(isProtectedAndShouldBeHidden::test) 193 .or(uncheck(p -> privateProperties.contains(p.getName()))); 194 195 /** 196 * Check whether a type is an internal type that should be suppressed from external output. 197 */ 198 public static final Predicate<URI> isInternalType = t -> t.toString().equals(MEMENTO_TYPE); 199 200 /** 201 * A functional predicate to check whether a property is a JCR property that should be exposed. 202 * Historically we exposed JCR properties when they seemed to match a fedora property we wanted to track, 203 * but when control over the property became a requirement, we introduced the direct storage 204 * of fedora properties that when present should overrule the JCR property. 205 */ 206 public static class IsExposedJCRPropertyPredicate implements Predicate<Property> { 207 208 private final FedoraResource subject; 209 210 /** 211 * Constructs this functional predicate for testing properties on the given 212 * resource. 213 * @param resource the resource whose properties can be tested by this predicate 214 */ 215 public IsExposedJCRPropertyPredicate(final FedoraResource resource) { 216 subject = resource; 217 } 218 219 @Override 220 public boolean test(final Property prop) { 221 try { 222 return (prop.getName().equals(JCR_LASTMODIFIED) && !subject.hasProperty(FEDORA_LASTMODIFIED)) 223 || (prop.getName().equals(JCR_LASTMODIFIEDBY) && !subject.hasProperty(FEDORA_LASTMODIFIEDBY)) 224 || (prop.getName().equals(JCR_CREATED) && !subject.hasProperty(FEDORA_CREATED)) 225 || (prop.getName().equals(JCR_CREATEDBY) && !subject.hasProperty(FEDORA_CREATEDBY)); 226 } catch (final RepositoryException e) { 227 throw new RepositoryRuntimeException(e); 228 } 229 } 230 } 231 232 /** 233 * Check if a node is "internal" and should not be exposed e.g. via the REST 234 * API 235 */ 236 public static final Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system")); 237 238 /** 239 * Check if a node is externally managed. 240 * 241 * Note: modeshape uses a source-workspace-identifier scheme 242 * to identify whether a node is externally-managed. 243 * Ordinary (non-external) nodes will have simple UUIDs 244 * as an identifier. These are never external nodes. 245 * 246 * External nodes will have a 7-character hex code 247 * identifying the "source", followed by another 248 * 7-character hex code identifying the "workspace", followed 249 * by a "/" and then the rest of the "identifier". 250 * 251 * Following that scheme, if a node's "source" key does not 252 * match the repository's configured store name, then it is an 253 * external node. 254 */ 255 public static final Predicate<Node> isExternalNode = uncheck(n -> { 256 if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) { 257 return false; 258 } else if (n.getPrimaryNodeType().getName().equals(ROOT)) { 259 return false; 260 } else { 261 final NodeKey key = new NodeKey(n.getIdentifier()); 262 final String source = NodeKey.keyForSourceName( 263 ((JcrRepository)n.getSession().getRepository()).getConfiguration().getName()); 264 return !key.getSourceKey().equals(source); 265 } 266 }); 267 268 /** 269 * Get the JCR property type ID for a given property name. If unsure, mark 270 * it as UNDEFINED. 271 * 272 * @param node the JCR node to add the property on 273 * @param propertyName the property name 274 * @return a PropertyType value 275 * @throws RepositoryException if repository exception occurred 276 */ 277 public static Optional<Integer> getPropertyType(final Node node, final String propertyName) 278 throws RepositoryException { 279 LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node); 280 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType); 281 } 282 283 /** 284 * Determine if a given JCR property name is single- or multi- valued. 285 * If unsure, choose the least restrictive option (multivalued = true) 286 * 287 * @param node the JCR node to check 288 * @param propertyName the property name (which may or may not already exist) 289 * @return true if the property is multivalued 290 * @throws RepositoryException if repository exception occurred 291 */ 292 public static boolean isMultivaluedProperty(final Node node, final String propertyName) 293 throws RepositoryException { 294 return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true); 295 } 296 297 /** 298 * Get the property definition information (containing type and multi-value 299 * information) 300 * 301 * @param node the node to use for inferring the property definition 302 * @param propertyName the property name to retrieve a definition for 303 * @return a JCR PropertyDefinition, if available 304 * @throws javax.jcr.RepositoryException if repository exception occurred 305 */ 306 private static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName) 307 throws RepositoryException { 308 LOGGER.debug("Looking for property name: {}", propertyName); 309 final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName()); 310 311 final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions(); 312 final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst(); 313 return primaryCandidate.isPresent() ? primaryCandidate : 314 stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream) 315 .filter(sameName).findFirst(); 316 } 317 318 /** 319 * When we add certain URI properties, we also want to leave a reference node 320 * @param propertyName the property name 321 * @return property name as a reference 322 */ 323 public static String getReferencePropertyName(final String propertyName) { 324 return propertyName + REFERENCE_PROPERTY_SUFFIX; 325 } 326 327 /** 328 * Given an internal reference node property, get the original name 329 * @param refPropertyName the reference node property name 330 * @return original property name of the reference property 331 */ 332 public static String getReferencePropertyOriginalName(final String refPropertyName) { 333 final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX); 334 return i < 0 ? refPropertyName : refPropertyName.substring(0, i); 335 } 336 337 /** 338 * Check if a property definition is a reference property 339 * @param node the given node 340 * @param propertyName the property name 341 * @return whether a property definition is a reference property 342 * @throws RepositoryException if repository exception occurred 343 */ 344 public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException { 345 final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName); 346 347 return propertyDefinition.isPresent() && 348 (propertyDefinition.get().getRequiredType() == REFERENCE 349 || propertyDefinition.get().getRequiredType() == WEAKREFERENCE); 350 } 351 352 353 /** 354 * Get the closest ancestor that current exists 355 * 356 * @param session the given session 357 * @param path the given path 358 * @return the closest ancestor that current exists 359 * @throws RepositoryException if repository exception occurred 360 */ 361 public static Node getClosestExistingAncestor(final Session session, final String path) 362 throws RepositoryException { 363 364 String potentialPath = path.startsWith("/") ? path : "/" + path; 365 while (!potentialPath.isEmpty()) { 366 if (session.nodeExists(potentialPath)) { 367 return session.getNode(potentialPath); 368 } 369 potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/')); 370 } 371 return session.getRootNode(); 372 } 373 374 /** 375 * Retrieve the underlying JCR Node from the FedoraResource 376 * 377 * @param resource the Fedora resource 378 * @return the JCR Node 379 */ 380 public static Node getJcrNode(final FedoraResource resource) { 381 if (resource instanceof FedoraResourceImpl) { 382 return ((FedoraResourceImpl)resource).getNode(); 383 } 384 throw new IllegalArgumentException("FedoraResource is of the wrong type"); 385 } 386 387 /** 388 * Given a JCR Node, fetch the parent's ldp:insertedContentRelation value, if 389 * one exists. 390 * 391 * @param node the JCR Node 392 * @return the ldp:insertedContentRelation Resource, if one exists. 393 */ 394 public static Optional<Resource> ldpInsertedContentProperty(final Node node) { 395 return getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE) && 396 parent.isNodeType(LDP_INDIRECT_CONTAINER) && parent.hasProperty(LDP_INSERTED_CONTENT_RELATION))) 397 .map(UncheckedFunction.uncheck(parent -> 398 createResource(parent.getProperty(LDP_INSERTED_CONTENT_RELATION).getString()))); 399 } 400 401 /** 402 * Using a JCR session, return a function that maps an RDF Resource to a corresponding property name. 403 * 404 * @param session The JCR session 405 * @return a Function that maps a Resource to an Optional-wrapped String 406 */ 407 public static Function<Resource, Optional<String>> resourceToProperty(final Session session) { 408 return resource -> { 409 try { 410 final NamespaceRegistry registry = getNamespaceRegistry(session); 411 return Optional.of(registry.getPrefix(resource.getNameSpace()) + ":" + resource.getLocalName()); 412 } catch (final RepositoryException ex) { 413 LOGGER.debug("Could not resolve resource namespace ({}): {}", resource.toString(), ex.getMessage()); 414 } 415 return empty(); 416 }; 417 } 418 419 /** 420 * Update the fedora:lastModified date and fedora:lastModifiedBy of the parent's ldp:membershipResource if that 421 * node is a direct or indirect container, provided the LDP constraints are valid. 422 * 423 * @param node The JCR node 424 */ 425 public static void touchLdpMembershipResource(final Node node) { 426 touchLdpMembershipResource(node, null, null); 427 } 428 429 /** 430 * Update the fedora:lastModified date and fedora:lastModifiedBy of the parent's ldp:membershipResource if that 431 * node is a direct or indirect container, provided the LDP constraints are valid. 432 * 433 * @param node The JCR node 434 * @param date the date the modification was supposed to have occurred or null to indicate now 435 * @param user the user who performed the action or null to indicate the user associated with the current session 436 */ 437 public static void touchLdpMembershipResource(final Node node, final Calendar date, final String user) { 438 getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE))).ifPresent(parent -> { 439 try { 440 final Optional<String> hasInsertedContentProperty = ldpInsertedContentProperty(node) 441 .flatMap(resourceToProperty(node.getSession())).filter(uncheck(node::hasProperty)); 442 if (parent.isNodeType(LDP_DIRECT_CONTAINER) || 443 (parent.isNodeType(LDP_INDIRECT_CONTAINER) && hasInsertedContentProperty.isPresent())) { 444 touch(parent.getProperty(LDP_MEMBER_RESOURCE).getNode(), date, user); 445 } 446 } catch (final javax.jcr.AccessDeniedException ex) { 447 throw new AccessDeniedException(ex); 448 } catch (final RepositoryException ex) { 449 throw new RepositoryRuntimeException(ex); 450 } 451 }); 452 } 453 454 /** 455 * Updates the LAST_MODIFIED_DATE and LAST_MODIFIED_BY properties to now, and the current user 456 * respectively. 457 * 458 * @param node The JCR node 459 * 460 */ 461 public static void touch(final Node node) { 462 touch(node, null, null, null, null); 463 } 464 465 /** 466 * Updates the LAST_MODIFIED_DATE and LAST_MODIFIED_BY properties to the provided values. 467 * 468 * @param node The JCR node 469 * @param modified the modification date, or null if not explicitly set 470 * @param modifyingUser the userID who modified this resource or null if not explicitly set 471 * 472 */ 473 private static void touch(final Node node, final Calendar modified, final String modifyingUser) { 474 touch(node, null, null, modified, modifyingUser); 475 } 476 477 /** 478 * Updates the LAST_MODIFIED_DATE, LAST_MODIFIED_BY, CREATED_DATE and CREATED_BY properties to the provided values. 479 * 480 * @param node The JCR node 481 * @param created the date the resource was created, or null if not explicitly set 482 * @param creatingUser the userID of created this resource or null if not explicitly set 483 * @param modified the modification date, or null if not explicitly set 484 * @param modifyingUser the userID who modified this resource or null if not explicitly set 485 * 486 */ 487 public static void touch(final Node node, final Calendar created, final String creatingUser, 488 final Calendar modified, final String modifyingUser) { 489 try { 490 if (created != null) { 491 node.setProperty(FEDORA_CREATED, created); 492 } 493 494 if (creatingUser != null) { 495 node.setProperty(FEDORA_CREATEDBY, creatingUser); 496 } 497 498 if (modified != null) { 499 node.setProperty(FEDORA_LASTMODIFIED, modified); 500 } else { 501 node.setProperty(FEDORA_LASTMODIFIED, getInstance(getTimeZone("UTC"))); 502 } 503 504 if (modifyingUser != null) { 505 node.setProperty(FEDORA_LASTMODIFIEDBY, modifyingUser); 506 } else { 507 // revert to the modeshape-managed property 508 if (node.hasProperty(FEDORA_LASTMODIFIEDBY)) { 509 node.getProperty(FEDORA_LASTMODIFIEDBY).remove(); 510 } 511 } 512 } catch (final javax.jcr.AccessDeniedException ex) { 513 throw new AccessDeniedException(ex); 514 } catch (final RepositoryException ex) { 515 throw new RepositoryRuntimeException(ex); 516 } 517 } 518 519 /** 520 * Get the JCR Node that corresponds to the containing node in the repository. 521 * This may be the direct parent node, but it may also be a more distant ancestor. 522 * 523 * @param node the JCR node 524 * @return the containing node, if one is present 525 */ 526 public static Optional<Node> getContainingNode(final Node node) { 527 try { 528 if (node.getDepth() == 0) { 529 return empty(); 530 } 531 532 // check ancestors recursively only either of the following two cases applies: 533 // 1. the PARENT is a FEDORA_PAIRTREE 534 // 2. the PARENT is FEDORA_NON_RDF_SOURCE_DESCRIPTION 535 final Node parent = node.getParent(); 536 if (parent.isNodeType(FEDORA_PAIRTREE) || parent.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) { 537 return getContainingNode(parent); 538 } 539 return Optional.of(parent); 540 } catch (final RepositoryException ex) { 541 throw new RepositoryRuntimeException(ex); 542 } 543 } 544}