diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java index c9ef4db..23731dc 100644 --- oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java @@ -14,54 +14,60 @@ * 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.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.addAll; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayListWithCapacity; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newLinkedHashMap; import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newLinkedHashSet; import static org.apache.jackrabbit.JcrConstants.JCR_FROZENMIXINTYPES; import static org.apache.jackrabbit.JcrConstants.JCR_FROZENPRIMARYTYPE; import static org.apache.jackrabbit.JcrConstants.JCR_FROZENUUID; import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; import static org.apache.jackrabbit.JcrConstants.JCR_UUID; -import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONHISTORY; import static org.apache.jackrabbit.JcrConstants.MIX_REFERENCEABLE; import static org.apache.jackrabbit.JcrConstants.MIX_VERSIONABLE; import static org.apache.jackrabbit.JcrConstants.NT_BASE; import static org.apache.jackrabbit.JcrConstants.NT_FROZENNODE; import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; import static org.apache.jackrabbit.JcrConstants.NT_VERSIONHISTORY; +import static org.apache.jackrabbit.core.RepositoryImpl.ACTIVITIES_NODE_ID; +import static org.apache.jackrabbit.core.RepositoryImpl.ROOT_NODE_ID; +import static org.apache.jackrabbit.core.RepositoryImpl.VERSION_STORAGE_NODE_ID; 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.plugins.tree.impl.TreeConstants.OAK_CHILD_ORDER; -import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.MIX_REP_VERSIONABLE_PATHS; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.jcr.Binary; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.apache.jackrabbit.api.ReferenceBinary; +import org.apache.jackrabbit.core.RepositoryContext; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.persistence.PersistenceManager; import org.apache.jackrabbit.core.persistence.util.NodePropBundle; import org.apache.jackrabbit.core.persistence.util.NodePropBundle.ChildNodeEntry; import org.apache.jackrabbit.core.persistence.util.NodePropBundle.PropertyEntry; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.core.value.InternalValue; @@ -93,27 +99,31 @@ class JackrabbitNodeState extends AbstractNodeState { private static void logNewNode(JackrabbitNodeState state) { count++; if (count % 10000 == 0) { log.info("Migrating node #" + count + ": " + state.getPath()); } } - private final JackrabbitNodeState parent; + private JackrabbitNodeState parent; private final String name; private String path; /** * Bundle loader based on the source persistence manager. */ private final BundleLoader loader; private final BinaryReferenceLoader binaryReferenceLoader; + /** + * Workspace name used for versionable paths. This is null + * for the jcr:versionStorage and jcr:activities nodes. + */ private final String workspaceName; private final TypePredicate isReferenceable; private final TypePredicate isOrderable; private final TypePredicate isVersionable; @@ -123,64 +133,112 @@ class JackrabbitNodeState extends AbstractNodeState { private final TypePredicate isFrozenNode; /** * Source namespace mappings (URI -< prefix). */ private final Map uriToPrefix; - private final Map versionablePaths; - private final boolean useBinaryReferences; private final Map nodes; private final Map properties; + private final Map mountPoints; + + private final Map nodeStateCache; + + private final List ignoredPaths = ImmutableList.of("/jcr:system/jcr:nodeTypes"); + + public static JackrabbitNodeState createRootNodeState( + RepositoryContext context, + String workspaceName, + NodeState root, + Map uriToPrefix, + boolean copyBinariesByReference + ) throws RepositoryException { + + final Map emptyMountPoints = ImmutableMap.of(); + final PersistenceManager versionPM = context.getInternalVersionManager().getPersistenceManager(); + final JackrabbitNodeState versionStorage = new JackrabbitNodeState( + versionPM, root, uriToPrefix, + VERSION_STORAGE_NODE_ID, "/jcr:system/jcr:versionStorage", + null, + emptyMountPoints, + copyBinariesByReference); + + final JackrabbitNodeState activities = new JackrabbitNodeState( + versionPM, root, uriToPrefix, + ACTIVITIES_NODE_ID, "/jcr:system/jcr:activities", + null, + emptyMountPoints, + copyBinariesByReference + ); + + + PersistenceManager pm = context.getWorkspaceInfo(workspaceName).getPersistenceManager(); + final Map mountPoints = ImmutableMap.of( + VERSION_STORAGE_NODE_ID, versionStorage, + ACTIVITIES_NODE_ID, activities + ); + return new JackrabbitNodeState( + pm, root, uriToPrefix, ROOT_NODE_ID, "/", + workspaceName, mountPoints, copyBinariesByReference); + } + private JackrabbitNodeState( JackrabbitNodeState parent, String name, NodePropBundle bundle) { this.parent = parent; this.name = name; this.path = null; this.loader = parent.loader; this.binaryReferenceLoader = parent.binaryReferenceLoader; this.workspaceName = parent.workspaceName; this.isReferenceable = parent.isReferenceable; this.isOrderable = parent.isOrderable; this.isVersionable = parent.isVersionable; this.isVersionHistory = parent.isVersionHistory; this.isFrozenNode = parent.isFrozenNode; this.uriToPrefix = parent.uriToPrefix; - this.versionablePaths = parent.versionablePaths; this.useBinaryReferences = parent.useBinaryReferences; this.properties = createProperties(bundle); this.nodes = createNodes(bundle); + this.mountPoints = parent.mountPoints; + this.nodeStateCache = parent.nodeStateCache; setChildOrder(); - setVersionablePaths(); fixFrozenUuid(); logNewNode(this); } JackrabbitNodeState( PersistenceManager source, NodeState root, Map uriToPrefix, NodeId id, String path, - String workspaceName, Map versionablePaths, + String workspaceName, + Map mountPoints, boolean useBinaryReferences) { this.parent = null; - this.name = null; + this.name = PathUtils.getName(path); this.path = path; this.loader = new BundleLoader(source); this.binaryReferenceLoader = BinaryReferenceLoader.INSTANCE; this.workspaceName = workspaceName; this.isReferenceable = new TypePredicate(root, MIX_REFERENCEABLE); this.isOrderable = TypePredicate.isOrderable(root); this.isVersionable = new TypePredicate(root, MIX_VERSIONABLE); this.isVersionHistory = new TypePredicate(root, NT_VERSIONHISTORY); this.isFrozenNode = new TypePredicate(root, NT_FROZENNODE); this.uriToPrefix = uriToPrefix; - this.versionablePaths = versionablePaths; + this.mountPoints = mountPoints; + final int cacheSize = 50; // cache size 50 results in > 25% cache hits during version copy + this.nodeStateCache = new LinkedHashMap(cacheSize, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } + }; this.useBinaryReferences = useBinaryReferences; try { NodePropBundle bundle = loader.loadBundle(id); this.properties = createProperties(bundle); this.nodes = createNodes(bundle); setChildOrder(); } catch (ItemStateException e) { @@ -233,21 +291,15 @@ class JackrabbitNodeState extends AbstractNodeState { } @Nonnull @Override public NodeState getChildNode(@Nonnull String name) { NodeId id = nodes.get(name); if (id != null) { - try { - return new JackrabbitNodeState( - this, name, loader.loadBundle(id)); - } catch (ItemStateException e) { - throw new IllegalStateException( - "Unable to access child node " + name, e); - } + return createNodeState(id, name); } checkValidName(name); return EmptyNodeState.MISSING_NODE; } @Override public Iterable getChildNodeNames() { @@ -255,72 +307,71 @@ class JackrabbitNodeState extends AbstractNodeState { } @Nonnull @Override public Iterable getChildNodeEntries() { List entries = newArrayList(); for (Map.Entry entry : nodes.entrySet()) { - String name = entry.getKey(); - try { - JackrabbitNodeState child = new JackrabbitNodeState( - this, name, loader.loadBundle(entry.getValue())); - entries.add(new MemoryChildNodeEntry(name, child)); - } catch (ItemStateException e) { - warn("Skipping broken child node entry " + name, e); - } + final String name = entry.getKey(); + NodeState child = createNodeState(entry.getValue(), name); + entries.add(new MemoryChildNodeEntry(name, child)); } return entries; } @Nonnull @Override public NodeBuilder builder() { return new MemoryNodeBuilder(this); } //-----------------------------------------------------------< private >-- + private JackrabbitNodeState createNodeState(NodeId id, String name) { + if (mountPoints.containsKey(id)) { + final JackrabbitNodeState nodeState = mountPoints.get(id); + checkState(name.equals(nodeState.name), + "Expected mounted node " + id + " to be called " + nodeState.name + + " instead of " + name); + nodeState.parent = this; + return nodeState; + } + try { + JackrabbitNodeState state = nodeStateCache.get(id); + if (state == null) { + state = new JackrabbitNodeState( + this, name, loader.loadBundle(id)); + nodeStateCache.put(id, state); + } + return state; + } catch (ItemStateException e) { + throw new IllegalStateException( + "Unable to access child node " + name, e); + } + } + private void setChildOrder() { if (isOrderable.apply(this)) { properties.put(OAK_CHILD_ORDER, PropertyStates.createProperty( OAK_CHILD_ORDER, nodes.keySet(), Type.NAMES)); } } - private void setVersionablePaths() { - if (isVersionable.apply(this)) { - String uuid = getString(JCR_VERSIONHISTORY); - if (uuid != null) { - versionablePaths.put(uuid, getPath()); - } - } else if (isVersionHistory.apply(this)) { - String uuid = getString(JCR_UUID); - String path = versionablePaths.get(uuid); - if (path != null) { - properties.put(workspaceName, PropertyStates.createProperty( - workspaceName, path, Type.PATH)); - - Set mixins = newLinkedHashSet(getNames(JCR_MIXINTYPES)); - if (mixins.add(MIX_REP_VERSIONABLE_PATHS)) { - properties.put(JCR_MIXINTYPES, PropertyStates.createProperty( - JCR_MIXINTYPES, mixins, Type.NAMES)); - } - } - } - } - private Map createNodes(NodePropBundle bundle) { Map children = newLinkedHashMap(); for (ChildNodeEntry entry : bundle.getChildNodeEntries()) { String base = createName(entry.getName()); String name = base; for (int i = 2; children.containsKey(name); i++) { name = base + '[' + i + ']'; } - children.put(name, entry.getId()); + + if (!ignoredPaths.contains(PathUtils.concat(getPath(), name))) { + children.put(name, entry.getId()); + } } return children; } private Map createProperties(NodePropBundle bundle) { Map properties = newHashMap(); diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java index d146e04..260fb4c 100644 --- oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java @@ -22,18 +22,14 @@ import static com.google.common.collect.ImmutableSet.copyOf; import static com.google.common.collect.ImmutableSet.of; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayListWithCapacity; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.union; import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; -import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONSTORAGE; -import static org.apache.jackrabbit.core.RepositoryImpl.ACTIVITIES_NODE_ID; -import static org.apache.jackrabbit.core.RepositoryImpl.ROOT_NODE_ID; -import static org.apache.jackrabbit.core.RepositoryImpl.VERSION_STORAGE_NODE_ID; import static org.apache.jackrabbit.oak.plugins.name.Namespaces.addCustomMapping; import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH; import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.JCR_ALL; import static org.apache.jackrabbit.oak.upgrade.nodestate.FilteringNodeState.ALL; import static org.apache.jackrabbit.oak.upgrade.nodestate.FilteringNodeState.NONE; import java.io.File; @@ -69,15 +65,14 @@ import org.apache.jackrabbit.core.RepositoryContext; import org.apache.jackrabbit.core.config.BeanConfig; import org.apache.jackrabbit.core.config.LoginModuleConfig; import org.apache.jackrabbit.core.config.RepositoryConfig; import org.apache.jackrabbit.core.config.SecurityConfig; import org.apache.jackrabbit.core.fs.FileSystem; import org.apache.jackrabbit.core.fs.FileSystemException; import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; -import org.apache.jackrabbit.core.persistence.PersistenceManager; import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; import org.apache.jackrabbit.core.security.user.UserManagerImpl; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.Root; import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.namepath.NamePathMapper; import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider; @@ -158,14 +153,20 @@ public class RepositoryUpgrade { * Whether or not to copy binaries by reference. Defaults to false. */ private boolean copyBinariesByReference = false; private List customCommitHooks = null; /** + * Whether or not to skip copying the version histories of versionable + * nodes. Defaults to {@code false}. + */ + private boolean skipVersionCopy = false; + + /** * Copies the contents of the repository in the given source directory * to the given target node store. * * @param source source repository directory * @param target target node store * @throws RepositoryException if the copy operation fails */ @@ -250,14 +251,28 @@ public class RepositoryUpgrade { * @param excludes Paths to be excluded from the copy. */ public void setExcludes(@Nonnull String... excludes) { this.excludePaths = copyOf(checkNotNull(excludes)); } /** + * Sets whether copying version histories for versionable nodes should be skipped or not. + *
+ * If set to {@code true} the version history is not copied and the versionable mixin + * and related properties are removed from the versionable node. + *
+ * Otherwise the version histories of all copied versionable nodes are copied as well. + * + * @param skipVersionCopy Whether or not to skip copying version histories. + */ + public void setSkipVersionCopy(boolean skipVersionCopy) { + this.skipVersionCopy = skipVersionCopy; + } + + /** * Copies the full content from the source to the target repository. *

* The source repository must not be modified while * the copy operation is running to avoid an inconsistent copy. *

* Note that both the source and the target repository must be closed * during the copy operation as this method requires exclusive access @@ -326,39 +341,42 @@ public class RepositoryUpgrade { logger.debug("Privilege registration completed."); // Triggers compilation of type information, which we need for // the type predicates used by the bulk copy operations below. new TypeEditorProvider(false).getRootEditor( base, builder.getNodeState(), builder, null); - Map versionablePaths = newHashMap(); NodeState root = builder.getNodeState(); + + final NodeState sourceState = JackrabbitNodeState.createRootNodeState( + source, workspaceName, root, uriToPrefix, copyBinariesByReference); + final Stopwatch watch = Stopwatch.createStarted(); logger.info("Copying workspace content"); - copyWorkspace(builder, root, workspaceName, uriToPrefix, versionablePaths); + copyWorkspace(sourceState, builder, workspaceName); + builder.getNodeState(); // on TarMK this does call triggers the actual copy logger.debug("Upgrading workspace content completed."); - logger.info("Copying version store content"); - copyVersionStore(builder, root, workspaceName, uriToPrefix, versionablePaths); - logger.debug("Upgrading version store content completed."); - logger.info("Applying default commit hooks"); // TODO: default hooks? List hooks = newArrayList(); UserConfiguration userConf = security.getConfiguration(UserConfiguration.class); String groupsPath = userConf.getParameters().getConfigValue( UserConstants.PARAM_GROUP_PATH, UserConstants.DEFAULT_GROUP_PATH); // hooks specific to the upgrade, need to run first hooks.add(new EditorHook(new CompositeEditorProvider( new RestrictionEditorProvider(), - new GroupEditorProvider(groupsPath)))); + new GroupEditorProvider(groupsPath), + // copy referenced version histories + new VersionHistoryEditor.Provider(sourceState, workspaceName, skipVersionCopy) + ))); // security-related hooks for (SecurityConfiguration sc : security.getConfigurations()) { hooks.addAll(sc.getCommitHooks(workspaceName)); } if (customCommitHooks != null) { @@ -720,79 +738,42 @@ public class RepositoryUpgrade { } tmpl.setDefaultValues(vs); } return tmpl; } - private void copyVersionStore( - NodeBuilder builder, NodeState root, String workspaceName, - Map uriToPrefix, - Map versionablePaths) { - PersistenceManager pm = source.getInternalVersionManager().getPersistenceManager(); - NodeBuilder system = builder.child(JCR_SYSTEM); - - logger.info("Copying version histories"); - NodeStateCopier.builder().copy( - new JackrabbitNodeState( - pm, root, uriToPrefix, VERSION_STORAGE_NODE_ID, - "/jcr:system/jcr:versionStorage", - workspaceName, versionablePaths, copyBinariesByReference - ), - system.child(JCR_VERSIONSTORAGE)); - - logger.info("Copying activities"); - NodeStateCopier.builder().copy( - new JackrabbitNodeState( - pm, root, uriToPrefix, ACTIVITIES_NODE_ID, - "/jcr:system/jcr:activities", - workspaceName, versionablePaths, copyBinariesByReference - ), - system.child("jcr:activities")); - } - - private String copyWorkspace( - NodeBuilder builder, NodeState root, String workspaceName, - Map uriToPrefix, Map versionablePaths) + private String copyWorkspace(NodeState sourceState, NodeBuilder builder, String workspaceName) throws RepositoryException { - PersistenceManager pm = - source.getWorkspaceInfo(workspaceName).getPersistenceManager(); - - NodeState state = new JackrabbitNodeState( - pm, root, uriToPrefix, ROOT_NODE_ID, "/", - workspaceName, versionablePaths, copyBinariesByReference); - - final Set includes = calculateEffectiveIncludePaths(state); - final Set excludes = union(copyOf(this.excludePaths), of("/jcr:system")); + final Set includes = calculateEffectiveIncludePaths(sourceState); + final Set excludes = union(copyOf(this.excludePaths), of("/jcr:system/jcr:versionStorage")); final Set merges = of("/jcr:system"); logger.info("Copying workspace {} [i: {}, e: {}, m: {}]", workspaceName, includes, excludes, merges); NodeStateCopier.builder() .include(includes) .exclude(excludes) .merge(merges) - .copy(state, builder); + .copy(sourceState, builder); return workspaceName; } private Set calculateEffectiveIncludePaths(NodeState state) { if (!this.includePaths.contains("/")) { return copyOf(this.includePaths); } // include child nodes from source individually to avoid deleting other initialized content final Set includes = newHashSet(); for (String childNodeName : state.getChildNodeNames()) { includes.add("/" + childNodeName); } - // jcr:system is handled explicitly - includes.remove("/jcr:system"); return includes; } private static class LoggingCompositeHook implements CommitHook { private final Collection hooks; public LoggingCompositeHook(Collection hooks) { diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/VersionHistoryEditor.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/VersionHistoryEditor.java new file mode 100644 index 0000000..52aa61b --- /dev/null +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/VersionHistoryEditor.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 org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +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.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableSet.of; +import static com.google.common.collect.Sets.newHashSet; +import static org.apache.jackrabbit.JcrConstants.JCR_BASEVERSION; +import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT; +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PREDECESSORS; +import static org.apache.jackrabbit.JcrConstants.JCR_UUID; +import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONHISTORY; +import static org.apache.jackrabbit.JcrConstants.MIX_REFERENCEABLE; +import static org.apache.jackrabbit.JcrConstants.MIX_VERSIONABLE; +import static org.apache.jackrabbit.oak.plugins.memory.MultiGenericPropertyState.nameProperty; +import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.MIX_REP_VERSIONABLE_PATHS; +import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.VERSION_STORE_PATH; + +/** + * The VersionHistoryEditor provides two possible ways to handle + * versionable nodes: + *

    + *
  • it can copy the version histories of versionable nodes, or
  • + *
  • + * it can skjip copying version histories and remove the + * {@code mix:versionable} mixin together with any related + * properties (see {@link #removeVersionProperties(NodeBuilder)}). + *
  • + *
+ */ +public class VersionHistoryEditor extends DefaultEditor { + + private static final Logger LOG = LoggerFactory.getLogger(VersionHistoryEditor.class); + + private static final Set SKIPPED_PATHS = of("/jcr:system", "/oak:index"); + + private final Provider provider; + + private final NodeBuilder rootBuilder; + + private final TypePredicate isVersionable; + + private final TypePredicate isReferenceable; + + private String path; + + private VersionHistoryEditor(Provider provider, NodeBuilder builder) { + this.provider = provider; + this.rootBuilder = builder; + this.isVersionable = new TypePredicate(builder.getNodeState(), MIX_VERSIONABLE); + this.isReferenceable = new TypePredicate(builder.getNodeState(), MIX_REFERENCEABLE); + this.path = "/"; + } + + public static class Provider implements EditorProvider { + + private NodeState sourceRoot; + + private String workspaceName; + + private boolean skipVersionCopy; + + public Provider(NodeState sourceRoot, String workspaceName, boolean skipVersionCopy) { + checkArgument(sourceRoot != null, "sourceRoot must not be null"); + this.sourceRoot = sourceRoot; + this.workspaceName = workspaceName; + this.skipVersionCopy = skipVersionCopy; + } + + @Override + public Editor getRootEditor(NodeState before, NodeState after, NodeBuilder builder, CommitInfo info) throws CommitFailedException { + return new VersionHistoryEditor(this, builder); + } + } + + @Override + public Editor childNodeAdded(String name, NodeState after) throws CommitFailedException { + + final String path = PathUtils.concat(this.path, name); + // skip deleted nodes and well known paths that may not contain versionable nodes + if (after == null || SKIPPED_PATHS.contains(path)) { + return null; + } + + // assign path field only after checking that we don't skip this subtree + this.path = path; + + if (isVersionable.apply(after)) { + final NodeBuilder builder = getNodeBuilder(rootBuilder, this.path); + if (provider.skipVersionCopy) { + removeVersionProperties(builder); + } else { + final String historyPath = copyVersions(provider.sourceRoot, rootBuilder, after); + final NodeBuilder historyBuilder = getNodeBuilder(rootBuilder, historyPath); + if (historyBuilder.exists()) { + historyBuilder.setProperty(provider.workspaceName, this.path); + addMixin(historyBuilder, MIX_REP_VERSIONABLE_PATHS); + } else { + LOG.warn("Version history for node {} does not exist, removing related properties.", path); + removeVersionProperties(builder); + } + } + } + + return this; + } + + @Override + public Editor childNodeChanged(String name, NodeState before, NodeState after) throws CommitFailedException { + return childNodeAdded(name, after); + } + + @Override + public Editor childNodeDeleted(String name, NodeState before) throws CommitFailedException { + return childNodeAdded(name, null); + } + + @Override + public void leave(NodeState before, NodeState after) throws CommitFailedException { + this.path = PathUtils.getParentPath(this.path); + } + + private static String copyVersions( + NodeState sourceRoot, NodeBuilder targetRoot, NodeState versionable) { + + final String versionableUUID = getProperty(versionable, JCR_UUID, Type.STRING); + final String historyPath = calculateVersionHistoryPath(versionableUUID); + + NodeStateCopier.builder() + .include(historyPath) + .merge(VERSION_STORE_PATH) + .copy(sourceRoot, targetRoot); + + return historyPath; + } + + private static T getProperty(NodeState state, String name, Type type) { + if (state.hasProperty(name)) { + return state.getProperty(name).getValue(type); + } + return null; + } + + private static NodeBuilder getNodeBuilder(NodeBuilder root, String path) { + NodeBuilder builder = root; + for (String name : PathUtils.elements(path)) { + builder = builder.getChildNode(name); + } + return builder; + } + + private void removeVersionProperties(final NodeBuilder builder) { + assert builder.exists(); + + removeMixin(builder, MIX_VERSIONABLE); + + // we don't know if the UUID is otherwise referenced, + // so make sure the node remains referencable + if (!isReferenceable.apply(builder.getNodeState())) { + addMixin(builder, MIX_REFERENCEABLE); + } + + builder.removeProperty(JCR_VERSIONHISTORY); + builder.removeProperty(JCR_PREDECESSORS); + builder.removeProperty(JCR_BASEVERSION); + builder.removeProperty(JCR_ISCHECKEDOUT); + } + + private static void addMixin(NodeBuilder builder, String name) { + if (builder.hasProperty(JCR_MIXINTYPES)) { + final Set mixins = newHashSet(builder.getProperty(JCR_MIXINTYPES).getValue(Type.NAMES)); + if (mixins.add(name)) { + builder.setProperty(nameProperty(JCR_MIXINTYPES, mixins)); + } + } else { + builder.setProperty(nameProperty(JCR_MIXINTYPES, of(name))); + } + } + + private static void removeMixin(NodeBuilder builder, String name) { + if (builder.hasProperty(JCR_MIXINTYPES)) { + final Set mixins = newHashSet(builder.getProperty(JCR_MIXINTYPES).getValue(Type.NAMES)); + if (mixins.remove(name)) { + if (mixins.isEmpty()) { + builder.removeProperty(JCR_MIXINTYPES); + } else { + builder.setProperty(nameProperty(JCR_MIXINTYPES, mixins)); + } + } + } + } + + private static String calculateVersionHistoryPath(String historyUUID) { + return PathUtils.concat( + VERSION_STORE_PATH, + historyUUID.substring(0, 2), + historyUUID.substring(2, 4), + historyUUID.substring(4, 6), + historyUUID + ); + } +} diff --git oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java new file mode 100644 index 0000000..972bd51 --- /dev/null +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java @@ -0,0 +1,141 @@ +/* + * 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 org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.oak.Oak; +import org.apache.jackrabbit.oak.jcr.Jcr; +import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.junit.AfterClass; +import org.junit.Test; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class CopyVersionHistoryTest extends AbstractRepositoryUpgradeTest { + + public static final String TEST_PATH = "/versionedNode"; + + /** + * Home directory of source repository. + */ + private static File source; + + @Override + protected void createSourceContent(Repository repository) throws Exception { + final Session session = repository.login(CREDENTIALS); + final VersionManager versionManager = session.getWorkspace().getVersionManager(); + + // create node with version history + final Node versionable = JcrUtils.getOrCreateByPath(TEST_PATH, "nt:unstructured", session); + versionable.addMixin("mix:versionable"); + versionable.setProperty("version", "root"); + session.save(); + for (int i = 0; i < 3; i++) { + versionable.setProperty("version", "1." + i); + session.save(); + versionManager.checkpoint(TEST_PATH); + } + final VersionHistory versionHistory = versionManager.getVersionHistory(TEST_PATH); + versionHistory.addVersionLabel(JcrConstants.JCR_ROOTVERSION, "version root", false); + versionHistory.addVersionLabel("1.0", "version 1.0", false); + versionHistory.addVersionLabel("1.1", "version 1.1", false); + versionHistory.addVersionLabel("1.2", "version 1.2", false); + } + + @Override + protected void doUpgradeRepository(File source, NodeStore target) throws RepositoryException { + // abuse this method to capture the source repo directory + CopyVersionHistoryTest.source = source; + } + + @AfterClass + public static void teardown() { + CopyVersionHistoryTest.source = null; + } + + @Test + public void shouldHaveFullVersionHistory() throws RepositoryException { + assert source != null; + + final NodeStore targetNodeStore = createTargetNodeStore(); + RepositoryUpgrade.copy(source, targetNodeStore); + + final Repository repository = new Jcr(new Oak(targetNodeStore)).createRepository(); + final Session session = repository.login(CREDENTIALS); + + assertTrue(session.nodeExists(TEST_PATH)); + assertVersionCount(4, session, TEST_PATH); + } + + @Test + public void shouldSkipCopyingVersionHistory() throws RepositoryException { + assert source != null; + + final RepositoryConfig sourceConfig = RepositoryConfig.create(source); + final RepositoryContext sourceContext = RepositoryContext.create(sourceConfig); + final NodeStore targetNodeStore = createTargetNodeStore(); + try { + final RepositoryUpgrade upgrade = new RepositoryUpgrade(sourceContext, targetNodeStore); + upgrade.setSkipVersionCopy(true); + upgrade.copy(null); + } finally { + sourceContext.getRepository().shutdown(); + } + + final Repository repository = new Jcr(new Oak(targetNodeStore)).createRepository(); + final Session session = repository.login(CREDENTIALS); + final VersionManager versionManager = session.getWorkspace().getVersionManager(); + + assertTrue(session.nodeExists(TEST_PATH)); + assertFalse(session.getNode(TEST_PATH).isNodeType("mix:versionable")); + + try { + versionManager.getVersionHistory(TEST_PATH); + fail("The node should not have a version history."); + } catch (UnsupportedRepositoryOperationException e) { + // expected + } + } + + private void assertVersionCount(int expectedCount, Session session, String path) throws RepositoryException { + final VersionManager versionManager = session.getWorkspace().getVersionManager(); + final VersionHistory versionHistory = versionManager.getVersionHistory(path); + final VersionIterator allVersions = versionHistory.getAllVersions(); + int versionCount = 0; + while (allVersions.hasNext()) { + allVersions.nextVersion(); + versionCount++; + } + assertEquals("Should have " + expectedCount + " versions", expectedCount, versionCount); + } +}