/*
* Licensed to DuraSpace under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* DuraSpace licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.kernel.modeshape.utils;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.jena.rdf.model.Resource;
import org.fcrepo.kernel.api.FedoraTypes;
import org.fcrepo.kernel.api.exception.AccessDeniedException;
import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
import org.fcrepo.kernel.api.models.FedoraResource;
import org.fcrepo.kernel.modeshape.FedoraResourceImpl;
import org.fcrepo.kernel.modeshape.services.functions.AnyTypesPredicate;
import org.modeshape.jcr.JcrRepository;
import org.modeshape.jcr.cache.NodeKey;
import org.slf4j.Logger;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import static java.util.Arrays.stream;
import static java.util.Calendar.getInstance;
import static java.util.Optional.empty;
import static java.util.TimeZone.getTimeZone;
import static javax.jcr.PropertyType.REFERENCE;
import static javax.jcr.PropertyType.WEAKREFERENCE;
import static com.google.common.collect.ImmutableSet.of;
import static org.apache.jena.rdf.model.ResourceFactory.createResource;
import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_LASTMODIFIED;
import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER;
import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER;
import static org.fcrepo.kernel.api.FedoraTypes.LDP_INSERTED_CONTENT_RELATION;
import static org.fcrepo.kernel.api.FedoraTypes.LDP_MEMBER_RESOURCE;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_PRIMARY_TYPE;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_NODE;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATEDBY;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_FROZEN_NODE;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIEDBY;
import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT;
import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isBinaryContentProperty;
import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry;
import static org.fcrepo.kernel.modeshape.utils.UncheckedPredicate.uncheck;
import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE;
import static org.modeshape.jcr.api.JcrConstants.JCR_MIXIN_TYPES;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Convenience class with static methods for manipulating Fedora types in the
* JCR.
*
* @author ajs6f
* @since Feb 14, 2013
*/
public abstract class FedoraTypesUtils implements FedoraTypes {
public static final String REFERENCE_PROPERTY_SUFFIX = "_ref";
private static final Logger LOGGER = getLogger(FedoraTypesUtils.class);
private static Set<String> privateProperties = of(
"jcr:mime",
"jcr:mimeType",
"jcr:frozenUuid",
"jcr:uuid",
JCR_CONTENT,
JCR_PRIMARY_TYPE,
JCR_LASTMODIFIED,
JCR_MIXIN_TYPES,
FROZEN_MIXIN_TYPES,
FROZEN_PRIMARY_TYPE);
private static Set<String> validJcrProperties = of(
JCR_CREATED,
JCR_CREATEDBY,
JCR_LASTMODIFIEDBY);
/**
* Predicate for determining whether this {@link Node} is a {@link org.fcrepo.kernel.api.models.Container}.
*/
public static Predicate<Node> isContainer = new AnyTypesPredicate(FEDORA_CONTAINER);
/**
* Predicate for determining whether this {@link Node} is a
* {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription}.
*/
public static Predicate<Node> isNonRdfSourceDescription = new AnyTypesPredicate(FEDORA_NON_RDF_SOURCE_DESCRIPTION);
/**
* Predicate for determining whether this {@link Node} is a Fedora
* binary.
*/
public static Predicate<Node> isFedoraBinary = new AnyTypesPredicate(FEDORA_BINARY);
/**
* Predicate for determining whether this {@link FedoraResource} has a frozen node
*/
public static Predicate<FedoraResource> isFrozenNode = f -> f.hasType(FROZEN_NODE) ||
f.getPath().contains(JCR_FROZEN_NODE);
/**
* Predicate for determining whether this {@link Node} is a Fedora Skolem node.
*/
public static Predicate<Node> isSkolemNode = new AnyTypesPredicate(FEDORA_SKOLEM);
/**
* Check if a property is a reference property.
*/
public static Predicate<Property> isInternalReferenceProperty = uncheck(p -> (p.getType() == REFERENCE ||
p.getType() == WEAKREFERENCE) &&
p.getName().endsWith(REFERENCE_PROPERTY_SUFFIX));
/**
* Check whether a type should be internal.
*/
public static Predicate<String> hasInternalNamespace = type ->
type.startsWith("jcr:") || type.startsWith("mode:") || type.startsWith("nt:") ||
type.startsWith("mix:");
/**
* Predicate for determining whether a JCR property should be converted to the fedora namespace.
*/
public static Predicate<String> isPublicJcrProperty = validJcrProperties::contains;
/**
* Check whether a property is protected (ie, cannot be modified directly) but
* is not one we've explicitly chosen to include.
*/
private static Predicate<Property> isProtectedAndShouldBeHidden = uncheck(p -> {
if (!p.getDefinition().isProtected()) {
return false;
} else if (p.getParent().isNodeType(FROZEN_NODE)) {
// everything on a frozen node is protected
// but we wish to display it anyway and there's
// another mechanism in place to make clear that
// things cannot be edited.
return false;
} else if (isPublicJcrProperty.test(p.getName())) {
return false;
}
return hasInternalNamespace.test(p.getName());
});
/**
* Check whether a property is an internal property that should be suppressed
* from external output.
*/
public static Predicate<Property> isInternalProperty = isBinaryContentProperty
.or(isProtectedAndShouldBeHidden::test)
.or(uncheck(p -> privateProperties.contains(p.getName())));
/**
* Check if a node is "internal" and should not be exposed e.g. via the REST
* API
*/
public static Predicate<Node> isInternalNode = uncheck(n -> n.isNodeType("mode:system"));
/**
* Check if a node is externally managed.
*
* Note: modeshape uses a source-workspace-identifier scheme
* to identify whether a node is externally-managed.
* Ordinary (non-external) nodes will have simple UUIDs
* as an identifier. These are never external nodes.
*
* External nodes will have a 7-character hex code
* identifying the "source", followed by another
* 7-character hex code identifying the "workspace", followed
* by a "/" and then the rest of the "identifier".
*
* Following that scheme, if a node's "source" key does not
* match the repository's configured store name, then it is an
* external node.
*/
public static Predicate<Node> isExternalNode = uncheck(n -> {
if (NodeKey.isValidRandomIdentifier(n.getIdentifier())) {
return false;
} else if (n.getPrimaryNodeType().getName().equals(ROOT)) {
return false;
} else {
final NodeKey key = new NodeKey(n.getIdentifier());
final String source = NodeKey.keyForSourceName(
((JcrRepository)n.getSession().getRepository()).getConfiguration().getName());
return !key.getSourceKey().equals(source);
}
});
/**
* Get the JCR property type ID for a given property name. If unsure, mark
* it as UNDEFINED.
*
* @param node the JCR node to add the property on
* @param propertyName the property name
* @return a PropertyType value
* @throws RepositoryException if repository exception occurred
*/
public static Optional<Integer> getPropertyType(final Node node, final String propertyName)
throws RepositoryException {
LOGGER.debug("Getting type of property: {} from node: {}", propertyName, node);
return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::getRequiredType);
}
/**
* Determine if a given JCR property name is single- or multi- valued.
* If unsure, choose the least restrictive option (multivalued = true)
*
* @param node the JCR node to check
* @param propertyName the property name (which may or may not already exist)
* @return true if the property is multivalued
* @throws RepositoryException if repository exception occurred
*/
public static boolean isMultivaluedProperty(final Node node, final String propertyName)
throws RepositoryException {
return getDefinitionForPropertyName(node, propertyName).map(PropertyDefinition::isMultiple).orElse(true);
}
/**
* Get the property definition information (containing type and multi-value
* information)
*
* @param node the node to use for inferring the property definition
* @param propertyName the property name to retrieve a definition for
* @return a JCR PropertyDefinition, if available
* @throws javax.jcr.RepositoryException if repository exception occurred
*/
public static Optional<PropertyDefinition> getDefinitionForPropertyName(final Node node, final String propertyName)
throws RepositoryException {
LOGGER.debug("Looking for property name: {}", propertyName);
final Predicate<PropertyDefinition> sameName = p -> propertyName.equals(p.getName());
final PropertyDefinition[] propDefs = node.getPrimaryNodeType().getPropertyDefinitions();
final Optional<PropertyDefinition> primaryCandidate = stream(propDefs).filter(sameName).findFirst();
return primaryCandidate.isPresent() ? primaryCandidate :
stream(node.getMixinNodeTypes()).map(NodeType::getPropertyDefinitions).flatMap(Arrays::stream)
.filter(sameName).findFirst();
}
/**
* When we add certain URI properties, we also want to leave a reference node
* @param propertyName the property name
* @return property name as a reference
*/
public static String getReferencePropertyName(final String propertyName) {
return propertyName + REFERENCE_PROPERTY_SUFFIX;
}
/**
* Given an internal reference node property, get the original name
* @param refPropertyName the reference node property name
* @return original property name of the reference property
*/
public static String getReferencePropertyOriginalName(final String refPropertyName) {
final int i = refPropertyName.lastIndexOf(REFERENCE_PROPERTY_SUFFIX);
return i < 0 ? refPropertyName : refPropertyName.substring(0, i);
}
/**
* Check if a property definition is a reference property
* @param node the given node
* @param propertyName the property name
* @return whether a property definition is a reference property
* @throws RepositoryException if repository exception occurred
*/
public static boolean isReferenceProperty(final Node node, final String propertyName) throws RepositoryException {
final Optional<PropertyDefinition> propertyDefinition = getDefinitionForPropertyName(node, propertyName);
return propertyDefinition.isPresent() &&
(propertyDefinition.get().getRequiredType() == REFERENCE
|| propertyDefinition.get().getRequiredType() == WEAKREFERENCE);
}
/**
* Get the closest ancestor that current exists
*
* @param session the given session
* @param path the given path
* @return the closest ancestor that current exists
* @throws RepositoryException if repository exception occurred
*/
public static Node getClosestExistingAncestor(final Session session, final String path)
throws RepositoryException {
String potentialPath = path.startsWith("/") ? path : "/" + path;
while (!potentialPath.isEmpty()) {
if (session.nodeExists(potentialPath)) {
return session.getNode(potentialPath);
}
potentialPath = potentialPath.substring(0, potentialPath.lastIndexOf('/'));
}
return session.getRootNode();
}
/**
* Retrieve the underlying JCR Node from the FedoraResource
*
* @param resource the Fedora resource
* @return the JCR Node
*/
public static Node getJcrNode(final FedoraResource resource) {
if (resource instanceof FedoraResourceImpl) {
return ((FedoraResourceImpl)resource).getNode();
}
throw new IllegalArgumentException("FedoraResource is of the wrong type");
}
/**
* Given a JCR Node, fetch the parent's ldp:insertedContentRelation value, if
* one exists.
*
* @param node the JCR Node
* @return the ldp:insertedContentRelation Resource, if one exists.
*/
public static Optional<Resource> ldpInsertedContentProperty(final Node node) {
return getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE) &&
parent.isNodeType(LDP_INDIRECT_CONTAINER) && parent.hasProperty(LDP_INSERTED_CONTENT_RELATION)))
.map(UncheckedFunction.uncheck(parent ->
createResource(parent.getProperty(LDP_INSERTED_CONTENT_RELATION).getString())));
}
/**
* Using a JCR session, return a function that maps an RDF Resource to a corresponding property name.
*
* @param session The JCR session
* @return a Function that maps a Resource to an Optional-wrapped String
*/
public static Function<Resource, Optional<String>> resourceToProperty(final Session session) {
return resource -> {
try {
final NamespaceRegistry registry = getNamespaceRegistry(session);
return Optional.of(registry.getPrefix(resource.getNameSpace()) + ":" + resource.getLocalName());
} catch (final RepositoryException ex) {
LOGGER.debug("Could not resolve resource namespace ({}): {}", resource.toString(), ex.getMessage());
}
return empty();
};
}
/**
* Update the fedora:lastModified date of the parent's ldp:membershipResource if that node is a direct
* or indirect container, provided the LDP constraints are valid.
*
* @param node The JCR node
*/
public static void touchLdpMembershipResource(final Node node) {
getContainingNode(node).filter(uncheck(parent -> parent.hasProperty(LDP_MEMBER_RESOURCE))).ifPresent(parent -> {
try {
final Optional<String> hasInsertedContentProperty = ldpInsertedContentProperty(node)
.flatMap(resourceToProperty(node.getSession())).filter(uncheck(node::hasProperty));
if (parent.isNodeType(LDP_DIRECT_CONTAINER) ||
(parent.isNodeType(LDP_INDIRECT_CONTAINER) && hasInsertedContentProperty.isPresent())) {
touch(parent.getProperty(LDP_MEMBER_RESOURCE).getNode());
}
} catch (final javax.jcr.AccessDeniedException ex) {
throw new AccessDeniedException(ex);
} catch (final RepositoryException ex) {
throw new RepositoryRuntimeException(ex);
}
});
}
/**
* Update the fedora:lastModified date of the node.
*
* @param node The JCR node
*/
public static void touch(final Node node) {
try {
node.setProperty(FEDORA_LASTMODIFIED, getInstance(getTimeZone("UTC")));
} catch (final javax.jcr.AccessDeniedException ex) {
throw new AccessDeniedException(ex);
} catch (final RepositoryException ex) {
throw new RepositoryRuntimeException(ex);
}
}
/**
* Get the JCR Node that corresponds to the containing node in the repository.
* This may be the direct parent node, but it may also be a more distant ancestor.
*
* @param node the JCR node
* @return the containing node, if one is present
*/
public static Optional<Node> getContainingNode(final Node node) {
try {
if (node.getDepth() == 0) {
return empty();
}
final Node parent = node.getParent();
if (parent.isNodeType(FEDORA_PAIRTREE) || parent.isNodeType(FEDORA_NON_RDF_SOURCE_DESCRIPTION)) {
return getContainingNode(parent);
}
return Optional.of(parent);
} catch (final RepositoryException ex) {
throw new RepositoryRuntimeException(ex);
}
}
}