diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java index 1b1f46e..a0dd00d 100644 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java @@ -465,7 +465,8 @@ public class RepositoryUpgrade { new RestrictionEditorProvider(), new GroupEditorProvider(groupsPath), // copy referenced version histories - new VersionableEditor.Provider(sourceRoot, workspaceName, versionCopyConfiguration) + new VersionableEditor.Provider(sourceRoot, workspaceName, versionCopyConfiguration), + new SameNameSiblingsEditor.Provider() ))); // security-related hooks diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/SameNameSiblingsEditor.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/SameNameSiblingsEditor.java new file mode 100644 index 0000000..99c000f --- /dev/null +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/SameNameSiblingsEditor.java @@ -0,0 +1,299 @@ +/* + * 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.upgrade; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static org.apache.jackrabbit.JcrConstants.JCR_SAMENAMESIBLINGS; +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_NODE_TYPES; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_NAMED_CHILD_NODE_DEFINITIONS; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_RESIDUAL_CHILD_NODE_DEFINITIONS; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate; +import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.commit.DefaultEditor; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.commit.EditorProvider; +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; + +/** + * This editor check if same name sibling nodes are allowed under a given + * parent. If they are not, they will be renamed by replacing brackets with a + * underscore: {@code sns_name[3] -> sns_name_3_}. + */ +public class SameNameSiblingsEditor extends DefaultEditor { + + private static final Logger logger = LoggerFactory.getLogger(SameNameSiblingsEditor.class); + + private static final Pattern SNS_REGEX = Pattern.compile("^(.+)\\[(\\d+)\\]$"); + + private static final Predicate NO_SNS_PROPERTY = new Predicate() { + @Override + public boolean apply(NodeState input) { + return !input.getBoolean(JCR_SAMENAMESIBLINGS); + } + }; + + /** + * List of node type definitions that doesn't allow to have SNS children. + */ + private final List childrenDefsWithoutSns; + + /** + * Builder of the current node. + */ + private final NodeBuilder builder; + + /** + * Path to the current node. + */ + private final String path; + + public static class Provider implements EditorProvider { + @Override + public Editor getRootEditor(NodeState before, NodeState after, NodeBuilder builder, CommitInfo info) + throws CommitFailedException { + return new SameNameSiblingsEditor(builder); + } + } + + public SameNameSiblingsEditor(NodeBuilder rootBuilder) { + this.childrenDefsWithoutSns = prepareChildDefsWithoutSns(rootBuilder.getNodeState()); + this.builder = rootBuilder; + this.path = ""; + } + + public SameNameSiblingsEditor(SameNameSiblingsEditor parent, String name, NodeBuilder builder) { + this.childrenDefsWithoutSns = parent.childrenDefsWithoutSns; + this.builder = builder; + this.path = new StringBuilder(parent.path).append('/').append(name).toString(); + } + + @Override + public Editor childNodeAdded(String name, NodeState after) throws CommitFailedException { + return new SameNameSiblingsEditor(this, name, builder.getChildNode(name)); + } + + @Override + public Editor childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + return new SameNameSiblingsEditor(this, name, builder.getChildNode(name)); + } + + @Override + public void leave(NodeState before, NodeState after) throws CommitFailedException { + if (hasSameNamedChildren(after)) { + renameSameNamedChildren(builder); + } + } + + /** + * Prepare a list of node definitions that doesn't allow having SNS children. + * + * @param root Repository root + * @return a list of node definitions denying SNS children + */ + private static List prepareChildDefsWithoutSns(NodeState root) { + List defs = new ArrayList(); + NodeState types = root.getChildNode(JCR_SYSTEM).getChildNode(JCR_NODE_TYPES); + for (ChildNodeEntry typeEntry : types.getChildNodeEntries()) { + NodeState type = typeEntry.getNodeState(); + TypePredicate typePredicate = new TypePredicate(root, typeEntry.getName()); + defs.addAll(parseResidualChildNodeDefs(root, type, typePredicate)); + defs.addAll(parseNamedChildNodeDefs(root, type, typePredicate)); + } + return defs; + } + + private static List parseNamedChildNodeDefs(NodeState root, NodeState parentType, + TypePredicate parentTypePredicate) { + List defs = new ArrayList(); + NodeState namedChildNodeDefinitions = parentType.getChildNode(REP_NAMED_CHILD_NODE_DEFINITIONS); + for (ChildNodeEntry childName : namedChildNodeDefinitions.getChildNodeEntries()) { + for (String childType : filterChildren(childName.getNodeState(), NO_SNS_PROPERTY)) { + TypePredicate childTypePredicate = new TypePredicate(root, childType); + defs.add(new ChildTypeDef(parentTypePredicate, childName.getName(), childTypePredicate)); + } + } + return defs; + } + + private static List parseResidualChildNodeDefs(NodeState root, NodeState parentType, + TypePredicate parentTypePredicate) { + List defs = new ArrayList(); + NodeState resChildNodeDefinitions = parentType.getChildNode(REP_RESIDUAL_CHILD_NODE_DEFINITIONS); + for (String childType : filterChildren(resChildNodeDefinitions, NO_SNS_PROPERTY)) { + TypePredicate childTypePredicate = new TypePredicate(root, childType); + defs.add(new ChildTypeDef(parentTypePredicate, childTypePredicate)); + } + return defs; + } + + /** + * Filter children of the given node using predicate and return the list of matching child names. + * + * @param parent + * @param predicate + * @return a list of names of children accepting the predicate + */ + private static Iterable filterChildren(NodeState parent, final Predicate predicate) { + return transform(filter(parent.getChildNodeEntries(), new Predicate() { + @Override + public boolean apply(ChildNodeEntry input) { + return predicate.apply(input.getNodeState()); + } + }), new Function() { + @Override + public String apply(ChildNodeEntry input) { + return input.getName(); + } + }); + } + + /** + * Check if there are SNS nodes under the given parent. + * + * @param parent + * @return {@code true} if there are SNS children + */ + private boolean hasSameNamedChildren(NodeState parent) { + for (String name : parent.getChildNodeNames()) { + if (SNS_REGEX.matcher(name).matches()) { + return true; + } + } + return false; + } + + /** + * Rename all SNS children which are not allowed under the given parent. + */ + private void renameSameNamedChildren(NodeBuilder parent) { + NodeState parentNode = parent.getNodeState(); + Map toBeRenamed = new HashMap(); + for (String name : parent.getChildNodeNames()) { + Matcher m = SNS_REGEX.matcher(name); + if (!m.matches()) { + continue; + } else if (isSnsAllowedForChild(parentNode, name)) { + continue; + } + String prefix = m.group(1); + String index = m.group(2); + toBeRenamed.put(name, createNewName(parentNode, prefix, index)); + } + for (Entry e : toBeRenamed.entrySet()) { + logger.warn("Renaming SNS {}/{} to {}", path, e.getKey(), e.getValue()); + parent.getChildNode(e.getKey()).moveTo(parent, e.getValue()); + } + } + + /** + * Check if SNS with given name is allowed under the given parent using the {@link #childrenDefsWithoutSns} list. + */ + private boolean isSnsAllowedForChild(NodeState parent, String name) { + for (ChildTypeDef snsDef : childrenDefsWithoutSns) { + if (snsDef.applies(parent, name)) { + return false; + } + } + return true; + } + + /** + * Create new name for the conflicting SNS node. This method makes sure that + * no node with this name already exists. + * + * @param prefix prefix of the new name, eg. my_name[3] + * @param index SNS index, eg. my_name[3] + * @param parent of the SNS node + * @return new and unused name for the node + */ + private String createNewName(NodeState parent, String prefix, String index) { + String newName; + int i = 1; + do { + if (i == 1) { + newName = String.format("%s_%s_", prefix, index); + } else { + newName = String.format("%s_%s_%d", prefix, index, i); + } + i++; + } while (parent.getChildNode(newName).exists()); + return newName; + } + + /** + * Definition of a children type. It contains the parent type, the child + * type and an optional child name. + */ + private static class ChildTypeDef { + + private final TypePredicate parentType; + + private final String childNameConstraint; + + private final TypePredicate childType; + + public ChildTypeDef(TypePredicate parentType, String childName, TypePredicate childType) { + this.parentType = parentType; + this.childNameConstraint = childName; + this.childType = childType; + } + + public ChildTypeDef(TypePredicate parentType, TypePredicate childType) { + this(parentType, null, childType); + } + + public boolean applies(NodeState parent, String childName) { + boolean result = true; + result &= parentType.apply(parent); + result &= childNameConstraint == null || childName.startsWith(this.childNameConstraint + '['); + result &= childType.apply(parent.getChildNode(childName)); + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(parentType.toString()).append(" > "); + if (childNameConstraint == null) { + result.append("*"); + } else { + result.append(childNameConstraint); + } + result.append(childType.toString()); + return result.toString(); + } + } +} diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/SameNodeSiblingsTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/SameNodeSiblingsTest.java new file mode 100644 index 0000000..feeed2f --- /dev/null +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/SameNodeSiblingsTest.java @@ -0,0 +1,167 @@ +/* + * 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.upgrade; + +import static com.google.common.collect.ImmutableSet.of; +import static com.google.common.collect.Sets.newHashSet; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore; +import org.apache.jackrabbit.oak.plugins.document.DocumentMK; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.io.Files; + +public class SameNodeSiblingsTest { + + public static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray()); + + private File crx2RepoDir; + + @Before + public void createCrx2RepoDir() throws IOException { + crx2RepoDir = Files.createTempDir(); + } + + @After + public void deleteCrx2RepoDir() { + FileUtils.deleteQuietly(crx2RepoDir); + } + + @Test + public void snsShouldBeRenamed() throws RepositoryException, IOException { + DocumentNodeStore nodeStore = migrate(new SourceDataCreator() { + @Override + public void create(Session session) throws RepositoryException { + Node parent = session.getRootNode().addNode("parent"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("something_else", "nt:folder"); + session.save(); + + parent.setPrimaryType("nt:folder"); // change parent type to + // something that doesn't + // allow SNS + session.save(); + } + }); + try { + NodeState parent = nodeStore.getRoot().getChildNode("parent"); + Set children = newHashSet(parent.getChildNodeNames()); + assertEquals(of("child", "child_2_", "child_3_", "something_else"), children); + } finally { + nodeStore.dispose(); + } + } + + @Test + public void snsShouldntBeRenamed() throws RepositoryException, IOException { + DocumentNodeStore nodeStore = migrate(new SourceDataCreator() { + @Override + public void create(Session session) throws RepositoryException { + Node parent = session.getRootNode().addNode("parent"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("something_else", "nt:folder"); + session.save(); + } + }); + try { + NodeState parent = nodeStore.getRoot().getChildNode("parent"); + Set children = newHashSet(parent.getChildNodeNames()); + assertEquals(of("child", "child[2]", "child[3]", "something_else"), children); + } finally { + nodeStore.dispose(); + } + } + + @Test + public void snsNewNameAlreadyExists() throws RepositoryException, IOException { + DocumentNodeStore nodeStore = migrate(new SourceDataCreator() { + @Override + public void create(Session session) throws RepositoryException { + Node parent = session.getRootNode().addNode("parent"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("child", "nt:folder"); + parent.addNode("child_2_", "nt:folder"); + parent.addNode("child_3_", "nt:folder"); + parent.addNode("child_3_2", "nt:folder"); + session.save(); + + parent.setPrimaryType("nt:folder"); + session.save(); + } + }); + try { + NodeState parent = nodeStore.getRoot().getChildNode("parent"); + Set children = newHashSet(parent.getChildNodeNames()); + assertEquals(of("child", "child_2_", "child_3_", "child_2_2", "child_3_2", "child_3_3"), children); + } finally { + nodeStore.dispose(); + } + } + + private DocumentNodeStore migrate(SourceDataCreator sourceDataCreator) throws RepositoryException, IOException { + RepositoryConfig config = RepositoryConfig.install(crx2RepoDir); + RepositoryImpl repository = RepositoryImpl.create(config); + + try { + Session session = repository.login(CREDENTIALS); + sourceDataCreator.create(session); + session.logout(); + } finally { + repository.shutdown(); + } + + config = RepositoryConfig.install(crx2RepoDir); // re-create the config + RepositoryContext context = RepositoryContext.create(config); + DocumentNodeStore target = new DocumentMK.Builder().getNodeStore(); + try { + RepositoryUpgrade upgrade = new RepositoryUpgrade(context, target); + upgrade.copy(null); + } finally { + context.getRepository().shutdown(); + } + return target; + } + + private static interface SourceDataCreator { + void create(Session session) throws RepositoryException; + } +}