| /* |
| * 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.oak.jcr.delegate; |
| |
| import static org.apache.jackrabbit.guava.common.base.MoreObjects.toStringHelper; |
| import static org.apache.jackrabbit.guava.common.collect.Iterables.addAll; |
| import static org.apache.jackrabbit.guava.common.collect.Iterables.contains; |
| import static org.apache.jackrabbit.guava.common.collect.Iterators.filter; |
| import static org.apache.jackrabbit.guava.common.collect.Iterators.transform; |
| import static org.apache.jackrabbit.guava.common.collect.Lists.newArrayList; |
| import static org.apache.jackrabbit.guava.common.collect.Sets.newHashSet; |
| import static org.apache.jackrabbit.guava.common.collect.Sets.newLinkedHashSet; |
| import static org.apache.jackrabbit.JcrConstants.JCR_ISMIXIN; |
| import static org.apache.jackrabbit.JcrConstants.JCR_LOCKISDEEP; |
| import static org.apache.jackrabbit.JcrConstants.JCR_LOCKOWNER; |
| import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; |
| import static org.apache.jackrabbit.JcrConstants.JCR_MULTIPLE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_NODETYPENAME; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PROTECTED; |
| import static org.apache.jackrabbit.JcrConstants.JCR_REQUIREDTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_SAMENAMESIBLINGS; |
| import static org.apache.jackrabbit.JcrConstants.JCR_UUID; |
| import static org.apache.jackrabbit.JcrConstants.MIX_LOCKABLE; |
| import static org.apache.jackrabbit.JcrConstants.NT_BASE; |
| import static org.apache.jackrabbit.oak.api.Type.BOOLEAN; |
| import static org.apache.jackrabbit.oak.api.Type.NAMES; |
| import static org.apache.jackrabbit.oak.api.Type.UNDEFINED; |
| import static org.apache.jackrabbit.oak.api.Type.UNDEFINEDS; |
| import static org.apache.jackrabbit.oak.commons.PathUtils.dropIndexFromName; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.JCR_IS_ABSTRACT; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.NODE_TYPES_PATH; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_HAS_PROTECTED_RESIDUAL_CHILD_NODES; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_HAS_PROTECTED_RESIDUAL_PROPERTIES; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_NAMED_CHILD_NODE_DEFINITIONS; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_NAMED_PROPERTY_DEFINITIONS; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_PROTECTED_CHILD_NODES; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_PROTECTED_PROPERTIES; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_RESIDUAL_CHILD_NODE_DEFINITIONS; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_RESIDUAL_PROPERTY_DEFINITIONS; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_SUPERTYPES; |
| import static org.apache.jackrabbit.oak.plugins.tree.TreeUtil.getBoolean; |
| import static org.apache.jackrabbit.oak.plugins.tree.TreeUtil.getNames; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.jcr.InvalidItemStateException; |
| import javax.jcr.ItemNotFoundException; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.ValueFormatException; |
| import javax.jcr.lock.LockException; |
| import javax.jcr.nodetype.ConstraintViolationException; |
| import javax.jcr.nodetype.NoSuchNodeTypeException; |
| import javax.jcr.security.AccessControlException; |
| |
| import org.apache.jackrabbit.guava.common.base.Function; |
| import org.apache.jackrabbit.guava.common.base.Predicate; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.jackrabbit.oak.api.CommitFailedException; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Root; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.api.Tree.Status; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.jcr.lock.LockDeprecation; |
| import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; |
| import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; |
| import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants; |
| import org.apache.jackrabbit.oak.plugins.tree.TreeUtil; |
| import org.apache.jackrabbit.value.ValueHelper; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| /** |
| * {@code NodeDelegate} serve as internal representations of {@code Node}s. |
| * Most methods of this class throw an {@code InvalidItemStateException} |
| * exception if the instance is stale. An instance is stale if the underlying |
| * items does not exist anymore. |
| */ |
| public class NodeDelegate extends ItemDelegate { |
| |
| /** The underlying {@link org.apache.jackrabbit.oak.api.Tree} of this node. */ |
| private final Tree tree; |
| |
| public NodeDelegate(SessionDelegate sessionDelegate, Tree tree) { |
| super(sessionDelegate); |
| this.tree = tree; |
| } |
| |
| @Override |
| @NotNull |
| public String getName() { |
| return tree.getName(); |
| } |
| |
| @Override |
| @NotNull |
| public String getPath() { |
| return tree.getPath(); |
| } |
| |
| @Override |
| @Nullable |
| public NodeDelegate getParent() { |
| return tree.isRoot() || !tree.getParent().exists() |
| ? null |
| : new NodeDelegate(sessionDelegate, tree.getParent()); |
| } |
| |
| @Override |
| public boolean exists() { |
| return tree.exists(); |
| } |
| |
| @Override |
| @Nullable |
| public Status getStatus() { |
| return tree.getStatus(); |
| } |
| |
| @NotNull |
| public String getIdentifier() throws InvalidItemStateException { |
| return IdentifierManager.getIdentifier(getTree()); |
| } |
| |
| @Override |
| public boolean isProtected() throws InvalidItemStateException { |
| Tree tree = getTree(); |
| if (tree.isRoot()) { |
| return false; |
| } |
| |
| Tree parent = tree.getParent(); |
| String nameWithIndex = tree.getName(); |
| String name = dropIndexFromName(nameWithIndex); |
| boolean sns = !name.equals(nameWithIndex); |
| Tree typeRoot = sessionDelegate.getRoot().getTree(NODE_TYPES_PATH); |
| List<Tree> types = TreeUtil.getEffectiveType(parent, typeRoot); |
| |
| boolean protectedResidual = false; |
| for (Tree type : types) { |
| if (contains(TreeUtil.getNames(type, REP_PROTECTED_CHILD_NODES), name)) { |
| return true; |
| } else if (!protectedResidual) { |
| protectedResidual = TreeUtil.getBoolean( |
| type, REP_HAS_PROTECTED_RESIDUAL_CHILD_NODES); |
| } |
| } |
| |
| // Special case: There are one or more protected *residual* |
| // child node definitions. Iterate through them to check whether |
| // there's a matching, protected one. |
| if (protectedResidual) { |
| Set<String> typeNames = newHashSet(); |
| for (Tree type : TreeUtil.getEffectiveType(tree, typeRoot)) { |
| typeNames.add(TreeUtil.getName(type, JCR_NODETYPENAME)); |
| addAll(typeNames, TreeUtil.getNames(type, REP_SUPERTYPES)); |
| } |
| |
| for (Tree type : types) { |
| Tree definitions = |
| type.getChild(REP_RESIDUAL_CHILD_NODE_DEFINITIONS); |
| for (String typeName : typeNames) { |
| Tree definition = definitions.getChild(typeName); |
| if ((!sns || TreeUtil.getBoolean(definition, JCR_SAMENAMESIBLINGS)) |
| && TreeUtil.getBoolean(definition, JCR_PROTECTED)) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| boolean isProtected(String propertyName, Type<?> propertyType) throws InvalidItemStateException { |
| Tree tree = getTree(); |
| Tree typeRoot = sessionDelegate.getRoot().getTree(NODE_TYPES_PATH); |
| List<Tree> types = TreeUtil.getEffectiveType(tree, typeRoot); |
| |
| boolean protectedResidual = false; |
| for (Tree type : types) { |
| if (contains(TreeUtil.getNames(type, REP_PROTECTED_PROPERTIES), propertyName)) { |
| return true; |
| } else if (!protectedResidual) { |
| protectedResidual = TreeUtil.getBoolean( |
| type, REP_HAS_PROTECTED_RESIDUAL_PROPERTIES); |
| } |
| } |
| |
| // Special case: There are one or more protected *residual* |
| // property definitions. Iterate through them to check whether |
| // there's a matching, protected one. |
| if (protectedResidual) { |
| Tree definition = findMatchingResidualPropertyDefinition(types, propertyType); |
| if (definition != null && TreeUtil.getBoolean(definition, JCR_PROTECTED)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Determine whether this is the root node |
| * |
| * @return {@code true} iff this is the root node |
| */ |
| public boolean isRoot() throws InvalidItemStateException { |
| return getTree().isRoot(); |
| } |
| |
| /** |
| * Get the number of properties of the node |
| * |
| * @return number of properties of the node |
| */ |
| public long getPropertyCount() throws InvalidItemStateException { |
| return getTree().getPropertyCount(); |
| } |
| |
| /** |
| * Get a property |
| * |
| * @param relPath oak path |
| * @return property at the path given by {@code relPath} or {@code null} if |
| * no such property exists |
| */ |
| @Nullable |
| public PropertyDelegate getPropertyOrNull(String relPath) |
| throws RepositoryException { |
| Tree parent = tree; |
| String name = relPath; |
| |
| int slash = relPath.lastIndexOf('/'); |
| if (slash != -1) { |
| parent = getTree(relPath.substring(0, slash)); |
| name = relPath.substring(slash + 1); |
| } |
| |
| if (parent != null) { |
| PropertyDelegate property = |
| new PropertyDelegate(sessionDelegate, parent, name); |
| if (property.exists()) { |
| return property; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Get a property. In contrast to {@link #getPropertyOrNull(String)} this |
| * method never returns {@code null}. In the case where no property exists |
| * at the given path, the returned property delegate throws an |
| * {@code InvalidItemStateException} on access. See See OAK-395. |
| * |
| * @param relPath oak path |
| * @return property at the path given by {@code relPath}. |
| */ |
| @NotNull |
| public PropertyDelegate getProperty(String relPath) throws RepositoryException { |
| Tree parent = tree; |
| String name = relPath; |
| |
| int slash = relPath.lastIndexOf('/'); |
| if (slash != -1) { |
| parent = getTree(relPath.substring(0, slash)); |
| name = relPath.substring(slash + 1); |
| } |
| |
| return new PropertyDelegate(sessionDelegate, parent, name); |
| } |
| |
| /** |
| * Get the properties of the node |
| * |
| * @return properties of the node |
| */ |
| @NotNull |
| public Iterator<PropertyDelegate> getProperties() throws InvalidItemStateException { |
| return transform(getTree().getProperties().iterator(), |
| new Function<PropertyState, PropertyDelegate>() { |
| @Override |
| public PropertyDelegate apply(PropertyState propertyState) { |
| return new PropertyDelegate(sessionDelegate, tree, propertyState.getName()); |
| } |
| }); |
| } |
| |
| /** |
| * Get the number of child nodes |
| * <p> |
| * If an implementation does know the exact value, it returns it (even if |
| * the value is higher than max). If the implementation does not know the |
| * exact value, and the child node count is higher than max, it may return |
| * Long.MAX_VALUE. The cost of the operation is at most O(max). |
| * |
| * @param max the maximum value |
| * @return number of child nodes of the node |
| */ |
| public long getChildCount(long max) throws InvalidItemStateException { |
| return getTree().getChildrenCount(max); |
| } |
| |
| /** |
| * Get child node |
| * |
| * @param relPath oak path |
| * @return node at the path given by {@code relPath} or {@code null} if |
| * no such node exists |
| */ |
| @Nullable |
| public NodeDelegate getChild(String relPath) throws RepositoryException { |
| if (relPath.isEmpty()) { |
| return this; |
| } |
| Tree tree = getTree(relPath); |
| return tree == null || !tree.exists() ? null : new NodeDelegate(sessionDelegate, tree); |
| } |
| |
| /** |
| * Returns an iterator for traversing all the children of this node. |
| * If the node is orderable then the iterator will return child nodes in the |
| * specified order. Otherwise the ordering of the iterator is undefined. |
| * |
| * @return child nodes of the node |
| */ |
| @NotNull |
| public Iterator<NodeDelegate> getChildren() throws InvalidItemStateException { |
| Iterator<Tree> iterator = getTree().getChildren().iterator(); |
| return transform( |
| filter(iterator, new Predicate<Tree>() { |
| @Override |
| public boolean apply(Tree tree) { |
| return tree.exists(); |
| } |
| }), |
| new Function<Tree, NodeDelegate>() { |
| @Override |
| public NodeDelegate apply(Tree tree) { |
| return new NodeDelegate(sessionDelegate, tree); |
| } |
| }); |
| } |
| |
| public void orderBefore(String source, String target) |
| throws ItemNotFoundException, InvalidItemStateException { |
| Tree tree = getTree(); |
| if (!tree.getChild(source).exists()) { |
| throw new ItemNotFoundException("Not a child: " + source); |
| } else if (target != null && !tree.getChild(target).exists()) { |
| throw new ItemNotFoundException("Not a child: " + target); |
| } else { |
| tree.getChild(source).orderBefore(target); |
| } |
| } |
| |
| public boolean canAddMixin(String typeName) throws RepositoryException { |
| if (!LockDeprecation.isLockingSupported() && JcrConstants.MIX_LOCKABLE.equals(typeName)) { |
| return false; |
| } |
| Tree type = sessionDelegate.getRoot().getTree(NODE_TYPES_PATH).getChild(typeName); |
| if (type.exists()) { |
| return !TreeUtil.getBoolean(type, JCR_IS_ABSTRACT) |
| && TreeUtil.getBoolean(type, JCR_ISMIXIN); |
| } else { |
| throw new NoSuchNodeTypeException( |
| "Node type " + typeName + " does not exist"); |
| } |
| } |
| |
| public void addMixin(Function<Tree, Iterable<String>> existingMixins, String typeName) throws RepositoryException { |
| TreeUtil.addMixin(getTree(), existingMixins, typeName, sessionDelegate.getRoot().getTree(NODE_TYPES_PATH), getUserID()); |
| } |
| |
| public void removeMixin(String typeName) throws RepositoryException { |
| Tree tree = getTree(); |
| Set<String> mixins = newLinkedHashSet(getNames(tree, JCR_MIXINTYPES)); |
| if (!mixins.remove(typeName)) { |
| throw new NoSuchNodeTypeException("Mixin " + typeName +" not contained in " + getPath()); |
| } |
| updateMixins(mixins, Collections.singleton(typeName)); |
| } |
| |
| public void setMixins(Set<String> mixinNames) throws RepositoryException { |
| Set<String> existingMixins = newLinkedHashSet(getNames(tree, JCR_MIXINTYPES)); |
| if (existingMixins.isEmpty()) { |
| updateMixins(mixinNames, Collections.<String>emptySet()); |
| } else { |
| Set<String> toRemove = newLinkedHashSet(); |
| for (String name : existingMixins) { |
| if (!mixinNames.remove(name)) { |
| toRemove.add(name); |
| } |
| } |
| updateMixins(mixinNames, toRemove); |
| } |
| } |
| |
| |
| public void updateMixins(Set<String> addMixinNames, Set<String> removedOakMixinNames) throws RepositoryException { |
| // 1. set all new mixin types including validation |
| for (String oakMixinName : addMixinNames) { |
| addMixin(TreeUtil::getMixinTypeNames, oakMixinName); |
| } |
| |
| if (!removedOakMixinNames.isEmpty()) { |
| // 2. retrieve the updated set of mixin types, remove the mixins that should no longer be present |
| Set<String> mixinNames = newLinkedHashSet(getNames(getTree(), JCR_MIXINTYPES)); |
| if (mixinNames.removeAll(removedOakMixinNames)) { |
| // FIXME: add mixins to add again as the removal may change the effect of type inheritance as evaluated during #addMixin |
| mixinNames.addAll(addMixinNames); |
| tree.setProperty(JCR_MIXINTYPES, mixinNames, NAMES); |
| } |
| |
| // 3. clean up set of properties and child nodes such that all child items |
| // have a valid item definition according to the effective node type present |
| // after having updated the mixin property. this includes removing all |
| // protected properties and child nodes associated with the removed mixin |
| // type(s), as there's no way for the client to do that. Other items |
| // defined in this mixin type might also need to be removed if there |
| // is no longer a matching item definition available. |
| Tree typeRoot = sessionDelegate.getRoot().getTree(NODE_TYPES_PATH); |
| List<Tree> removed = new ArrayList<Tree>(); |
| for (String name : removedOakMixinNames) { |
| removed.add(typeRoot.getChild(name)); |
| } |
| List<Tree> remaining = getNodeTypes(tree, typeRoot); |
| |
| for (PropertyState property : tree.getProperties()) { |
| String name = property.getName(); |
| Type<?> type = property.getType(); |
| |
| Tree oldDefinition = findMatchingPropertyDefinition(removed, name, type, true); |
| if (oldDefinition != null) { |
| Tree newDefinition = findMatchingPropertyDefinition(remaining, name, type, true); |
| if (newDefinition == null |
| || (getBoolean(oldDefinition, JCR_PROTECTED) |
| && !getBoolean(newDefinition, JCR_PROTECTED))) { |
| tree.removeProperty(name); |
| } |
| } |
| } |
| |
| for (Tree child : tree.getChildren()) { |
| String name = child.getName(); |
| Set<String> typeNames = newLinkedHashSet(); |
| for (Tree type : getNodeTypes(child, typeRoot)) { |
| typeNames.add(TreeUtil.getName(type, JCR_NODETYPENAME)); |
| addAll(typeNames, getNames(type, REP_SUPERTYPES)); |
| } |
| |
| Tree oldDefinition = findMatchingChildNodeDefinition(removed, name, typeNames); |
| if (oldDefinition != null) { |
| Tree newDefinition = findMatchingChildNodeDefinition(remaining, name, typeNames); |
| if (newDefinition == null |
| || (getBoolean(oldDefinition, JCR_PROTECTED) |
| && !getBoolean(newDefinition, JCR_PROTECTED))) { |
| child.remove(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Set a property |
| * |
| * @return the set property |
| */ |
| @NotNull |
| public PropertyDelegate setProperty( |
| PropertyState propertyState, boolean exactTypeMatch, |
| boolean setProtected) throws RepositoryException { |
| Tree tree = getTree(); |
| String name = propertyState.getName(); |
| Type<?> type = propertyState.getType(); |
| |
| PropertyState old = tree.getProperty(name); |
| if (old != null && old.isArray() && !propertyState.isArray()) { |
| throw new ValueFormatException( |
| "Can not assign a single value to multi-valued property: " |
| + propertyState); |
| } |
| if (old != null && !old.isArray() && propertyState.isArray()) { |
| throw new ValueFormatException( |
| "Can not assign multiple values to single valued property: " |
| + propertyState); |
| } |
| |
| Tree definition = findMatchingPropertyDefinition( |
| getNodeTypes(tree), name, type, exactTypeMatch); |
| if (definition == null) { |
| throw new ConstraintViolationException( |
| "No matching property definition: " + propertyState); |
| } else if (!setProtected && TreeUtil.getBoolean(definition, JCR_PROTECTED)) { |
| throw new ConstraintViolationException( |
| "Property is protected: " + propertyState); |
| } |
| Type<?> requiredType = |
| Type.fromString(TreeUtil.getString(definition, JCR_REQUIREDTYPE)); |
| if (requiredType != Type.UNDEFINED) { |
| if (TreeUtil.getBoolean(definition, JCR_MULTIPLE)) { |
| requiredType = requiredType.getArrayType(); |
| } |
| ValueHelper.checkSupportedConversion(propertyState.getType().tag(), requiredType.tag()); |
| propertyState = PropertyStates.convert(propertyState, requiredType); |
| } |
| |
| tree.setProperty(propertyState); |
| return new PropertyDelegate(sessionDelegate, tree, name); |
| } |
| |
| /** |
| * Returns the type nodes of the primary and mixin types of the given node. |
| * |
| * @param tree node |
| * @return primary and mixin type nodes |
| */ |
| private List<Tree> getNodeTypes(Tree tree) { |
| Root root = sessionDelegate.getRoot(); |
| return getNodeTypes(tree, root.getTree(NODE_TYPES_PATH)); |
| } |
| |
| private List<Tree> getNodeTypes(Tree tree, Tree typeRoot) { |
| // Find applicable node types |
| List<Tree> types = newArrayList(); |
| String primaryName = TreeUtil.getName(tree, JCR_PRIMARYTYPE); |
| if (primaryName == null) { |
| primaryName = NT_BASE; |
| } |
| types.add(typeRoot.getChild(primaryName)); |
| for (String mixinName : TreeUtil.getNames(tree, JCR_MIXINTYPES)) { |
| types.add(typeRoot.getChild(mixinName)); |
| } |
| |
| return types; |
| } |
| |
| private boolean isNodeType(String typeName) { |
| return isNodeType(tree, typeName, sessionDelegate.getRoot()); |
| } |
| |
| private boolean isNodeType(Tree tree, String typeName, Root root) { |
| return TreeUtil.isNodeType(tree, typeName, root.getTree(NODE_TYPES_PATH)); |
| } |
| |
| private Tree findMatchingPropertyDefinition( |
| List<Tree> types, String propertyName, Type<?> propertyType, |
| boolean exactTypeMatch) { |
| // Escape the property name for looking up a matching definition |
| String escapedName; |
| if (JCR_PRIMARYTYPE.equals(propertyName)) { |
| escapedName = NodeTypeConstants.REP_PRIMARY_TYPE; |
| } else if (JCR_MIXINTYPES.equals(propertyName)) { |
| escapedName = NodeTypeConstants.REP_MIXIN_TYPES; |
| } else if (JCR_UUID.equals(propertyName)) { |
| escapedName = NodeTypeConstants.REP_UUID; |
| } else { |
| escapedName = propertyName; |
| } |
| |
| String definedType = propertyType.toString(); |
| String undefinedType = UNDEFINED.toString(); |
| if (propertyType.isArray()) { |
| undefinedType = UNDEFINEDS.toString(); |
| } |
| |
| // First look for a matching named property definition |
| Tree fuzzyMatch = null; |
| for (Tree type : types) { |
| Tree definitions = type |
| .getChild(REP_NAMED_PROPERTY_DEFINITIONS) |
| .getChild(escapedName); |
| Tree definition = definitions.getChild(definedType); |
| if (definition.exists()) { |
| return definition; |
| } |
| definition = definitions.getChild(undefinedType); |
| if (definition.exists()) { |
| return definition; |
| } |
| for (Tree def : definitions.getChildren()) { |
| if (propertyType.isArray() == TreeUtil.getBoolean(def, JCR_MULTIPLE)) { |
| if (getBoolean(def, JCR_PROTECTED)) { |
| return null; // no fuzzy matches for protected items |
| } else if (!exactTypeMatch && fuzzyMatch == null) { |
| fuzzyMatch = def; |
| } |
| } |
| } |
| } |
| |
| // Then look through any residual property definitions |
| return findMatchingResidualPropertyDefinition(fuzzyMatch, types, propertyType.isArray(), definedType, undefinedType, exactTypeMatch); |
| } |
| |
| private Tree findMatchingResidualPropertyDefinition(List<Tree> types, Type<?> propertyType) { |
| String definedType = propertyType.toString(); |
| String undefinedType = UNDEFINED.toString(); |
| if (propertyType.isArray()) { |
| undefinedType = UNDEFINEDS.toString(); |
| } |
| return findMatchingResidualPropertyDefinition(null, types, propertyType.isArray(), definedType, undefinedType, true); |
| } |
| |
| private Tree findMatchingResidualPropertyDefinition(Tree fuzzyMatch, List<Tree> types, boolean isMultiValue, String definedType, String undefinedType, boolean exactTypeMatch) { |
| for (Tree type : types) { |
| Tree definitions = type.getChild(REP_RESIDUAL_PROPERTY_DEFINITIONS); |
| Tree definition = definitions.getChild(definedType); |
| if (definition.exists()) { |
| return definition; |
| } |
| definition = definitions.getChild(undefinedType); |
| if (definition.exists()) { |
| return definition; |
| } |
| if (!exactTypeMatch && fuzzyMatch == null) { |
| for (Tree def : definitions.getChildren()) { |
| if (isMultiValue == TreeUtil.getBoolean(def, JCR_MULTIPLE)) { |
| fuzzyMatch = def; |
| break; |
| } |
| } |
| } |
| } |
| |
| return fuzzyMatch; |
| } |
| |
| private Tree findMatchingChildNodeDefinition( |
| List<Tree> types, String childNodeName, Set<String> typeNames) { |
| // First look for a matching named property definition |
| for (Tree type : types) { |
| Tree definitions = type |
| .getChild(REP_NAMED_CHILD_NODE_DEFINITIONS) |
| .getChild(childNodeName); |
| for (String typeName : typeNames) { |
| Tree definition = definitions.getChild(typeName); |
| if (definition.exists()) { |
| return definition; |
| } |
| } |
| } |
| |
| // Then look through any residual property definitions |
| for (Tree type : types) { |
| Tree definitions = type |
| .getChild(REP_RESIDUAL_CHILD_NODE_DEFINITIONS); |
| for (String typeName : typeNames) { |
| Tree definition = definitions.getChild(typeName); |
| if (definition.exists()) { |
| return definition; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Add a child node |
| * |
| * @param name Oak name of the new child node |
| * @param typeName Oak name of the type of the new child node, |
| * or {@code null} if a default type should be used |
| * @return the added node or {@code null} if such a node already exists |
| */ |
| @Nullable |
| public NodeDelegate addChild(String name, String typeName) |
| throws RepositoryException { |
| Tree tree = getTree(); |
| if (tree.hasChild(name)) { |
| return null; |
| } |
| |
| Tree typeRoot = sessionDelegate.getRoot().getTree(NODE_TYPES_PATH); |
| Tree child = TreeUtil.addChild(tree, name, typeName, typeRoot, getUserID()); |
| return new NodeDelegate(sessionDelegate, child); |
| } |
| |
| /** |
| * Remove this node. This operation never succeeds for the root node. |
| * |
| * @return {@code true} if the node was removed; {@code false} otherwise. |
| */ |
| @Override |
| public boolean remove() throws InvalidItemStateException { |
| return getTree().remove(); |
| } |
| |
| /** |
| * Enables or disabled orderable children on the underlying tree. |
| * |
| * @param enable whether to enable or disable orderable children. |
| */ |
| public void setOrderableChildren(boolean enable) |
| throws InvalidItemStateException { |
| getTree().setOrderableChildren(enable); |
| } |
| |
| /** |
| * Checks whether this node is locked, either directly or through |
| * a deep lock on an ancestor. |
| * |
| * @return whether this node is locked |
| */ |
| // FIXME: access to locking status should not depend on access rights (OAK-4234) |
| public boolean isLocked() { |
| return getLock() != null; |
| } |
| |
| @Nullable |
| public NodeDelegate getLock() { |
| Tree lock = findLock(tree, false); |
| if (lock != null) { |
| NodeDelegate delegate = new NodeDelegate(sessionDelegate, lock); |
| if (delegate.isNodeType(MIX_LOCKABLE)) { |
| return delegate; |
| } else if (lock.isRoot()) { |
| return null; |
| } else { |
| lock = findLock(lock.getParent(), true); |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private Tree findLock(@NotNull Tree tree, boolean deep) { |
| if (holdsLock(tree, deep)) { |
| return tree; |
| } else if (tree.isRoot()) { |
| return null; |
| } else { |
| return findLock(tree.getParent(), true); |
| } |
| } |
| |
| private boolean holdsLock(Tree tree, boolean deep) { |
| // FIXME: access to locking status should not depend on access rights (OAK-4234) |
| PropertyState property = tree.getProperty(JCR_LOCKISDEEP); |
| return property != null |
| && property.getType() == Type.BOOLEAN |
| && (!deep || property.getValue(BOOLEAN)); |
| } |
| |
| @Nullable |
| private Tree findDescendantLock(@NotNull Tree tree) { |
| for (Tree child : tree.getChildren()) { |
| if (holdsLock(child, false)) { |
| return child; |
| } else { |
| Tree desc = findDescendantLock(child); |
| if (desc != null) { |
| return desc; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Checks whether this node holds a lock. |
| * |
| * @param deep if {@code true}, only check for deep locks |
| * @return whether this node holds a lock |
| */ |
| public boolean holdsLock(boolean deep) { |
| return holdsLock(tree, deep) && isNodeType(MIX_LOCKABLE); |
| } |
| |
| public String getLockOwner() { |
| PropertyState property = tree.getProperty(JCR_LOCKOWNER); |
| if (property != null && property.getType() == Type.STRING) { |
| return property.getValue(Type.STRING); |
| } else { |
| return null; |
| } |
| } |
| |
| public boolean isLockOwner(String user) { |
| return user != null && user.equals(getLockOwner()); |
| } |
| |
| public void lock(boolean isDeep) throws RepositoryException { |
| String path = getPath(); |
| |
| Root root = sessionDelegate.getContentSession().getLatestRoot(); |
| Tree tree = root.getTree(path); |
| if (!tree.exists()) { |
| throw new ItemNotFoundException("Node " + path + " does not exist"); |
| } else if (!isNodeType(tree, MIX_LOCKABLE, root)) { |
| throw new LockException("Node " + path + " is not lockable"); |
| } else if (tree.hasProperty(JCR_LOCKISDEEP)) { |
| throw new LockException("Node " + path + " is already locked"); |
| } |
| |
| // look for locked ancestor |
| Tree inheritedLock = findLock(tree, true); |
| if (inheritedLock != null) { |
| throw new LockException("Node already indirectly locked by " + inheritedLock.getPath()); |
| } |
| |
| // scan for locked descendant |
| if (isDeep) { |
| Tree descendantLock = findDescendantLock(tree); |
| if (descendantLock != null) { |
| throw new LockException("Lock conflicts with lock hold by " + descendantLock.getPath()); |
| } |
| } |
| |
| try { |
| String owner = sessionDelegate.getAuthInfo().getUserID(); |
| if (owner == null) { |
| owner = ""; |
| } |
| tree.setProperty(JCR_LOCKISDEEP, isDeep); |
| tree.setProperty(JCR_LOCKOWNER, owner); |
| sessionDelegate.commit(root); |
| } catch (CommitFailedException e) { |
| if (e.isAccessViolation()) { |
| throw new AccessControlException( |
| "Access denied to lock node " + path, e); |
| } else { |
| throw new RepositoryException( |
| "Unable to lock node " + path, e); |
| } |
| } |
| } |
| |
| public void unlock() throws RepositoryException { |
| String path = getPath(); |
| |
| Root root = sessionDelegate.getContentSession().getLatestRoot(); |
| Tree tree = root.getTree(path); |
| if (!tree.exists()) { |
| throw new ItemNotFoundException("Node " + path + " does not exist"); |
| } else if (!isNodeType(tree, MIX_LOCKABLE, root)) { |
| throw new LockException("Node " + path + " is not lockable"); |
| } else if (!tree.hasProperty(JCR_LOCKISDEEP)) { |
| throw new LockException("Node " + path + " is not locked"); |
| } |
| |
| try { |
| tree.removeProperty(JCR_LOCKISDEEP); |
| tree.removeProperty(JCR_LOCKOWNER); |
| sessionDelegate.commit(root); |
| } catch (CommitFailedException e) { |
| if (e.isAccessViolation()) { |
| throw new AccessControlException( |
| "Access denied to unlock node " + path, e); |
| } else { |
| throw new RepositoryException( |
| "Unable to unlock node " + path, e); |
| } |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return toStringHelper(this).add("tree", tree).toString(); |
| } |
| |
| //------------------------------------------------------------< internal >--- |
| |
| @NotNull // FIXME this should be package private. OAK-672 |
| public Tree getTree() throws InvalidItemStateException { |
| if (!tree.exists()) { |
| throw new InvalidItemStateException("Item is stale " + tree.getPath()); |
| } |
| return tree; |
| } |
| |
| // -----------------------------------------------------------< private >--- |
| |
| private Tree getTree(String relPath) throws RepositoryException { |
| if (PathUtils.isAbsolute(relPath)) { |
| throw new RepositoryException("Not a relative path: " + relPath); |
| } |
| |
| return TreeUtil.getTree(tree, relPath); |
| } |
| |
| private String getUserID() { |
| return sessionDelegate.getAuthInfo().getUserID(); |
| } |
| |
| } |