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