| /* |
| * 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.plugins.tree; |
| |
| import java.util.Calendar; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.jcr.AccessDeniedException; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.nodetype.ConstraintViolationException; |
| import javax.jcr.nodetype.NoSuchNodeTypeException; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| |
| import org.apache.jackrabbit.JcrConstants; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.commons.UUIDUtils; |
| import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; |
| import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants; |
| import org.apache.jackrabbit.util.ISO8601; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import static com.google.common.collect.Iterables.contains; |
| import static com.google.common.collect.Lists.newArrayList; |
| import static java.util.Collections.emptyList; |
| import static org.apache.jackrabbit.JcrConstants.JCR_AUTOCREATED; |
| import static org.apache.jackrabbit.JcrConstants.JCR_CREATED; |
| import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTPRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTVALUES; |
| import static org.apache.jackrabbit.JcrConstants.JCR_HASORDERABLECHILDNODES; |
| import static org.apache.jackrabbit.JcrConstants.JCR_ISMIXIN; |
| import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED; |
| import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; |
| import static org.apache.jackrabbit.JcrConstants.JCR_MULTIPLE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_SAMENAMESIBLINGS; |
| import static org.apache.jackrabbit.JcrConstants.JCR_UUID; |
| import static org.apache.jackrabbit.oak.api.Type.BOOLEAN; |
| import static org.apache.jackrabbit.oak.api.Type.DATE; |
| import static org.apache.jackrabbit.oak.api.Type.LONG; |
| import static org.apache.jackrabbit.oak.api.Type.NAME; |
| import static org.apache.jackrabbit.oak.api.Type.NAMES; |
| import static org.apache.jackrabbit.oak.api.Type.STRING; |
| import static org.apache.jackrabbit.oak.api.Type.STRINGS; |
| import static org.apache.jackrabbit.oak.commons.PathUtils.dropIndexFromName; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.JCR_CREATEDBY; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.JCR_IS_ABSTRACT; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.JCR_LASTMODIFIEDBY; |
| 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_RESIDUAL_CHILD_NODE_DEFINITIONS; |
| import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_SUPERTYPES; |
| |
| /** |
| * Utility providing common operations for the {@link org.apache.jackrabbit.oak.api.Tree} that are not provided |
| * by the API. |
| */ |
| public final class TreeUtil { |
| |
| private TreeUtil() { |
| } |
| |
| @Nullable |
| public static String getPrimaryTypeName(@NotNull Tree tree) { |
| return getStringInternal(tree, JcrConstants.JCR_PRIMARYTYPE, Type.NAME); |
| } |
| |
| @Nullable |
| public static Iterable<String> getStrings(@NotNull Tree tree, @NotNull String propertyName) { |
| PropertyState property = tree.getProperty(propertyName); |
| if (property == null) { |
| return null; |
| } else { |
| return property.getValue(STRINGS); |
| } |
| } |
| |
| @Nullable |
| public static String getString(@NotNull Tree tree, @NotNull String propertyName) { |
| return getStringInternal(tree, propertyName, Type.STRING); |
| } |
| |
| @Nullable |
| public static String getString(@NotNull Tree tree, @NotNull String name, @Nullable String defaultValue) { |
| String str = getString(tree, name); |
| return (str != null) ? str : defaultValue; |
| } |
| |
| @Nullable |
| private static String getStringInternal(@NotNull Tree tree, |
| @NotNull String propertyName, |
| @NotNull Type<String> type) { |
| PropertyState property = tree.getProperty(propertyName); |
| if (property != null && !property.isArray()) { |
| return property.getValue(type); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the boolean representation of the property with the specified |
| * {@code propertyName}. If the property does not exist or |
| * {@link org.apache.jackrabbit.oak.api.PropertyState#isArray() is an array} |
| * this method returns {@code false}. |
| * |
| * @param tree The target tree. |
| * @param propertyName The name of the property. |
| * @return the boolean representation of the property state with the given |
| * name. This utility returns {@code false} if the property does not exist |
| * or is an multivalued property. |
| */ |
| public static boolean getBoolean(@NotNull Tree tree, @NotNull String propertyName) { |
| PropertyState property = tree.getProperty(propertyName); |
| return property != null && !property.isArray() && property.getValue(BOOLEAN); |
| } |
| |
| @Nullable |
| public static String getName(@NotNull Tree tree, @NotNull String name) { |
| PropertyState property = tree.getProperty(name); |
| if (property != null && property.getType() == NAME) { |
| return property.getValue(NAME); |
| } else { |
| return null; |
| } |
| } |
| |
| @NotNull |
| public static Iterable<String> getNames(@NotNull Tree tree, @NotNull String name) { |
| PropertyState property = tree.getProperty(name); |
| if (property != null && property.getType() == NAMES) { |
| return property.getValue(NAMES); |
| } else { |
| return emptyList(); |
| } |
| } |
| |
| public static long getLong(@NotNull Tree tree, @NotNull String name, long defaultValue) { |
| PropertyState property = tree.getProperty(name); |
| if (property != null && !property.isArray()) { |
| return property.getValue(LONG); |
| } else { |
| return defaultValue; |
| } |
| } |
| |
| /** |
| * Return the possibly non existing tree located at the passed {@code path} from |
| * the location of the start {@code tree} or {@code null} if {@code path} results |
| * in a parent of the root. |
| * |
| * @param tree start tree |
| * @param path path from the start tree |
| * @return tree located at {@code path} from {@code start} or {@code null} |
| */ |
| @Nullable |
| public static Tree getTree(@NotNull Tree tree, @NotNull String path) { |
| for (String element : PathUtils.elements(path)) { |
| if (PathUtils.denotesParent(element)) { |
| if (tree.isRoot()) { |
| return null; |
| } else { |
| tree = tree.getParent(); |
| } |
| } else if (!PathUtils.denotesCurrent(element)) { |
| tree = tree.getChild(element); |
| } // else . -> skip to next element |
| } |
| return tree; |
| } |
| |
| public static Tree addChild( |
| @NotNull Tree parent, @NotNull String name, |
| @Nullable String typeName, @NotNull Tree typeRoot, |
| @Nullable String userID) throws RepositoryException { |
| if (typeName == null) { |
| typeName = getDefaultChildType(typeRoot, parent, name); |
| if (typeName == null) { |
| String path = PathUtils.concat(parent.getPath(), name); |
| throw new ConstraintViolationException( |
| "No default node type available for " + path); |
| } |
| } |
| |
| Tree type = typeRoot.getChild(typeName); |
| if (!type.exists()) { |
| throw new NoSuchNodeTypeException( |
| "Node type " + typeName + " does not exist"); |
| } else if (getBoolean(type, JCR_IS_ABSTRACT) |
| // OAK-1013: backwards compatibility for abstract default types |
| && !typeName.equals(getDefaultChildType(typeRoot, parent, name))) { |
| throw new ConstraintViolationException( |
| "Node type " + typeName + " is abstract"); |
| } else if (getBoolean(type, JCR_ISMIXIN)) { |
| throw new ConstraintViolationException( |
| "Node type " + typeName + " is a mixin type"); |
| } |
| |
| Tree child = parent.addChild(name); |
| child.setProperty(JCR_PRIMARYTYPE, typeName, NAME); |
| if (getBoolean(type, JCR_HASORDERABLECHILDNODES)) { |
| child.setOrderableChildren(true); |
| } |
| autoCreateItems(child, type, typeRoot, userID); |
| return child; |
| } |
| |
| /** |
| * Adds a new child tree with the given name and primary type name. |
| * This method is a shortcut for calling {@link Tree#addChild(String)} and |
| * {@link Tree#setProperty(String, Object, org.apache.jackrabbit.oak.api.Type)} |
| * where the property name is {@link JcrConstants#JCR_PRIMARYTYPE}. |
| * Note, that this method in addition verifies if the created tree exists |
| * and is accessible in order to avoid {@link IllegalStateException} upon |
| * subsequent modification of the new child. |
| * |
| * @param childName The Oak name of the child item. |
| * @param primaryTypeName The Oak name of the primary node type. |
| * @return The new child tree with the specified name and primary type. |
| * @throws AccessDeniedException If the child does not exist after creation. |
| */ |
| @NotNull |
| public static Tree addChild(@NotNull Tree tree, @NotNull String childName, @NotNull String primaryTypeName) throws AccessDeniedException { |
| Tree child = tree.addChild(childName); |
| if (!child.exists()) { |
| throw new AccessDeniedException(); |
| } |
| child.setProperty(JcrConstants.JCR_PRIMARYTYPE, primaryTypeName, NAME); |
| return child; |
| } |
| |
| /** |
| * Combination of {@link Tree#getChild(String)} and adding a child including |
| * its jcr:primaryType property (i.e. {@link Tree#addChild(String)} and |
| * {@link Tree#setProperty(PropertyState)}) in case no tree exists with the specified name. |
| * |
| * @param childName The Oak name of the child item. |
| * @param primaryTypeName The Oak name of the primary node type. |
| * @return The new child node with the specified name and primary type. |
| * @throws AccessDeniedException If the child does not exist after creation. |
| */ |
| @NotNull |
| public static Tree getOrAddChild(@NotNull Tree tree, @NotNull String childName, @NotNull String primaryTypeName) throws AccessDeniedException { |
| Tree child = tree.getChild(childName); |
| return (child.exists()) ? child : addChild(tree, childName, primaryTypeName); |
| } |
| |
| public static void addMixin(@NotNull Tree tree, @NotNull String mixinName, @NotNull Tree typeRoot, @Nullable String userID) throws RepositoryException { |
| Tree type = typeRoot.getChild(mixinName); |
| if (!type.exists()) { |
| throw new NoSuchNodeTypeException( |
| "Node type " + mixinName + " does not exist"); |
| } else if (getBoolean(type, JCR_IS_ABSTRACT)) { |
| throw new ConstraintViolationException( |
| "Node type " + mixinName + " is abstract"); |
| } else if (!getBoolean(type, JCR_ISMIXIN)) { |
| throw new ConstraintViolationException( |
| "Node type " + mixinName + " is a not a mixin type"); |
| } |
| |
| List<String> mixins = Lists.newArrayList(); |
| String primary = getName(tree, JCR_PRIMARYTYPE); |
| if (primary != null |
| && Iterables.contains(getNames(type, NodeTypeConstants.REP_PRIMARY_SUBTYPES), primary)) { |
| return; |
| } |
| |
| Set<String> subMixins = Sets.newHashSet(getNames(type, NodeTypeConstants.REP_MIXIN_SUBTYPES)); |
| for (String mixin : getNames(tree, NodeTypeConstants.JCR_MIXINTYPES)) { |
| if (mixinName.equals(mixin) || subMixins.contains(mixin)) { |
| return; |
| } |
| mixins.add(mixin); |
| } |
| |
| mixins.add(mixinName); |
| tree.setProperty(JcrConstants.JCR_MIXINTYPES, mixins, NAMES); |
| |
| autoCreateItems(tree, type, typeRoot, userID); |
| } |
| |
| public static void autoCreateItems(@NotNull Tree tree, @NotNull Tree type, @NotNull Tree typeRoot, @Nullable String userID) |
| throws RepositoryException { |
| // TODO: use a separate rep:autoCreatePropertyDefinitions |
| Tree properties = type.getChild(REP_NAMED_PROPERTY_DEFINITIONS); |
| for (Tree definitions : properties.getChildren()) { |
| String name = definitions.getName(); |
| if (name.equals(NodeTypeConstants.REP_PRIMARY_TYPE) |
| || name.equals(NodeTypeConstants.REP_MIXIN_TYPES)) { |
| continue; |
| } else if (name.equals(NodeTypeConstants.REP_UUID)) { |
| name = JCR_UUID; |
| } |
| for (Tree definition : definitions.getChildren()) { |
| if (getBoolean(definition, JCR_AUTOCREATED)) { |
| if (!tree.hasProperty(name)) { |
| PropertyState property = |
| autoCreateProperty(name, definition, userID); |
| if (property != null) { |
| tree.setProperty(property); |
| } else { |
| throw new RepositoryException( |
| "Unable to auto-create value for " |
| + PathUtils.concat(tree.getPath(), name)); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| // TODO: use a separate rep:autoCreateChildNodeDefinitions |
| // Note that we use only named, non-SNS child node definitions |
| // as there can be no reasonable default values for residual or |
| // SNS child nodes |
| Tree childNodes = type.getChild(REP_NAMED_CHILD_NODE_DEFINITIONS); |
| for (Tree definitions : childNodes.getChildren()) { |
| String name = definitions.getName(); |
| for (Tree definition : definitions.getChildren()) { |
| if (getBoolean(definition, JCR_AUTOCREATED)) { |
| if (!tree.hasChild(name)) { |
| String typeName = |
| getName(definition, JCR_DEFAULTPRIMARYTYPE); |
| addChild(tree, name, typeName, typeRoot, userID); |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| public static PropertyState autoCreateProperty(@NotNull String name, |
| @NotNull Tree definition, |
| @Nullable String userID) { |
| if (JCR_UUID.equals(name)) { |
| String uuid = UUIDUtils.generateUUID(); |
| return PropertyStates.createProperty(name, uuid, STRING); |
| } else if (JCR_CREATED.equals(name)) { |
| String now = ISO8601.format(Calendar.getInstance()); |
| return PropertyStates.createProperty(name, now, DATE); |
| } else if (JCR_CREATEDBY.equals(name)) { |
| return PropertyStates.createProperty(name, Strings.nullToEmpty(userID), STRING); |
| } else if (JCR_LASTMODIFIED.equals(name)) { |
| String now = ISO8601.format(Calendar.getInstance()); |
| return PropertyStates.createProperty(name, now, DATE); |
| } else if (JCR_LASTMODIFIEDBY.equals(name)) { |
| return PropertyStates.createProperty(name, Strings.nullToEmpty(userID), STRING); |
| } |
| |
| // does the definition have a default value? |
| PropertyState values = definition.getProperty(JCR_DEFAULTVALUES); |
| if (values != null) { |
| Type<?> type = values.getType(); |
| if (getBoolean(definition, JCR_MULTIPLE)) { |
| return PropertyStates.createProperty( |
| name, values.getValue(type), type); |
| } else if (values.count() > 0) { |
| type = type.getBaseType(); |
| return PropertyStates.createProperty( |
| name, values.getValue(type, 0), type); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Finds the default primary type for a new child node with the given name. |
| * |
| * @param typeRoot root of the {@code /jcr:system/jcr:nodeTypes} tree |
| * @param parent parent node |
| * @param childName name of the new child node |
| * @return name of the default type, or {@code null} if not available |
| */ |
| public static String getDefaultChildType( |
| Tree typeRoot, Tree parent, String childName) { |
| String name = dropIndexFromName(childName); |
| boolean sns = !name.equals(childName); |
| List<Tree> types = getEffectiveType(parent, typeRoot); |
| |
| // first look for named node definitions |
| for (Tree type : types) { |
| Tree definitions = type |
| .getChild(REP_NAMED_CHILD_NODE_DEFINITIONS) |
| .getChild(name); |
| String defaultName = findDefaultPrimaryType(definitions, sns); |
| if (defaultName != null) { |
| return defaultName; |
| } |
| } |
| |
| // then check residual definitions |
| for (Tree type : types) { |
| Tree definitions = type |
| .getChild(REP_RESIDUAL_CHILD_NODE_DEFINITIONS); |
| String defaultName = findDefaultPrimaryType(definitions, sns); |
| if (defaultName != null) { |
| return defaultName; |
| } |
| } |
| |
| // no matching child node definition found |
| return null; |
| } |
| |
| /** |
| * Returns the effective node types of the given node. |
| */ |
| public static List<Tree> getEffectiveType(Tree tree, Tree typeRoot) { |
| List<Tree> types = newArrayList(); |
| |
| String primary = getName(tree, JCR_PRIMARYTYPE); |
| if (primary != null) { |
| Tree type = typeRoot.getChild(primary); |
| if (type.exists()) { |
| types.add(type); |
| } |
| } |
| |
| for (String mixin : getNames(tree, JCR_MIXINTYPES)) { |
| Tree type = typeRoot.getChild(mixin); |
| if (type.exists()) { |
| types.add(type); |
| } |
| } |
| |
| return types; |
| } |
| |
| public static String findDefaultPrimaryType(Tree definitions, boolean sns) { |
| for (Tree definition : definitions.getChildren()) { |
| String defaultName = getName(definition, JCR_DEFAULTPRIMARYTYPE); |
| if (defaultName != null |
| && (!sns || getBoolean(definition, JCR_SAMENAMESIBLINGS))) { |
| return defaultName; |
| } |
| } |
| return null; |
| } |
| |
| public static boolean isNodeType(Tree tree, String typeName, Tree typeRoot) { |
| String primaryName = TreeUtil.getName(tree, JCR_PRIMARYTYPE); |
| if (typeName.equals(primaryName)) { |
| return true; |
| } else if (primaryName != null) { |
| Tree type = typeRoot.getChild(primaryName); |
| if (contains(getNames(type, REP_SUPERTYPES), typeName)) { |
| return true; |
| } |
| } |
| |
| for (String mixinName : getNames(tree, JCR_MIXINTYPES)) { |
| if (typeName.equals(mixinName)) { |
| return true; |
| } else { |
| Tree type = typeRoot.getChild(mixinName); |
| if (contains(getNames(type, REP_SUPERTYPES), typeName)) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Returns {@code true} if the specified {@code tree} is a read-only tree.. |
| * |
| * @param tree The tree object to be tested. |
| * @return {@code true} if the specified tree is an immutable read-only tree. |
| * @see org.apache.jackrabbit.oak.plugins.tree.ReadOnly |
| */ |
| public static boolean isReadOnlyTree(@NotNull Tree tree) { |
| return tree instanceof ReadOnly; |
| } |
| } |