You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-dev@jackrabbit.apache.org by Julian Sedding <js...@gmail.com> on 2015/08/06 16:41:27 UTC

Re: svn commit: r1694498 - in /jackrabbit/oak/trunk: oak-run/src/main/java/org/apache/jackrabbit/oak/run/ oak-run/src/test/java/org/apache/jackrabbit/oak/run/ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/ oak-upgrade/src/main/java/org/apache...

Thanks Manfred for getting this in!

For completeness regarding credits (Manfred couldn't know): I created
the initial patch and developed some doubts over time. Tomek took care
of adapting the code to address those doubts and create a superior
feature (IMHO). I assisted at that stage with code reviews and
suggestions only. So thanks Tomek!

Regards
Julian


On Thu, Aug 6, 2015 at 3:57 PM,  <ba...@apache.org> wrote:
> Author: baedke
> Date: Thu Aug  6 13:57:58 2015
> New Revision: 1694498
>
> URL: http://svn.apache.org/r1694498
> Log:
> OAK-2776: Upgrade should allow to skip copying versions
>
> Initial implementation. Full credit goes to Julian Sedding (jsedding@gmail.com) for the patch and to Tomek Rekawek (trekawek@gmail.com) for aligning it with the current trunk and integrating into oak-run.
>
> Added:
>     jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/run/ParseVersionCopyArgumentTest.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/DescendantsIterator.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopier.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopyConfiguration.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionHistoryUtil.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionableEditor.java
>     jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java
>     jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/VersionCopyTestUtils.java
> Modified:
>     jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java
>     jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
>     jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java
>
> Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java?rev=1694498&r1=1694497&r2=1694498&view=diff
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java (original)
> +++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java Thu Aug  6 13:57:58 2015
> @@ -28,8 +28,12 @@ import java.io.File;
>  import java.io.IOException;
>  import java.io.InputStream;
>  import java.sql.Timestamp;
> +import java.text.DateFormat;
> +import java.text.ParseException;
> +import java.text.SimpleDateFormat;
>  import java.util.ArrayList;
>  import java.util.Arrays;
> +import java.util.Calendar;
>  import java.util.Collections;
>  import java.util.HashMap;
>  import java.util.HashSet;
> @@ -60,6 +64,8 @@ import joptsimple.ArgumentAcceptingOptio
>  import joptsimple.OptionParser;
>  import joptsimple.OptionSet;
>  import joptsimple.OptionSpec;
> +
> +import org.apache.commons.lang.time.DateUtils;
>  import org.apache.jackrabbit.core.RepositoryContext;
>  import org.apache.jackrabbit.core.config.RepositoryConfig;
>  import org.apache.jackrabbit.oak.Oak;
> @@ -939,6 +945,8 @@ public final class Main {
>      private static void upgrade(String[] args) throws Exception {
>          OptionParser parser = new OptionParser();
>          parser.accepts("datastore", "keep data store");
> +        ArgumentAcceptingOptionSpec<String> copyVersions = parser.accepts("copy-versions", "copy referenced versions. valid arguments: true|false|yyyy-mm-dd").withRequiredArg().defaultsTo("true");
> +        ArgumentAcceptingOptionSpec<String> copyOrphanedVersions = parser.accepts("copy-orphaned-versions", "copy all versions. valid arguments: true|false|yyyy-mm-dd").withRequiredArg().defaultsTo("true");
>          OptionSpec<String> nonOption = parser.nonOptions();
>          OptionSet options = parser.parse(args);
>
> @@ -967,6 +975,7 @@ public final class Main {
>                                      new RepositoryUpgrade(source, target);
>                              upgrade.setCopyBinariesByReference(
>                                      options.has("datastore"));
> +                            setCopyVersionOptions(copyVersions.value(options), copyOrphanedVersions.value(options), upgrade);
>                              upgrade.copy(null);
>                          } finally {
>                              target.dispose();
> @@ -996,6 +1005,26 @@ public final class Main {
>          }
>      }
>
> +    private static void setCopyVersionOptions(String copyVersions, String copyOrphanedVersions, RepositoryUpgrade upgrade) throws ParseException {
> +        upgrade.setCopyVersions(parseVersionCopyArgument(copyVersions));
> +        upgrade.setCopyOrphanedVersions(parseVersionCopyArgument(copyOrphanedVersions));
> +    }
> +
> +    static Calendar parseVersionCopyArgument(String string) throws ParseException {
> +        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
> +        final Calendar calendar;
> +
> +        if (Boolean.parseBoolean(string)) {
> +            calendar = Calendar.getInstance();
> +            calendar.setTimeInMillis(0);
> +        } else if (string != null && string.matches("^\\d{4}-\\d{2}-\\d{2}$")) {
> +            calendar = DateUtils.toCalendar(df.parse(string));
> +        } else {
> +            calendar = null;
> +        }
> +        return calendar;
> +    }
> +
>      private static void server(String defaultUri, String[] args) throws Exception {
>          OptionParser parser = new OptionParser();
>
>
> Added: jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/run/ParseVersionCopyArgumentTest.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/run/ParseVersionCopyArgumentTest.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/run/ParseVersionCopyArgumentTest.java (added)
> +++ jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/run/ParseVersionCopyArgumentTest.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,53 @@
> +/*
> + * 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.run;
> +
> +import java.text.ParseException;
> +import java.util.Arrays;
> +import java.util.Calendar;
> +import java.util.GregorianCalendar;
> +
> +import org.junit.Assert;
> +import org.junit.Test;
> +
> +public class ParseVersionCopyArgumentTest {
> +
> +    @Test
> +    public void parseTrue() throws ParseException {
> +        for (String argument : Arrays.asList("true", "TRUE", "TrUe")) {
> +            final Calendar result = Main.parseVersionCopyArgument(argument);
> +            Assert.assertEquals(0, result.getTimeInMillis());
> +        }
> +    }
> +
> +    @Test
> +    public void parseDate() throws ParseException {
> +        final Calendar result = Main.parseVersionCopyArgument("2013-01-01");
> +        Assert.assertEquals(new GregorianCalendar(2013, 0, 1), result);
> +    }
> +
> +    @Test
> +    public void parseFalse() throws ParseException {
> +        for (String argument : Arrays.asList("false", "FaLse", "", "xyz", null)) {
> +            final Calendar result = Main.parseVersionCopyArgument(argument);
> +            Assert.assertNull(result);
> +        }
> +    }
> +}
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/DescendantsIterator.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/DescendantsIterator.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/DescendantsIterator.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/DescendantsIterator.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,46 @@
> +package org.apache.jackrabbit.oak.upgrade;
> +
> +import java.util.ArrayDeque;
> +import java.util.Deque;
> +import java.util.Iterator;
> +
> +import org.apache.jackrabbit.commons.iterator.AbstractLazyIterator;
> +import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
> +import org.apache.jackrabbit.oak.spi.state.NodeState;
> +
> +public class DescendantsIterator extends AbstractLazyIterator<NodeState> {
> +
> +    private final Deque<Iterator<? extends ChildNodeEntry>> stack = new ArrayDeque<Iterator<? extends ChildNodeEntry>>();
> +
> +    private final int maxLevel;
> +
> +    public DescendantsIterator(NodeState root, int maxLevel) {
> +        this.maxLevel = maxLevel;
> +        stack.push(root.getChildNodeEntries().iterator());
> +    }
> +
> +    @Override
> +    protected NodeState getNext() {
> +        if (!fillStack()) {
> +            return null;
> +        }
> +        return stack.peekFirst().next().getNodeState();
> +    }
> +
> +    private boolean fillStack() {
> +        while (stack.size() < maxLevel || !stack.peekFirst().hasNext()) {
> +            Iterator<? extends ChildNodeEntry> topIterator = stack.peekFirst();
> +            if (topIterator.hasNext()) {
> +                final NodeState nextNode = topIterator.next().getNodeState();
> +                stack.push(nextNode.getChildNodeEntries().iterator());
> +            } else {
> +                stack.pop();
> +                if (stack.isEmpty()) {
> +                    return false;
> +                }
> +            }
> +        }
> +        return true;
> +    }
> +
> +}
>
> Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java?rev=1694498&r1=1694497&r2=1694498&view=diff
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java (original)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java Thu Aug  6 13:57:58 2015
> @@ -32,7 +32,6 @@ import static org.apache.jackrabbit.JcrC
>  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;
> @@ -46,7 +45,6 @@ import static org.apache.jackrabbit.oak.
>  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;
> @@ -139,8 +137,6 @@ class JackrabbitNodeState extends Abstra
>       */
>      private final Map<String, String> uriToPrefix;
>
> -    private final Map<String, String> versionablePaths;
> -
>      private final boolean useBinaryReferences;
>
>      private final Map<String, NodeId> nodes;
> @@ -158,7 +154,6 @@ class JackrabbitNodeState extends Abstra
>              String workspaceName,
>              NodeState root,
>              Map<String, String> uriToPrefix,
> -            Map<String, String> versionablePaths,
>              boolean copyBinariesByReference,
>              boolean skipOnError
>      ) throws RepositoryException {
> @@ -169,7 +164,6 @@ class JackrabbitNodeState extends Abstra
>                  versionPM, root, uriToPrefix,
>                  VERSION_STORAGE_NODE_ID, "/jcr:system/jcr:versionStorage",
>                  null,
> -                versionablePaths,
>                  emptyMountPoints,
>                  copyBinariesByReference,
>                  skipOnError
> @@ -179,7 +173,6 @@ class JackrabbitNodeState extends Abstra
>                  versionPM, root, uriToPrefix,
>                  ACTIVITIES_NODE_ID, "/jcr:system/jcr:activities",
>                  null,
> -                versionablePaths,
>                  emptyMountPoints,
>                  copyBinariesByReference,
>                  skipOnError
> @@ -193,7 +186,7 @@ class JackrabbitNodeState extends Abstra
>          );
>          return new JackrabbitNodeState(
>                  pm, root, uriToPrefix, ROOT_NODE_ID, "/",
> -                workspaceName, versionablePaths, mountPoints, copyBinariesByReference, skipOnError);
> +                workspaceName, mountPoints, copyBinariesByReference, skipOnError);
>      }
>
>      private JackrabbitNodeState(
> @@ -209,7 +202,6 @@ class JackrabbitNodeState extends Abstra
>          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);
> @@ -217,7 +209,6 @@ class JackrabbitNodeState extends Abstra
>          this.mountPoints = parent.mountPoints;
>          this.nodeStateCache = parent.nodeStateCache;
>          setChildOrder();
> -        setVersionablePaths();
>          fixFrozenUuid();
>          logNewNode(this);
>      }
> @@ -225,7 +216,7 @@ class JackrabbitNodeState extends Abstra
>      JackrabbitNodeState(
>              PersistenceManager source, NodeState root,
>              Map<String, String> uriToPrefix, NodeId id, String path,
> -            String workspaceName, Map<String, String> versionablePaths,
> +            String workspaceName,
>              Map<NodeId, JackrabbitNodeState> mountPoints,
>              boolean useBinaryReferences, boolean skipOnError) {
>          this.parent = null;
> @@ -239,7 +230,6 @@ class JackrabbitNodeState extends Abstra
>          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<NodeId, JackrabbitNodeState>(cacheSize, 0.75f, true) {
> @@ -380,28 +370,6 @@ class JackrabbitNodeState extends Abstra
>          }
>      }
>
> -    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<String> 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<String, NodeId> createNodes(NodePropBundle bundle) {
>          Map<String, NodeId> children = newLinkedHashMap();
>          for (ChildNodeEntry entry : bundle.getChildNodeEntries()) {
> @@ -728,4 +696,4 @@ class JackrabbitNodeState extends Abstra
>          log.warn(getPath() + ": " + message, cause);
>      }
>
> -}
> \ No newline at end of file
> +}
>
> Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java?rev=1694498&r1=1694497&r2=1694498&view=diff
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java (original)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java Thu Aug  6 13:57:58 2015
> @@ -26,6 +26,7 @@ import static com.google.common.collect.
>  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.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;
> @@ -35,6 +36,7 @@ import static org.apache.jackrabbit.oak.
>  import java.io.File;
>  import java.io.IOException;
>  import java.io.InputStream;
> +import java.util.Calendar;
>  import java.util.Collection;
>  import java.util.Iterator;
>  import java.util.List;
> @@ -110,6 +112,9 @@ import org.apache.jackrabbit.oak.spi.sta
>  import org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier;
>  import org.apache.jackrabbit.oak.upgrade.security.GroupEditorProvider;
>  import org.apache.jackrabbit.oak.upgrade.security.RestrictionEditorProvider;
> +import org.apache.jackrabbit.oak.upgrade.version.VersionCopier;
> +import org.apache.jackrabbit.oak.upgrade.version.VersionCopyConfiguration;
> +import org.apache.jackrabbit.oak.upgrade.version.VersionableEditor;
>  import org.apache.jackrabbit.spi.Name;
>  import org.apache.jackrabbit.spi.QNodeDefinition;
>  import org.apache.jackrabbit.spi.QNodeTypeDefinition;
> @@ -168,6 +173,8 @@ public class RepositoryUpgrade {
>
>      private List<CommitHook> customCommitHooks = null;
>
> +    private VersionCopyConfiguration versionCopyConfiguration = new VersionCopyConfiguration();
> +
>      /**
>       * Copies the contents of the repository in the given source directory
>       * to the given target node store.
> @@ -288,6 +295,41 @@ public class RepositoryUpgrade {
>      }
>
>      /**
> +     * Configures the version storage copy. Be default all versions are copied.
> +     * One may disable it completely by setting {@code null} here or limit it to
> +     * a selected date range: {@code <minDate, now()>}.
> +     *
> +     * @param minDate
> +     *            minimum date of the versions to copy or {@code null} to
> +     *            disable the storage version copying completely. Default value:
> +     *            {@code 1970-01-01 00:00:00}.
> +     */
> +    public void setCopyVersions(Calendar minDate) {
> +        versionCopyConfiguration.setCopyVersions(minDate);
> +    }
> +
> +    /**
> +     * Configures copying of the orphaned version histories (eg. ones that are
> +     * not referenced by the existing nodes). By default all orphaned version
> +     * histories are copied. One may disable it completely by setting
> +     * {@code null} here or limit it to a selected date range:
> +     * {@code <minDate, now()>}. <br/>
> +     * <br/>
> +     * Please notice, that this option is overriden by the
> +     * {@link #setCopyVersions(Calendar)}. You can't copy orphaned versions
> +     * older than set in {@link #setCopyVersions(Calendar)} and if you set
> +     * {@code null} there, this option will be ignored.
> +     *
> +     * @param minDate
> +     *            minimum date of the orphaned versions to copy or {@code null}
> +     *            to not copy them at all. Default value:
> +     *            {@code 1970-01-01 00:00:00}.
> +     */
> +    public void setCopyOrphanedVersions(Calendar minDate) {
> +        versionCopyConfiguration.setCopyOrphanedVersions(minDate);
> +    }
> +
> +    /**
>       * Copies the full content from the source to the target repository.
>       * <p>
>       * The source repository <strong>must not be modified</strong> while
> @@ -364,11 +406,10 @@ public class RepositoryUpgrade {
>              new TypeEditorProvider(false).getRootEditor(
>                      base, builder.getNodeState(), builder, null);
>
> -            Map<String, String> versionablePaths = newHashMap();
>              NodeState root = builder.getNodeState();
>
>              final NodeState sourceState = JackrabbitNodeState.createRootNodeState(
> -                    source, workspaceName, root, uriToPrefix, versionablePaths, copyBinariesByReference, skipOnError);
> +                    source, workspaceName, root, uriToPrefix, copyBinariesByReference, skipOnError);
>
>              final Stopwatch watch = Stopwatch.createStarted();
>
> @@ -377,10 +418,15 @@ public class RepositoryUpgrade {
>              builder.getNodeState(); // on TarMK this does call triggers the actual copy
>              logger.info("Upgrading workspace content completed in {}s ({})", watch.elapsed(TimeUnit.SECONDS), watch);
>
> -            watch.reset().start();
> -            logger.info("Copying version store content");
> -            copyVersionStore(sourceState, builder);
> -            logger.debug("Upgrading version store content completed in {}s ({}).", watch.elapsed(TimeUnit.SECONDS), watch);
> +            if (!versionCopyConfiguration.skipOrphanedVersionsCopy()) {
> +                logger.info("Copying version storage");
> +                watch.reset().start();
> +                copyVersionStorage(sourceState, builder);
> +                builder.getNodeState(); // on TarMK this does call triggers the actual copy
> +                logger.info("Version storage copied in {}s ({})", watch.elapsed(TimeUnit.SECONDS), watch);
> +            } else {
> +                logger.info("Skipping the version storage as the copyOrphanedVersions is set to false");
> +            }
>
>              watch.reset().start();
>              logger.info("Applying default commit hooks");
> @@ -396,7 +442,9 @@ public class RepositoryUpgrade {
>              // 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 VersionableEditor.Provider(sourceState, workspaceName, versionCopyConfiguration)
>              )));
>
>              // security-related hooks
> @@ -768,10 +816,10 @@ public class RepositoryUpgrade {
>          return tmpl;
>      }
>
> -    private void copyWorkspace(NodeState sourceState, NodeBuilder builder, String workspaceName)
> +    private String copyWorkspace(NodeState sourceState, NodeBuilder builder, String workspaceName)
>              throws RepositoryException {
>          final Set<String> includes = calculateEffectiveIncludePaths(sourceState);
> -        final Set<String> excludes = union(copyOf(this.excludePaths), of("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities"));
> +        final Set<String> excludes = union(copyOf(this.excludePaths), of("/jcr:system/jcr:versionStorage"));
>          final Set<String> merges = union(copyOf(this.mergePaths), of("/jcr:system"));
>
>          logger.info("Copying workspace {} [i: {}, e: {}, m: {}]", workspaceName, includes, excludes, merges);
> @@ -781,14 +829,22 @@ public class RepositoryUpgrade {
>                  .exclude(excludes)
>                  .merge(merges)
>                  .copy(sourceState, builder);
> +
> +        return workspaceName;
>      }
>
> -    private void copyVersionStore(NodeState sourceState, NodeBuilder builder)
> +    private void copyVersionStorage(NodeState sourceState, NodeBuilder builder)
>              throws RepositoryException {
> -        NodeStateCopier.builder()
> -                .include("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities")
> -                .merge("/jcr:system")
> -                .copy(sourceState, builder);
> +        final NodeState versionStorage = sourceState.getChildNode(JCR_SYSTEM).getChildNode(JCR_VERSIONSTORAGE);
> +        final Iterator<NodeState> versionStorageIterator = new DescendantsIterator(versionStorage, 3);
> +        final VersionCopier versionCopier = new VersionCopier(sourceState, builder);
> +
> +        while (versionStorageIterator.hasNext()) {
> +            final NodeState versionHistoryBucket = versionStorageIterator.next();
> +            for (String versionHistory : versionHistoryBucket.getChildNodeNames()) {
> +                versionCopier.copyVersionHistory(versionHistory, versionCopyConfiguration.getOrphanedMinDate());
> +            }
> +        }
>      }
>
>      private Set<String> calculateEffectiveIncludePaths(NodeState state) {
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopier.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopier.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopier.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopier.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,97 @@
> +/*
> + * 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.version;
> +
> +import static org.apache.jackrabbit.JcrConstants.JCR_CREATED;
> +import static org.apache.jackrabbit.JcrConstants.NT_VERSION;
> +
> +import java.util.Calendar;
> +
> +import org.apache.jackrabbit.oak.api.Type;
> +import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
> +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.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier;
> +import org.apache.jackrabbit.util.ISO8601;
> +
> +import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.VERSION_STORE_PATH;
> +
> +import static org.apache.jackrabbit.oak.upgrade.version.VersionHistoryUtil.getVersionHistoryPath;
> +import static org.apache.jackrabbit.oak.upgrade.version.VersionHistoryUtil.getVersionHistoryNodeState;
> +
> +/**
> + * This class allows to copy the version history, optionally filtering it with a
> + * given date.
> + */
> +public class VersionCopier {
> +
> +    private final TypePredicate isVersion;
> +
> +    private final NodeState sourceRoot;
> +
> +    private final NodeBuilder rootBuilder;
> +
> +    public VersionCopier(NodeState sourceRoot, NodeBuilder rootBuilder) {
> +        this.isVersion = new TypePredicate(rootBuilder.getNodeState(), NT_VERSION);
> +        this.sourceRoot = sourceRoot;
> +        this.rootBuilder = rootBuilder;
> +    }
> +
> +    /**
> +     * Copy history filtering versions using passed date and returns @{code
> +     * true} if at least one version has been copied.
> +     *
> +     * @param versionableUuid
> +     *            Name of the version history node
> +     * @param minDate
> +     *            Only versions older than this date will be copied
> +     * @return {@code true} if at least one version has been copied
> +     */
> +    public boolean copyVersionHistory(String versionableUuid, Calendar minDate) {
> +        final String versionHistoryPath = getVersionHistoryPath(versionableUuid);
> +        final NodeState versionHistory = getVersionHistoryNodeState(sourceRoot, versionableUuid);
> +        final Calendar lastModified = getVersionHistoryLastModified(versionHistory);
> +
> +        if (lastModified.after(minDate) || minDate.getTimeInMillis() == 0) {
> +            NodeStateCopier.builder()
> +                    .include(versionHistoryPath)
> +                    .merge(VERSION_STORE_PATH)
> +                    .copy(sourceRoot, rootBuilder);
> +            return true;
> +        }
> +        return false;
> +    }
> +
> +    private Calendar getVersionHistoryLastModified(final NodeState versionHistory) {
> +        Calendar youngest = Calendar.getInstance();
> +        youngest.setTimeInMillis(0);
> +        for (final ChildNodeEntry entry : versionHistory.getChildNodeEntries()) {
> +            final NodeState version = entry.getNodeState();
> +            if (!isVersion.apply(version)) {
> +                continue;
> +            }
> +            if (version.hasProperty(JCR_CREATED)) {
> +                final Calendar created = ISO8601.parse(version.getProperty(JCR_CREATED).getValue(Type.DATE));
> +                if (created.after(youngest)) {
> +                    youngest = created;
> +                }
> +            }
> +        }
> +        return youngest;
> +    }
> +}
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopyConfiguration.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopyConfiguration.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopyConfiguration.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionCopyConfiguration.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,67 @@
> +/*
> + * 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.version;
> +
> +import java.util.Calendar;
> +
> +/**
> + * This class allows to configure the behaviour of the version copier.
> + */
> +public class VersionCopyConfiguration {
> +
> +    private Calendar copyVersions;
> +
> +    private Calendar copyOrphanedVersions;
> +
> +    public VersionCopyConfiguration() {
> +        final Calendar epoch = Calendar.getInstance();
> +        epoch.setTimeInMillis(0);
> +        this.copyVersions = epoch;
> +        this.copyOrphanedVersions = epoch;
> +    }
> +
> +    public void setCopyVersions(Calendar copyVersions) {
> +        this.copyVersions = copyVersions;
> +    }
> +
> +    public void setCopyOrphanedVersions(Calendar copyOrphanedVersions) {
> +        this.copyOrphanedVersions = copyOrphanedVersions;
> +    }
> +
> +    public Calendar getVersionsMinDate() {
> +        return copyVersions;
> +    }
> +
> +    public Calendar getOrphanedMinDate() {
> +        if (copyVersions == null) {
> +            return copyVersions;
> +        } else if (copyOrphanedVersions != null && copyVersions.after(copyOrphanedVersions)) {
> +            return copyVersions;
> +        } else {
> +            return copyOrphanedVersions;
> +        }
> +    }
> +
> +    public boolean isCopyVersions() {
> +        return copyVersions != null;
> +    }
> +
> +    public boolean skipOrphanedVersionsCopy() {
> +        return copyVersions == null || copyOrphanedVersions == null;
> +    }
> +
> +}
> \ No newline at end of file
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionHistoryUtil.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionHistoryUtil.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionHistoryUtil.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionHistoryUtil.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,67 @@
> +/*
> + * 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.version;
> +
> +import static com.google.common.collect.Iterables.concat;
> +import static java.util.Collections.singleton;
> +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
> +import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONSTORAGE;
> +
> +import java.util.ArrayList;
> +import java.util.List;
> +
> +import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
> +import org.apache.jackrabbit.oak.spi.state.NodeState;
> +
> +import com.google.common.base.Joiner;
> +
> +public class VersionHistoryUtil {
> +
> +    public static String getVersionHistoryPath(String versionableUuid) {
> +        return Joiner.on('/').join(concat(
> +                singleton(""),
> +                getVersionHistoryPathSegments(versionableUuid),
> +                singleton(versionableUuid)));
> +    }
> +
> +    static NodeState getVersionHistoryNodeState(NodeState root, String versionableUuid) {
> +        NodeState historyParent = root;
> +        for (String segment : getVersionHistoryPathSegments(versionableUuid)) {
> +            historyParent = historyParent.getChildNode(segment);
> +        }
> +        return historyParent.getChildNode(versionableUuid);
> +    }
> +
> +    static NodeBuilder getVersionHistoryBuilder(NodeBuilder root, String versionableUuid) {
> +        NodeBuilder history = root;
> +        for (String segment : getVersionHistoryPathSegments(versionableUuid)) {
> +            history = history.getChildNode(segment);
> +        }
> +        return history.getChildNode(versionableUuid);
> +    }
> +
> +    private static List<String> getVersionHistoryPathSegments(String versionableUuid) {
> +        final List<String> segments = new ArrayList<String>();
> +        segments.add(JCR_SYSTEM);
> +        segments.add(JCR_VERSIONSTORAGE);
> +        for (int i = 0; i < 3; i++) {
> +            segments.add(versionableUuid.substring(i * 2, i * 2 + 2));
> +        }
> +        return segments;
> +    }
> +
> +}
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionableEditor.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionableEditor.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionableEditor.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/version/VersionableEditor.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,226 @@
> +/*
> + * 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.version;
> +
> +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 java.util.Set;
> +
> +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;
> +
> +/**
> + * The VersionableEditor provides two possible ways to handle
> + * versionable nodes:
> + * <ul>
> + *     <li>it can copy the version histories of versionable nodes, or</li>
> + *     <li>
> + *         it can skip copying version histories and remove the
> + *         {@code mix:versionable} mixin together with any related
> + *         properties (see {@link #removeVersionProperties(NodeBuilder)}).
> + *     </li>
> + * </ul>
> + */
> +public class VersionableEditor extends DefaultEditor {
> +
> +    private static final Set<String> SKIPPED_PATHS = of("/oak:index", "/jcr:system/jcr:versionStorage");
> +
> +    private final Provider provider;
> +
> +    private final NodeBuilder rootBuilder;
> +
> +    private final TypePredicate isReferenceable;
> +
> +    private final TypePredicate isVersionable;
> +
> +    private final VersionCopier versionCopier;
> +
> +    private String path;
> +
> +    private VersionableEditor(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.versionCopier = new VersionCopier(provider.sourceRoot, builder);
> +        this.path = "/";
> +    }
> +
> +    public static class Provider implements EditorProvider {
> +
> +        private final NodeState sourceRoot;
> +
> +        private final String workspaceName;
> +
> +        private final VersionCopyConfiguration config;
> +
> +        public Provider(NodeState sourceRoot, String workspaceName, VersionCopyConfiguration config) {
> +            this.sourceRoot = sourceRoot;
> +            this.workspaceName = workspaceName;
> +            this.config = config;
> +        }
> +
> +        @Override
> +        public Editor getRootEditor(NodeState before, NodeState after, NodeBuilder builder, CommitInfo info) throws CommitFailedException {
> +            return new VersionableEditor(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;
> +
> +        final VersionCopyConfiguration c = provider.config;
> +        if (isVersionable.apply(after)) {
> +            final String versionableUuid = getProperty(after, JCR_UUID, Type.STRING);
> +            boolean versionHistoryExists = isVersionHistoryExists(versionableUuid);
> +            if (c.isCopyVersions() && c.skipOrphanedVersionsCopy()) {
> +                versionHistoryExists = copyVersionHistory(after);
> +            } else if (c.isCopyVersions() && !c.skipOrphanedVersionsCopy()) {
> +                // all version histories have been copied, but maybe the date
> +                // range for orphaned entries is narrower
> +                if (c.getOrphanedMinDate().after(c.getVersionsMinDate())) {
> +                    versionHistoryExists = copyVersionHistory(after);
> +                }
> +            } else {
> +                versionHistoryExists = false;
> +            }
> +
> +            if (versionHistoryExists) {
> +                setVersionablePath(versionableUuid);
> +            } else {
> +                removeVersionProperties(getNodeBuilder(rootBuilder, this.path));
> +            }
> +        }
> +
> +        return this;
> +    }
> +
> +    private boolean copyVersionHistory(NodeState versionable) {
> +        assert versionable.exists();
> +
> +        final String versionableUuid = versionable.getProperty(JCR_UUID).getValue(Type.STRING);
> +        return versionCopier.copyVersionHistory(versionableUuid, provider.config.getVersionsMinDate());
> +    }
> +
> +    private void setVersionablePath(String versionableUuid) {
> +        final NodeBuilder versionHistory = VersionHistoryUtil.getVersionHistoryBuilder(rootBuilder, versionableUuid);
> +        versionHistory.setProperty(provider.workspaceName, path, Type.PATH);
> +        addMixin(versionHistory, MIX_REP_VERSIONABLE_PATHS);
> +    }
> +
> +    private boolean isVersionHistoryExists(String versionableUuid) {
> +        return VersionHistoryUtil.getVersionHistoryNodeState(rootBuilder.getNodeState(), versionableUuid).exists();
> +    }
> +
> +    private void removeVersionProperties(final NodeBuilder versionableBuilder) {
> +        assert versionableBuilder.exists();
> +
> +        removeMixin(versionableBuilder, MIX_VERSIONABLE);
> +
> +        // we don't know if the UUID is otherwise referenced,
> +        // so make sure the node remains referencable
> +        if (!isReferenceable.apply(versionableBuilder.getNodeState())) {
> +            addMixin(versionableBuilder, MIX_REFERENCEABLE);
> +        }
> +
> +        versionableBuilder.removeProperty(JCR_VERSIONHISTORY);
> +        versionableBuilder.removeProperty(JCR_PREDECESSORS);
> +        versionableBuilder.removeProperty(JCR_BASEVERSION);
> +        versionableBuilder.removeProperty(JCR_ISCHECKEDOUT);
> +    }
> +
> +    @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 <T> T getProperty(NodeState state, String name, Type<T> 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 static void addMixin(NodeBuilder builder, String name) {
> +        if (builder.hasProperty(JCR_MIXINTYPES)) {
> +            final Set<String> 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<String> 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));
> +                }
> +            }
> +        }
> +    }
> +}
>
> Modified: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java?rev=1694498&r1=1694497&r2=1694498&view=diff
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java (original)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java Thu Aug  6 13:57:58 2015
> @@ -47,7 +47,7 @@ import static org.junit.Assert.assertTru
>
>  public abstract class AbstractRepositoryUpgradeTest {
>
> -    protected static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray());
> +    public static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray());
>
>      private static NodeStore targetNodeStore;
>
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/CopyVersionHistoryTest.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,277 @@
> +/*
> + * 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.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.plugins.memory.MemoryNodeStore;
> +import org.apache.jackrabbit.oak.spi.state.NodeStore;
> +import org.apache.jackrabbit.oak.upgrade.util.VersionCopyTestUtils.RepositoryUpgradeSetup;
> +import org.junit.AfterClass;
> +import org.junit.Test;
> +
> +import javax.jcr.Node;
> +import javax.jcr.Property;
> +import javax.jcr.PropertyType;
> +import javax.jcr.Repository;
> +import javax.jcr.RepositoryException;
> +import javax.jcr.Session;
> +import javax.jcr.version.VersionManager;
> +
> +import java.io.File;
> +import java.util.Calendar;
> +
> +import static org.junit.Assert.assertEquals;
> +import static org.junit.Assert.assertFalse;
> +import static org.junit.Assert.assertTrue;
> +import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.MIX_REP_VERSIONABLE_PATHS;
> +import static org.apache.jackrabbit.oak.upgrade.util.VersionCopyTestUtils.createVersionableNode;
> +import static org.apache.jackrabbit.oak.upgrade.util.VersionCopyTestUtils.isVersionable;
> +
> +public class CopyVersionHistoryTest extends AbstractRepositoryUpgradeTest {
> +
> +    private static final String VERSIONABLES_OLD = "/versionables/old";
> +
> +    private static final String VERSIONABLES_OLD_ORPHANED = "/versionables/oldOrphaned";
> +
> +    private static final String VERSIONABLES_YOUNG = "/versionables/young";
> +
> +    private static final String VERSIONABLES_YOUNG_ORPHANED = "/versionables/youngOrphaned";
> +
> +    private static Calendar betweenHistories;
> +
> +    /**
> +     * Home directory of source repository.
> +     */
> +    private static File source;
> +
> +    private static String oldOrphanedHistory;
> +    private static String youngOrphanedHistory;
> +    private static String oldHistory;
> +    private static String youngHistory;
> +
> +    @Override
> +    protected void createSourceContent(Repository repository) throws Exception {
> +        final Session session = repository.login(CREDENTIALS);
> +
> +        oldHistory = createVersionableNode(session, VERSIONABLES_OLD);
> +        oldOrphanedHistory = createVersionableNode(session, VERSIONABLES_OLD_ORPHANED);
> +        Thread.sleep(10);
> +        betweenHistories = Calendar.getInstance();
> +        Thread.sleep(10);
> +        youngOrphanedHistory = createVersionableNode(session, VERSIONABLES_YOUNG_ORPHANED);
> +        youngHistory = createVersionableNode(session, VERSIONABLES_YOUNG);
> +
> +        session.getNode(VERSIONABLES_OLD_ORPHANED).remove();
> +        session.getNode(VERSIONABLES_YOUNG_ORPHANED).remove();
> +        session.save();
> +    }
> +
> +    @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 copyAllVersions() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                // copying all versions is enabled by default
> +            }
> +        });
> +        assertTrue(isVersionable(session, VERSIONABLES_OLD));
> +        assertTrue(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertExisting(session, oldOrphanedHistory, youngOrphanedHistory, oldHistory, youngHistory);
> +        assertHasVersionablePath(session, oldHistory, youngHistory);
> +    }
> +
> +    @Test
> +    public void referencedSinceDate() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyVersions(betweenHistories);
> +            }
> +        });
> +
> +        assertFalse(isVersionable(session, VERSIONABLES_OLD));
> +        assertTrue(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldHistory, oldOrphanedHistory);
> +        assertExisting(session, youngHistory, youngOrphanedHistory);
> +        assertHasVersionablePath(session, youngHistory);
> +    }
> +
> +    @Test
> +    public void referencedOlderThanOrphaned() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyOrphanedVersions(betweenHistories);
> +            }
> +        });
> +
> +        assertTrue(isVersionable(session, VERSIONABLES_OLD));
> +        assertTrue(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldOrphanedHistory);
> +        assertExisting(session, oldHistory, youngHistory, youngOrphanedHistory);
> +        assertHasVersionablePath(session, oldHistory, youngHistory);
> +    }
> +
> +    @Test
> +    public void onlyReferenced() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyOrphanedVersions(null);
> +            }
> +        });
> +        assertTrue(isVersionable(session, VERSIONABLES_OLD));
> +        assertTrue(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldOrphanedHistory, youngOrphanedHistory);
> +        assertExisting(session, oldHistory, youngHistory);
> +        assertHasVersionablePath(session, oldHistory, youngHistory);
> +    }
> +
> +    @Test
> +    public void onlyReferencedAfterDate() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyVersions(betweenHistories);
> +                upgrade.setCopyOrphanedVersions(null);
> +            }
> +        });
> +        assertFalse(isVersionable(session, VERSIONABLES_OLD));
> +        assertTrue(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldHistory, oldOrphanedHistory, youngOrphanedHistory);
> +        assertExisting(session, youngHistory);
> +        assertHasVersionablePath(session, youngHistory);
> +    }
> +
> +    @Test
> +    public void onlyOrphaned() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyVersions(null);
> +            }
> +        });
> +
> +        assertFalse(isVersionable(session, VERSIONABLES_OLD));
> +        assertFalse(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldHistory, youngHistory, oldOrphanedHistory, youngOrphanedHistory);
> +    }
> +
> +    @Test
> +    public void onlyOrphanedAfterDate() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyVersions(null);
> +                upgrade.setCopyOrphanedVersions(betweenHistories);
> +            }
> +        });
> +
> +        assertFalse(isVersionable(session, VERSIONABLES_OLD));
> +        assertFalse(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldHistory, youngHistory, oldOrphanedHistory, youngOrphanedHistory);
> +    }
> +
> +    @Test
> +    public void dontCopyVersionHistory() throws RepositoryException {
> +        assert source != null;
> +
> +        Session session = performCopy(source, new RepositoryUpgradeSetup() {
> +            @Override
> +            public void setup(RepositoryUpgrade upgrade) {
> +                upgrade.setCopyVersions(null);
> +                upgrade.setCopyOrphanedVersions(null);
> +            }
> +        });
> +
> +        assertFalse(isVersionable(session, VERSIONABLES_OLD));
> +        assertFalse(isVersionable(session, VERSIONABLES_YOUNG));
> +        assertMissing(session, oldHistory, youngHistory, oldOrphanedHistory, youngOrphanedHistory);
> +    }
> +
> +    public Session performCopy(File source, RepositoryUpgradeSetup setup) throws RepositoryException {
> +        final RepositoryConfig sourceConfig = RepositoryConfig.create(source);
> +        final RepositoryContext sourceContext = RepositoryContext.create(sourceConfig);
> +        final NodeStore targetNodeStore = new MemoryNodeStore();
> +        try {
> +            final RepositoryUpgrade upgrade = new RepositoryUpgrade(sourceContext, targetNodeStore);
> +            setup.setup(upgrade);
> +            upgrade.copy(null);
> +        } finally {
> +            sourceContext.getRepository().shutdown();
> +        }
> +
> +        final Repository repository = new Jcr(new Oak(targetNodeStore)).createRepository();
> +        return repository.login(AbstractRepositoryUpgradeTest.CREDENTIALS);
> +    }
> +
> +    private static void assertExisting(final Session session, final String... paths) throws RepositoryException {
> +        for (final String path : paths) {
> +            final String relPath = path.substring(1);
> +            assertTrue("node " + path + " should exist", session.getRootNode().hasNode(relPath));
> +        }
> +    }
> +
> +    private static void assertMissing(final Session session, final String... paths) throws RepositoryException {
> +        for (final String path : paths) {
> +            final String relPath = path.substring(1);
> +            assertFalse("node " + path + " should not exist", session.getRootNode().hasNode(relPath));
> +        }
> +    }
> +
> +    public static void assertHasVersionablePath(final Session session, final String... historyPaths) throws RepositoryException {
> +        for (String historyPath : historyPaths) {
> +            final String workspaceName = session.getWorkspace().getName();
> +            final Node versionHistory = session.getNode(historyPath);
> +            assertTrue(versionHistory.isNodeType(MIX_REP_VERSIONABLE_PATHS));
> +            assertTrue(versionHistory.hasProperty(workspaceName));
> +            final Property pathProperty = versionHistory.getProperty(workspaceName);
> +            assertEquals(PropertyType.PATH, pathProperty.getType());
> +
> +            final VersionManager vm = session.getWorkspace().getVersionManager();
> +            assertEquals(historyPath, vm.getVersionHistory(pathProperty.getString()).getPath());
> +        }
> +    }
> +}
>
> Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/VersionCopyTestUtils.java
> URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/VersionCopyTestUtils.java?rev=1694498&view=auto
> ==============================================================================
> --- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/VersionCopyTestUtils.java (added)
> +++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/VersionCopyTestUtils.java Thu Aug  6 13:57:58 2015
> @@ -0,0 +1,57 @@
> +package org.apache.jackrabbit.oak.upgrade.util;
> +
> +import static org.apache.jackrabbit.oak.plugins.version.VersionConstants.MIX_REP_VERSIONABLE_PATHS;
> +import static org.junit.Assert.assertEquals;
> +import static org.junit.Assert.assertTrue;
> +
> +import java.util.ArrayList;
> +import java.util.List;
> +
> +import javax.jcr.Node;
> +import javax.jcr.Property;
> +import javax.jcr.PropertyType;
> +import javax.jcr.RepositoryException;
> +import javax.jcr.Session;
> +import javax.jcr.version.Version;
> +import javax.jcr.version.VersionHistory;
> +import javax.jcr.version.VersionManager;
> +
> +import org.apache.jackrabbit.JcrConstants;
> +import org.apache.jackrabbit.commons.JcrUtils;
> +import org.apache.jackrabbit.oak.upgrade.RepositoryUpgrade;
> +
> +public class VersionCopyTestUtils {
> +
> +    public static String createVersionableNode(Session session, String versionablePath)
> +            throws RepositoryException, InterruptedException {
> +        final VersionManager versionManager = session.getWorkspace().getVersionManager();
> +        final Node versionable = JcrUtils.getOrCreateUniqueByPath(session.getRootNode(), versionablePath,
> +                JcrConstants.NT_UNSTRUCTURED);
> +        versionable.addMixin("mix:versionable");
> +        versionable.setProperty("version", "root");
> +        session.save();
> +
> +        final String path = versionable.getPath();
> +        final List<String> versionNames = new ArrayList<String>();
> +        for (int i = 0; i < 3; i++) {
> +            versionable.setProperty("version", "1." + i);
> +            session.save();
> +            final Version v = versionManager.checkpoint(path);
> +            versionNames.add(v.getName());
> +        }
> +
> +        final VersionHistory history = versionManager.getVersionHistory(path);
> +        for (final String versionName : versionNames) {
> +            history.addVersionLabel(versionName, String.format("version %s", versionName), false);
> +        }
> +        return history.getPath();
> +    }
> +
> +    public static boolean isVersionable(Session session, String path) throws RepositoryException {
> +        return session.getNode(path).isNodeType(JcrConstants.MIX_VERSIONABLE);
> +    }
> +
> +    public interface RepositoryUpgradeSetup {
> +        void setup(RepositoryUpgrade upgrade);
> +    }
> +}
>
>