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