diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java index 6e158401f26..16461a4143c 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/util/Utils.java @@ -84,7 +84,7 @@ public class Utils { /** * The maximum size a node name, in bytes. This is only a problem for long path. */ - private static final int NODE_NAME_LIMIT = Integer.getInteger("oak.nodeNameLimit", 150); + public static final int NODE_NAME_LIMIT = Integer.getInteger("oak.nodeNameLimit", 150); private static final Charset UTF_8 = Charset.forName("UTF-8"); diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/NameFilteringNodeState.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/NameFilteringNodeState.java new file mode 100644 index 00000000000..9bf377c9fc2 --- /dev/null +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/NameFilteringNodeState.java @@ -0,0 +1,233 @@ +/* + * 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.base.Preconditions.checkNotNull; +import static org.apache.jackrabbit.oak.plugins.tree.impl.TreeConstants.OAK_CHILD_ORDER; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.commons.io.Charsets; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.document.util.Utils; +import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; +import org.apache.jackrabbit.oak.plugins.memory.PropertyStates; +import org.apache.jackrabbit.oak.spi.state.AbstractNodeState; +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 com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + +/** + * This is a node state wrapper that filters out all children nodes with names + * longer than supported by the DocumentNodeStore. Returned children are wrapped + * as well. + */ +class NameFilteringNodeState extends AbstractNodeState { + + /** + * Max character size in bytes in UTF8 = 4. Therefore if the number of characters is smaller + * than NODE_NAME_LIMIT / 4 we don't need to count bytes. + */ + private static final int SAFE_NODE_NAME_LENGTH = Utils.NODE_NAME_LIMIT / 4; + + private final NodeState delegate; + + private NameFilteringNodeState(@Nonnull NodeState delegate) { + this.delegate = checkNotNull(delegate); + } + + @Nonnull + public static NodeState wrap(@Nonnull NodeState nodeState) { + return new NameFilteringNodeState(checkNotNull(nodeState)); + } + + @Override + public boolean exists() { + return delegate.exists(); + } + + @Override + public boolean hasProperty(String name) { + return delegate.hasProperty(name); + } + + @Override + public boolean getBoolean(String name) { + return delegate.getBoolean(name); + } + + @Override + public long getLong(String name) { + return delegate.getLong(name); + } + + @Override + public String getString(String name) { + return delegate.getString(name); + } + + @Override + public Iterable getStrings(String name) { + return delegate.getStrings(name); + } + + @Override + public String getName(String name) { + return delegate.getName(name); + } + + @Override + public Iterable getNames(String name) { + return delegate.getNames(name); + } + + @Override + public long getPropertyCount() { + return delegate.getPropertyCount(); + } + + @Override + @Nonnull + public Iterable getProperties() { + return Iterables.transform(delegate.getProperties(), new Function() { + @Nullable + @Override + public PropertyState apply(@Nullable final PropertyState propertyState) { + return fixChildOrderPropertyState(propertyState); + } + }); + } + + @Override + public PropertyState getProperty(String name) { + return fixChildOrderPropertyState(delegate.getProperty(name)); + } + + @Override + public boolean hasChildNode(String name) { + if (isNameTooLong(name)) { + return false; + } else { + return delegate.hasChildNode(name); + } + } + + @Override + public NodeState getChildNode(String name) throws IllegalArgumentException { + if (isNameTooLong(name)) { + return EmptyNodeState.MISSING_NODE; + } else { + return wrap(delegate.getChildNode(name)); + } + } + + @Override + public Iterable getChildNodeEntries() { + final Iterable transformed = Iterables.transform(delegate.getChildNodeEntries(), + new Function() { + @Nullable + @Override + public ChildNodeEntry apply(@Nullable final ChildNodeEntry childNodeEntry) { + if (childNodeEntry != null) { + final String name = childNodeEntry.getName(); + final NodeState nodeState = childNodeEntry.getNodeState(); + if (isNameTooLong(name)) { + return null; + } else { + return new MemoryChildNodeEntry(name, wrap(nodeState)); + } + } + return null; + } + }); + return Iterables.filter(transformed, new Predicate() { + @Override + public boolean apply(@Nullable final ChildNodeEntry childNodeEntry) { + return childNodeEntry != null; + } + }); + + } + + @Override + public NodeBuilder builder() { + return new MemoryNodeBuilder(this); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof NameFilteringNodeState) { + return delegate.equals(((NameFilteringNodeState) obj).delegate); + } else if (obj instanceof NodeState){ + for (String name : ((NodeState) obj).getChildNodeNames()) { + if (isNameTooLong(name)) { + return false; + } + } + return delegate.equals(obj); + } + return false; + } + + /** + * This method checks whether the name is no longer than the maximum node + * name length supported by the DocumentNodeStore. + * + * @param name + * to check + * @return true if the name is longer than {@link Utils#NODE_NAME_LIMIT} + */ + private static boolean isNameTooLong(@Nonnull String name) { + return name.length() > SAFE_NODE_NAME_LENGTH && name.getBytes(Charsets.UTF_8).length > Utils.NODE_NAME_LIMIT; + } + + /** + * Utility method to fix the PropertyState of properties called + * {@code :childOrder}. + * + * @param propertyState + * A property-state. + * @return The original property-state or if the property name is + * {@code :childOrder}, a property-state with hidden child names + * removed from the value. + */ + @CheckForNull + private PropertyState fixChildOrderPropertyState(@Nullable final PropertyState propertyState) { + if (propertyState != null && OAK_CHILD_ORDER.equals(propertyState.getName())) { + final Iterable values = Iterables.filter(propertyState.getValue(Type.NAMES), + new Predicate() { + @Override + public boolean apply(@Nullable final String name) { + return !(name == null || isNameTooLong(name)); + } + }); + return PropertyStates.createProperty(OAK_CHILD_ORDER, values, Type.NAMES); + } + return propertyState; + } +} diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositorySidegrade.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositorySidegrade.java index 27729c9ad28..6cb6c6a8600 100755 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositorySidegrade.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositorySidegrade.java @@ -79,6 +79,8 @@ public class RepositorySidegrade { */ private Set mergePaths = DEFAULT_MERGE_PATHS; + private boolean skipLongNames = true; + private List customCommitHooks = null; VersionCopyConfiguration versionCopyConfiguration = new VersionCopyConfiguration(); @@ -182,6 +184,14 @@ public void setMerges(@Nonnull String... merges) { this.mergePaths = copyOf(checkNotNull(merges)); } + public boolean isSkipLongNames() { + return skipLongNames; + } + + public void setSkipLongNames(boolean skipLongNames) { + this.skipLongNames = skipLongNames; + } + /** * Same as {@link #copy(RepositoryInitializer)}, but with no custom initializer. */ @@ -205,7 +215,6 @@ public void copy() throws RepositoryException { */ public void copy(RepositoryInitializer initializer) throws RepositoryException { try { - NodeState sourceRoot = source.getRoot(); NodeBuilder targetRoot = target.getRoot().builder(); new InitialContent().initialize(targetRoot); @@ -213,10 +222,14 @@ public void copy(RepositoryInitializer initializer) throws RepositoryException { initializer.initialize(targetRoot); } - copyState( - ReportingNodeState.wrap(sourceRoot, new LoggingReporter(LOG, "Copying", 10000, -1)), - targetRoot - ); + final NodeState reportingSourceRoot = ReportingNodeState.wrap(source.getRoot(), new LoggingReporter(LOG, "Copying", 10000, -1)); + final NodeState sourceRoot; + if (skipLongNames) { + sourceRoot = NameFilteringNodeState.wrap(reportingSourceRoot); + } else { + sourceRoot = reportingSourceRoot; + } + copyState(sourceRoot, targetRoot); } catch (Exception e) { throw new RepositoryException("Failed to copy content", e); 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 1037c016261..b0271a7856a 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 @@ -175,6 +175,8 @@ public class RepositoryUpgrade { private List customCommitHooks = null; + private boolean skipLongNames = true; + VersionCopyConfiguration versionCopyConfiguration = new VersionCopyConfiguration(); /** @@ -245,6 +247,14 @@ public void setEarlyShutdown(boolean earlyShutdown) { this.earlyShutdown = earlyShutdown; } + public boolean isSkipLongNames() { + return skipLongNames; + } + + public void setSkipLongNames(boolean skipLongNames) { + this.skipLongNames = skipLongNames; + } + /** * Returns the list of custom CommitHooks to be applied before the final * type validation, reference and indexing hooks. @@ -407,13 +417,19 @@ protected Root getWriteRoot() { new TypeEditorProvider(false).getRootEditor( targetBuilder.getBaseState(), targetBuilder.getNodeState(), targetBuilder, null); - final NodeState sourceRoot = ReportingNodeState.wrap( + final NodeState reportingSourceRoot = ReportingNodeState.wrap( JackrabbitNodeState.createRootNodeState( source, workspaceName, targetBuilder.getNodeState(), uriToPrefix, copyBinariesByReference, skipOnError ), new LoggingReporter(logger, "Migrating", 10000, -1) ); + final NodeState sourceRoot; + if (skipLongNames) { + sourceRoot = NameFilteringNodeState.wrap(reportingSourceRoot); + } else { + sourceRoot = reportingSourceRoot; + } final Stopwatch watch = Stopwatch.createStarted(); diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java index 253070a800f..9c5f52a5807 100644 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java @@ -16,7 +16,6 @@ */ package org.apache.jackrabbit.oak.upgrade.nodestate; -import com.google.common.base.Charsets; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.commons.PathUtils; @@ -33,6 +32,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; + import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; @@ -187,12 +187,6 @@ private static boolean copyNodeState(@Nonnull final NodeState source, @Nonnull f for (ChildNodeEntry child : source.getChildNodeEntries()) { final String childName = child.getName(); - // OAK-1589: maximum supported length of name for DocumentNodeStore - // is 150 bytes. Skip the sub tree if the the name is too long - if (childName.length() > 37 && childName.getBytes(Charsets.UTF_8).length > 150) { - LOG.warn("Node name too long. Skipping {}", source); - continue; - } final NodeState childSource = child.getNodeState(); if (!target.hasChildNode(childName)) { // add new children diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/LongNameTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/LongNameTest.java new file mode 100644 index 00000000000..05a2921b429 --- /dev/null +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/LongNameTest.java @@ -0,0 +1,172 @@ +/* + * 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.cycle; +import static com.google.common.collect.Iterables.limit; + +import java.io.File; +import java.io.IOException; + +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.segment.SegmentNodeStore; +import org.apache.jackrabbit.oak.plugins.segment.SegmentStore; +import org.apache.jackrabbit.oak.plugins.segment.memory.MemoryStore; +import org.apache.jackrabbit.oak.plugins.document.DocumentMK; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.stats.Clock; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class LongNameTest { + + public static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray()); + + private static final String TOO_LONG_NAME = "this string is an example of a very long node name which is approximately one hundred fifty eight bytes long so too long for the document node store to handle"; + + private static final String NOT_TOO_LONG_NAME = "this string despite it is very long as well is not too long for the document node store to handle so it may be migrated succesfully without troubles"; + + private static RepositoryConfig sourceRepositoryConfig; + + private static File crx2RepoDir; + + @BeforeClass + public static void prepareSourceRepository() throws RepositoryException, IOException, InterruptedException { + crx2RepoDir = new File("target", "upgrade-" + Clock.SIMPLE.getTimeIncreasing()); + FileUtils.deleteQuietly(crx2RepoDir); + + sourceRepositoryConfig = createCrx2Config(crx2RepoDir); + RepositoryImpl sourceRepository = RepositoryImpl.create(sourceRepositoryConfig); + Session session = sourceRepository.login(CREDENTIALS); + try { + Assert.assertTrue(TOO_LONG_NAME.getBytes().length > 150); + Assert.assertTrue(NOT_TOO_LONG_NAME.getBytes().length < 150); + + Node longNameParent = createParent(session.getRootNode()); + Assert.assertTrue(longNameParent.getPath().length() >= 350); + + longNameParent.addNode(TOO_LONG_NAME); + longNameParent.addNode(NOT_TOO_LONG_NAME); + session.save(); + + Assert.assertTrue(longNameParent.hasNode(TOO_LONG_NAME)); + Assert.assertTrue(longNameParent.hasNode(NOT_TOO_LONG_NAME)); + } finally { + session.logout(); + } + sourceRepository.shutdown(); + } + + private static RepositoryConfig createCrx2Config(File crx2RepoDir) throws RepositoryException, IOException { + File source = new File(crx2RepoDir, "source"); + source.mkdirs(); + return RepositoryConfig.install(source); + } + + @Test + public void longNameShouldBeSkipped() throws RepositoryException, IOException { + DocumentNodeStore nodeStore = new DocumentMK.Builder().getNodeStore(); + try { + upgrade(nodeStore, true); + + NodeState parent = getParent(nodeStore.getRoot()); + Assert.assertTrue(parent.hasChildNode(NOT_TOO_LONG_NAME)); + Assert.assertEquals(1, parent.getChildNodeCount(10)); + + // The following throws an DocumentStoreException: + // Assert.assertFalse(parent.hasChildNode(TOO_LONG_NAME)); + } finally { + nodeStore.dispose(); + } + } + + @Test(expected = RepositoryException.class) + @Ignore + public void longNameOnDocumentStoreThrowsAnException() throws RepositoryException, IOException { + DocumentNodeStore nodeStore = new DocumentMK.Builder().getNodeStore(); + try { + upgrade(nodeStore, false); + } finally { + nodeStore.dispose(); + } + } + + @Test + @Ignore + public void longNameOnSegmentStoreWorksFine() throws RepositoryException, IOException { + SegmentStore memoryStore = new MemoryStore(); + try { + SegmentNodeStore nodeStore = SegmentNodeStore.newSegmentNodeStore(memoryStore).create(); + upgrade(nodeStore, false); + + NodeState parent = getParent(nodeStore.getRoot()); + Assert.assertTrue(parent.hasChildNode(NOT_TOO_LONG_NAME)); + Assert.assertTrue(parent.hasChildNode(TOO_LONG_NAME)); + } finally { + memoryStore.close(); + } + } + + private static void upgrade(NodeStore target, boolean skipLongNames) throws RepositoryException, IOException { + RepositoryConfig config = createCrx2Config(crx2RepoDir); + RepositoryContext context = RepositoryContext.create(config); + try { + RepositoryUpgrade upgrade = new RepositoryUpgrade(context, target); + upgrade.setSkipLongNames(skipLongNames); + upgrade.copy(null); + } finally { + context.getRepository().shutdown(); + } + } + + private static Node createParent(Node root) throws RepositoryException { + Node current = root; + for (String segment : getParentSegments()) { + current = current.addNode(segment); + } + return current; + } + + private static NodeState getParent(NodeState root) throws RepositoryException { + NodeState current = root; + for (String segment : getParentSegments()) { + current = current.getChildNode(segment); + } + return current; + } + + private static Iterable getParentSegments() { + return limit(cycle("this", "is", "a", "path"), 100); // total path + // length + // = 350 + } +}