/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.jackrabbit.core;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.NamespaceException;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.ItemDefinition;
import javax.jcr.version.VersionException;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.session.SessionOperation;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.QItemDefinition;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class for validating an item against constraints
* specified by its definition.
*/
public class ItemValidator {
/**
* check access permissions
*/
public static final int CHECK_ACCESS = 1;
/**
* option to check lock status
*/
public static final int CHECK_LOCK = 2;
/**
* option to check checked-out status
*/
public static final int CHECK_CHECKED_OUT = 4;
/**
* check for referential integrity upon removal
*/
public static final int CHECK_REFERENCES = 8;
/**
* option to check if the item is protected by it's nt definition
*/
public static final int CHECK_CONSTRAINTS = 16;
/**
* option to check for pending changes on the session
*/
public static final int CHECK_PENDING_CHANGES = 32;
/**
* option to check for pending changes on the specified node
*/
public static final int CHECK_PENDING_CHANGES_ON_NODE = 64;
/**
* option to check for effective holds
*/
public static final int CHECK_HOLD = 128;
/**
* option to check for effective retention policies
*/
public static final int CHECK_RETENTION = 256;
/**
* Logger instance for this class
*/
private static Logger log = LoggerFactory.getLogger(ItemValidator.class);
/**
* Component context of the associated session.
*/
protected final SessionContext context;
/**
* A bit mask of the checks that are currently enabled. All access to
* this mask must be synchronized to ensure that only the thread that
* uses the {@link #performRelaxed(SessionOperation, int)} method will
* experience the effect of the relaxed set of checks.
*/
private int enabledChecks = ~0;
/**
* Creates a new <code>ItemValidator</code> instance.
*
* @param context component context of this session
*/
public ItemValidator(SessionContext context) {
this.context = context;
}
/**
* Performs the given session operation with the specified checks disabled.
*
* @param operation the session operation to be performed
* @param checksToDisable bit mask of checks to be disabled
* @return return value of the session operation
* @throws RepositoryException if the operation could not be performed
*/
public synchronized <T> T performRelaxed(
SessionOperation<T> operation, int checksToDisable)
throws RepositoryException {
int previousChecks = enabledChecks;
try {
enabledChecks &= ~checksToDisable;
log.debug("Performing {} with checks [{}] disabled",
operation, Integer.toBinaryString(~enabledChecks));
return operation.perform(context);
} finally {
enabledChecks = previousChecks;
}
}
/**
* Checks whether the given node state satisfies the constraints specified
* by its primary and mixin node types. The following validations/checks are
* performed:
* <ul>
* <li>check if its node type satisfies the 'required node types' constraint
* specified in its definition</li>
* <li>check if all 'mandatory' child items exist</li>
* <li>for every property: check if the property value satisfies the
* value constraints specified in the property's definition</li>
* </ul>
*
* @param nodeState state of node to be validated
* @throws ConstraintViolationException if any of the validations fail
* @throws RepositoryException if another error occurs
*/
public void validate(NodeState nodeState)
throws ConstraintViolationException, RepositoryException {
// effective primary node type
NodeTypeRegistry registry = context.getNodeTypeRegistry();
EffectiveNodeType entPrimary =
registry.getEffectiveNodeType(nodeState.getNodeTypeName());
// effective node type (primary type incl. mixins)
EffectiveNodeType entPrimaryAndMixins = getEffectiveNodeType(nodeState);
QNodeDefinition def =
context.getItemManager().getDefinition(nodeState).unwrap();
// check if primary type satisfies the 'required node types' constraint
for (Name requiredPrimaryType : def.getRequiredPrimaryTypes()) {
if (!entPrimary.includesNodeType(requiredPrimaryType)) {
String msg = safeGetJCRPath(nodeState.getNodeId())
+ ": missing required primary type "
+ requiredPrimaryType;
log.debug(msg);
throw new ConstraintViolationException(msg);
}
}
// mandatory properties
for (QPropertyDefinition pd : entPrimaryAndMixins.getMandatoryPropDefs()) {
if (!nodeState.hasPropertyName(pd.getName())) {
String msg = safeGetJCRPath(nodeState.getNodeId())
+ ": mandatory property " + pd.getName()
+ " does not exist";
log.debug(msg);
throw new ConstraintViolationException(msg);
}
}
// mandatory child nodes
for (QItemDefinition cnd : entPrimaryAndMixins.getMandatoryNodeDefs()) {
if (!nodeState.hasChildNodeEntry(cnd.getName())) {
String msg = safeGetJCRPath(nodeState.getNodeId())
+ ": mandatory child node " + cnd.getName()
+ " does not exist";
log.debug(msg);
throw new ConstraintViolationException(msg);
}
}
}
/**
* Checks whether the given property state satisfies the constraints
* specified by its definition. The following validations/checks are
* performed:
* <ul>
* <li>check if the type of the property values does comply with the
* requiredType specified in the property's definition</li>
* <li>check if the property values satisfy the value constraints
* specified in the property's definition</li>
* </ul>
*
* @param propState state of property to be validated
* @throws ConstraintViolationException if any of the validations fail
* @throws RepositoryException if another error occurs
*/
public void validate(PropertyState propState)
throws ConstraintViolationException, RepositoryException {
QPropertyDefinition def =
context.getItemManager().getDefinition(propState).unwrap();
InternalValue[] values = propState.getValues();
int type = PropertyType.UNDEFINED;
for (InternalValue value : values) {
if (type == PropertyType.UNDEFINED) {
type = value.getType();
} else if (type != value.getType()) {
throw new ConstraintViolationException(safeGetJCRPath(propState.getPropertyId())
+ ": inconsistent value types");
}
if (def.getRequiredType() != PropertyType.UNDEFINED
&& def.getRequiredType() != type) {
throw new ConstraintViolationException(safeGetJCRPath(propState.getPropertyId())
+ ": requiredType constraint is not satisfied");
}
}
EffectiveNodeType.checkSetPropertyValueConstraints(def, values);
}
public synchronized void checkModify(
ItemImpl item, int options, int permissions)
throws RepositoryException {
checkCondition(item, options & enabledChecks, permissions, false);
}
public synchronized void checkRemove(
ItemImpl item, int options, int permissions)
throws RepositoryException {
checkCondition(item, options & enabledChecks, permissions, true);
}
private void checkCondition(ItemImpl item, int options, int permissions, boolean isRemoval) throws RepositoryException {
if ((options & CHECK_PENDING_CHANGES) == CHECK_PENDING_CHANGES) {
if (item.getSession().hasPendingChanges()) {
String msg = "Unable to perform operation. Session has pending changes.";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
}
if ((options & CHECK_PENDING_CHANGES_ON_NODE) == CHECK_PENDING_CHANGES_ON_NODE) {
if (item.isNode() && ((NodeImpl) item).hasPendingChanges()) {
String msg = "Unable to perform operation. Session has pending changes.";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
}
if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
if (isProtected(item)) {
String msg = "Unable to perform operation. Node is protected.";
log.debug(msg);
throw new ConstraintViolationException(msg);
}
}
if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) {
NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent();
if (!node.isCheckedOut()) {
String msg = "Unable to perform operation. Node is checked-in.";
log.debug(msg);
throw new VersionException(msg);
}
}
if ((options & CHECK_LOCK) == CHECK_LOCK) {
checkLock(item);
}
if (permissions > Permission.NONE) {
Path path = item.getPrimaryPath();
context.getAccessManager().checkPermission(path, permissions);
}
if ((options & CHECK_HOLD) == CHECK_HOLD) {
if (hasHold(item, isRemoval)) {
throw new RepositoryException("Unable to perform operation. Node is affected by a hold.");
}
}
if ((options & CHECK_RETENTION) == CHECK_RETENTION) {
if (hasRetention(item, isRemoval)) {
throw new RepositoryException("Unable to perform operation. Node is affected by a retention.");
}
}
}
public synchronized boolean canModify(
ItemImpl item, int options, int permissions)
throws RepositoryException {
return hasCondition(item, options & enabledChecks, permissions, false);
}
private boolean hasCondition(ItemImpl item, int options, int permissions, boolean isRemoval) throws RepositoryException {
if ((options & CHECK_PENDING_CHANGES) == CHECK_PENDING_CHANGES) {
if (item.getSession().hasPendingChanges()) {
return false;
}
}
if ((options & CHECK_PENDING_CHANGES_ON_NODE) == CHECK_PENDING_CHANGES_ON_NODE) {
if (item.isNode() && ((NodeImpl) item).hasPendingChanges()) {
return false;
}
}
if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
if (isProtected(item)) {
return false;
}
}
if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) {
NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent();
if (!node.isCheckedOut()) {
return false;
}
}
if ((options & CHECK_LOCK) == CHECK_LOCK) {
try {
checkLock(item);
} catch (LockException e) {
return false;
}
}
if (permissions > Permission.NONE) {
Path path = item.getPrimaryPath();
if (!context.getAccessManager().isGranted(path, permissions)) {
return false;
}
}
if ((options & CHECK_HOLD) == CHECK_HOLD) {
if (hasHold(item, isRemoval)) {
return false;
}
}
if ((options & CHECK_RETENTION) == CHECK_RETENTION) {
if (hasRetention(item, isRemoval)) {
return false;
}
}
return true;
}
private void checkLock(ItemImpl item) throws LockException, RepositoryException {
if (item.isNew()) {
// a new item needs no check
return;
}
NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent();
context.getWorkspace().getInternalLockManager().checkLock(node);
}
private boolean isProtected(ItemImpl item) throws RepositoryException {
ItemDefinition def;
if (item.isNode()) {
def = ((Node) item).getDefinition();
} else {
def = ((Property) item).getDefinition();
}
return def.isProtected();
}
private boolean hasHold(ItemImpl item, boolean isRemoval) throws RepositoryException {
if (item.isNew()) {
return false;
}
Path path = item.getPrimaryPath();
if (!item.isNode()) {
path = path.getAncestor(1);
}
boolean checkParent = (item.isNode() && isRemoval);
return context.getSessionImpl().getRetentionRegistry().hasEffectiveHold(path, checkParent);
}
private boolean hasRetention(ItemImpl item, boolean isRemoval) throws RepositoryException {
if (item.isNew()) {
return false;
}
Path path = item.getPrimaryPath();
if (!item.isNode()) {
path = path.getAncestor(1);
}
boolean checkParent = (item.isNode() && isRemoval);
return context.getSessionImpl().getRetentionRegistry().hasEffectiveRetention(path, checkParent);
}
//-------------------------------------------------< misc. helper methods >
/**
* Helper method that builds the effective (i.e. merged and resolved)
* node type representation of the specified node's primary and mixin
* node types.
*
* @param state
* @return the effective node type
* @throws RepositoryException
*/
public EffectiveNodeType getEffectiveNodeType(NodeState state)
throws RepositoryException {
try {
return context.getNodeTypeRegistry().getEffectiveNodeType(
state.getNodeTypeName(), state.getMixinTypeNames());
} catch (NodeTypeConflictException ntce) {
String msg = "internal error: failed to build effective node type for node "
+ safeGetJCRPath(state.getNodeId());
log.debug(msg);
throw new RepositoryException(msg, ntce);
}
}
/**
* Helper method that finds the applicable definition for a child node with
* the given name and node type in the parent node's node type and
* mixin types.
*
* @param name
* @param nodeTypeName
* @param parentState
* @return a <code>QNodeDefinition</code>
* @throws ConstraintViolationException if no applicable child node definition
* could be found
* @throws RepositoryException if another error occurs
*/
public QNodeDefinition findApplicableNodeDefinition(Name name,
Name nodeTypeName,
NodeState parentState)
throws RepositoryException, ConstraintViolationException {
EffectiveNodeType entParent = getEffectiveNodeType(parentState);
return entParent.getApplicableChildNodeDef(
name, nodeTypeName, context.getNodeTypeRegistry());
}
/**
* Helper method that finds the applicable definition for a property with
* the given name, type and multiValued characteristic in the parent node's
* node type and mixin types. If there more than one applicable definitions
* then the following rules are applied:
* <ul>
* <li>named definitions are preferred to residual definitions</li>
* <li>definitions with specific required type are preferred to definitions
* with required type UNDEFINED</li>
* </ul>
*
* @param name
* @param type
* @param multiValued
* @param parentState
* @return a <code>QPropertyDefinition</code>
* @throws ConstraintViolationException if no applicable property definition
* could be found
* @throws RepositoryException if another error occurs
*/
public QPropertyDefinition findApplicablePropertyDefinition(Name name,
int type,
boolean multiValued,
NodeState parentState)
throws RepositoryException, ConstraintViolationException {
EffectiveNodeType entParent = getEffectiveNodeType(parentState);
return entParent.getApplicablePropertyDef(name, type, multiValued);
}
/**
* Helper method that finds the applicable definition for a property with
* the given name, type in the parent node's node type and mixin types.
* Other than <code>{@link #findApplicablePropertyDefinition(Name, int, boolean, NodeState)}</code>
* this method does not take the multiValued flag into account in the
* selection algorithm. If there more than one applicable definitions then
* the following rules are applied:
* <ul>
* <li>named definitions are preferred to residual definitions</li>
* <li>definitions with specific required type are preferred to definitions
* with required type UNDEFINED</li>
* <li>single-value definitions are preferred to multiple-value definitions</li>
* </ul>
*
* @param name
* @param type
* @param parentState
* @return a <code>QPropertyDefinition</code>
* @throws ConstraintViolationException if no applicable property definition
* could be found
* @throws RepositoryException if another error occurs
*/
public QPropertyDefinition findApplicablePropertyDefinition(Name name,
int type,
NodeState parentState)
throws RepositoryException, ConstraintViolationException {
EffectiveNodeType entParent = getEffectiveNodeType(parentState);
return entParent.getApplicablePropertyDef(name, type);
}
/**
* Failsafe conversion of internal <code>Path</code> to JCR path for use in
* error messages etc.
*
* @param path path to convert
* @return JCR path
*/
public String safeGetJCRPath(Path path) {
try {
return context.getJCRPath(path);
} catch (NamespaceException e) {
log.error("failed to convert {} to a JCR path", path);
// return string representation of internal path as a fallback
return path.toString();
}
}
/**
* Failsafe translation of internal <code>ItemId</code> to JCR path for use
* in error messages etc.
*
* @param id id to translate
* @return JCR path
*/
public String safeGetJCRPath(ItemId id) {
try {
return safeGetJCRPath(
context.getHierarchyManager().getPath(id));
} catch (ItemNotFoundException e) {
// return string representation of id as a fallback
return id.toString();
} catch (RepositoryException e) {
log.error(id + ": failed to build path");
// return string representation of id as a fallback
return id.toString();
}
}
}