/* * ModeShape (http://www.modeshape.org) * * Licensed 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.modeshape.jcr; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemExistsException; import javax.jcr.ItemNotFoundException; import javax.jcr.MergeException; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.Value; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.OnParentVersionAction; import javax.jcr.version.Version; import javax.jcr.version.VersionException; import javax.jcr.version.VersionHistory; import javax.jcr.version.VersionIterator; import org.modeshape.common.annotation.NotThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; import org.modeshape.jcr.AbstractJcrNode.Type; import org.modeshape.jcr.api.value.DateTime; import org.modeshape.jcr.cache.CachedNode; import org.modeshape.jcr.cache.ChildReference; import org.modeshape.jcr.cache.ChildReferences; import org.modeshape.jcr.cache.MutableCachedNode; import org.modeshape.jcr.cache.NodeCache; import org.modeshape.jcr.cache.NodeKey; import org.modeshape.jcr.cache.SessionCache; import org.modeshape.jcr.value.DateTimeFactory; import org.modeshape.jcr.value.Name; import org.modeshape.jcr.value.NameFactory; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.PathFactory; import org.modeshape.jcr.value.Property; import org.modeshape.jcr.value.PropertyFactory; import org.modeshape.jcr.value.Reference; import org.modeshape.jcr.value.ReferenceFactory; /** * Local implementation of version management code, comparable to an implementation of the JSR-283 {@code VersionManager} * interface. Valid instances of this class can be obtained by calling {@link JcrWorkspace#versionManager()}. */ final class JcrVersionManager implements org.modeshape.jcr.api.version.VersionManager { private final static Logger LOGGER = Logger.getLogger(JcrVersionManager.class); /** * Property names from nt:frozenNode that should never be copied directly to a node when the frozen node is restored. */ static final Set<Name> IGNORED_PROP_NAMES_FOR_RESTORE = Collections.unmodifiableSet(new HashSet<Name>( Arrays.asList(new Name[] { JcrLexicon.FROZEN_PRIMARY_TYPE, JcrLexicon.FROZEN_MIXIN_TYPES, JcrLexicon.FROZEN_UUID, JcrLexicon.PRIMARY_TYPE, JcrLexicon.MIXIN_TYPES, JcrLexicon.UUID}))); private final JcrSession session; private final Path versionStoragePath; private final PathAlgorithm versionHistoryPathAlgorithm; private final SystemContent readableSystem; public JcrVersionManager( JcrSession session ) { super(); this.session = session; versionStoragePath = absolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE); ExecutionContext context = session.context(); versionHistoryPathAlgorithm = new HiearchicalPathAlgorithm(versionStoragePath, context); readableSystem = new SystemContent(this.session.cache()); } final ExecutionContext context() { return session.context(); } final Name name( String s ) { return session().nameFactory().create(s); } final String string( Object propertyValue ) { return session.stringFactory().create(propertyValue); } final Name name( Object ob ) { return session.nameFactory().create(ob); } final Path path( Path root, Name child ) { return session.pathFactory().create(root, child); } final Path path( Path root, Path.Segment childSegment ) { return session.pathFactory().create(root, childSegment); } final Path absolutePath( Name... absolutePathSegments ) { return session.pathFactory().createAbsolutePath(absolutePathSegments); } final PropertyFactory propertyFactory() { return session.propertyFactory(); } final SessionCache cache() { return session.cache(); } final JcrRepository repository() { return session.repository(); } final JcrSession session() { return session; } final JcrWorkspace workspace() { return session.workspace(); } /** * Return the path to the nt:versionHistory node for the node with the supplied NodeKey. * <p> * This method uses one of two algorithms, both of which operate upon the {@link NodeKey#getIdentifierHash() SHA-1 hash of the * identifier part} of the versionable node's {@link NodeKey key}. In the following descriptions, "{sha1}" is hex string form * of the SHA-1 hash of the identifier part of the versionable node's key. * <ul> * <li>The flat algorithm just returns the path <code>/jcr:system/jcr:versionStorage/{sha1}</code>. For example, given a node * key with an identifier part of "fae2b929-c5ef-4ce5-9fa1-514779ca0ae3", the SHA-1 hash of the identifier is * "b46dde8905f76361779339fa3ccacc4f47664255", so the path to the node's version history would be * <code>/jcr:system/jcr:versionStorage/b46dde8905f76361779339fa3ccacc4f47664255</code>.</li> * <li>The hierarchical algorithm creates a hiearchical path based upon the first 6 characters of the "{sha1}" hash: * <code>/jcr:system/jcr:versionStorage/{part1}/{part2}/{part3}/{part4}</code>, where "{part1}" consists of the 1st and 2nd * hex characters of the "{sha1}" string, "{part2}" consists of the 3rd and 4th hex characters of the "{sha1}" string, * "{part3}" consists of the 5th and 6th characters of the "{sha1}" string, "{part4}" consists of the remaining characters. * For example, given a node key with an identifier part of "fae2b929-c5ef-4ce5-9fa1-514779ca0ae3", the SHA-1 hash of the * identifier is "b46dde8905f76361779339fa3ccacc4f47664255", so the path to the node's version history would be * <code>/jcr:system/jcr:versionStorage/b4/6d/de/298905f76361779339fa3ccacc4f47664255</code>.</li> * </ul> * </p> * * @param key the key for the node for which the path to the version history should be returned * @return the path to the version history node that corresponds to the node with the given key. This does not guarantee that * a node exists at the returned path. In fact, this method will return null for every node that is and has never been * versionable, or every node that is versionable but not checked in. */ Path versionHistoryPathFor( NodeKey key ) { return versionHistoryPathAlgorithm.versionHistoryPathFor(key.getIdentifierHash()); } @Override public JcrVersionHistoryNode getVersionHistory( String absPath ) throws RepositoryException { return getVersionHistory(session.getNode(absPath)); } JcrVersionHistoryNode getVersionHistory( AbstractJcrNode node ) throws RepositoryException { checkVersionable(node); // Try to look up the version history by its key ... NodeKey historyKey = readableSystem.versionHistoryNodeKeyFor(node.key()); SessionCache cache = session.cache(); CachedNode historyNode = cache.getNode(historyKey); if (historyNode != null) { return (JcrVersionHistoryNode)session.node(historyNode, Type.VERSION_HISTORY); } // Per Section 15.1: // "Under both simple and full versioning, on persist of a new versionable node N that neither corresponds // nor shares with an existing node: // - The jcr:isCheckedOut property of N is set to true and // - A new VersionHistory (H) is created for N. H contains one Version, the root version (V0) // (see §3.13.5.2 Root Version)." // // This means that the version history should not be created until save is performed. This makes sense, // because otherwise the version history would be persisted for a newly-created node, even though that node // is not yet persisted. Tests with the reference implementation (see sandbox) verified this behavior. // // If the node is new, then we'll throw an exception if (node.isNew()) { String msg = JcrI18n.noVersionHistoryForTransientVersionableNodes.text(node.location()); throw new InvalidItemStateException(msg); } // Get the cached node and see if the 'mix:versionable' mixin was added transiently ... CachedNode cachedNode = node.node(); if (cachedNode instanceof MutableCachedNode) { // There are at least some changes. See if the node is newly versionable ... MutableCachedNode mutable = (MutableCachedNode)cachedNode; NodeTypes nodeTypeCapabilities = repository().nodeTypeManager().getNodeTypes(); Name primaryType = mutable.getPrimaryType(cache); Set<Name> mixinTypes = mutable.getAddedMixins(cache); if (nodeTypeCapabilities.isVersionable(primaryType, mixinTypes)) { // We don't create the verison history until the versionable state is persisted ... String msg = JcrI18n.versionHistoryForNewlyVersionableNodesNotAvailableUntilSave.text(node.location()); throw new UnsupportedRepositoryOperationException(msg); } } // Otherwise the node IS versionable and we need to initialize the version history ... initializeVersionHistoryFor(node, historyKey, cache); // Look up the history node again, using this session ... historyNode = cache.getNode(historyKey); return (JcrVersionHistoryNode)session.node(historyNode, Type.VERSION_HISTORY); } private void initializeVersionHistoryFor( AbstractJcrNode node, NodeKey historyKey, SessionCache cache ) throws RepositoryException { SystemContent content = new SystemContent(session.createSystemCache(false)); CachedNode cachedNode = node.node(); Name primaryTypeName = cachedNode.getPrimaryType(cache); Set<Name> mixinTypeNames = cachedNode.getMixinTypes(cache); NodeKey versionedKey = cachedNode.getKey(); Path versionHistoryPath = versionHistoryPathFor(versionedKey); DateTime now = session().dateFactory().create(); content.initializeVersionStorage(versionedKey, historyKey, null, primaryTypeName, mixinTypeNames, versionHistoryPath, null, now); content.save(); } /** * Throw an {@link UnsupportedRepositoryOperationException} if the node is not versionable (i.e., * isNodeType(JcrMixLexicon.VERSIONABLE) == false). * * @param node the node to check * @throws UnsupportedRepositoryOperationException if <code>!isNodeType({@link JcrMixLexicon#VERSIONABLE})</code> * @throws RepositoryException if an error occurs reading the node types for this node */ private void checkVersionable( AbstractJcrNode node ) throws UnsupportedRepositoryOperationException, RepositoryException { if (!node.isNodeType(JcrMixLexicon.VERSIONABLE)) { throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text()); } } @Override public Version getBaseVersion( String absPath ) throws UnsupportedRepositoryOperationException, RepositoryException { return session.getNode(absPath).getBaseVersion(); } @Override public boolean isCheckedOut( String absPath ) throws RepositoryException { return session.getNode(absPath).isCheckedOut(); } @Override public Version checkin( String absPath ) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.checkin('{0}')", absPath); return checkin(session.getNode(absPath)); } /** * Checks in the given node, creating (and returning) a new {@link Version}. * * @param node the node to be checked in * @return the {@link Version} object created as a result of this checkin * @throws RepositoryException if an error occurs during the checkin. See {@link javax.jcr.Node#checkin()} for a full * description of the possible error conditions. * @see #checkin(String) * @see AbstractJcrNode#checkin() */ JcrVersionNode checkin( AbstractJcrNode node ) throws RepositoryException { checkVersionable(node); if (node.isNew() || node.isModified()) { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); } // Check this separately since it throws a different type of exception if (node.isLocked() && !node.holdsLock()) { throw new LockException(JcrI18n.lockTokenNotHeld.text(node.getPath())); } if (node.getProperty(JcrLexicon.MERGE_FAILED) != null) { throw new VersionException(JcrI18n.pendingMergeConflicts.text(node.getPath())); } javax.jcr.Property isCheckedOut = node.getProperty(JcrLexicon.IS_CHECKED_OUT); if (!isCheckedOut.getBoolean()) { return node.getBaseVersion(); } // Collect some of the information about the node that we'll need ... SessionCache cache = cache(); NodeKey versionedKey = node.key(); Path versionHistoryPath = versionHistoryPathFor(versionedKey); CachedNode cachedNode = node.node(); DateTime now = session().dateFactory().create(); // Create the system content that we'll use to update the system branch ... SessionCache systemSession = session.createSystemCache(false); SystemContent systemContent = new SystemContent(systemSession); MutableCachedNode version = null; try { // Create a new version in the history for this node; this initializes the version history if it is missing ... List<Property> versionableProps = new ArrayList<Property>(); addVersionedPropertiesFor(node, false, versionableProps); AtomicReference<MutableCachedNode> frozen = new AtomicReference<MutableCachedNode>(); version = systemContent.recordNewVersion(cachedNode, cache, versionHistoryPath, null, versionableProps, now, frozen); NodeKey historyKey = version.getParentKey(systemSession); // Update the node's 'mix:versionable' properties, using a new session ... SessionCache versionSession = session.spawnSessionCache(false); MutableCachedNode versionableNode = versionSession.mutable(versionedKey); PropertyFactory props = propertyFactory(); ReferenceFactory refFactory = session.referenceFactory(); Reference historyRef = refFactory.create(historyKey, true); Reference baseVersionRef = refFactory.create(version.getKey(), true); versionableNode.setProperty(versionSession, props.create(JcrLexicon.VERSION_HISTORY, historyRef)); versionableNode.setProperty(versionSession, props.create(JcrLexicon.BASE_VERSION, baseVersionRef)); versionableNode.setProperty(versionSession, props.create(JcrLexicon.IS_CHECKED_OUT, Boolean.FALSE)); // The 'jcr:predecessors' set to an empty array, per Section 15.2 in JSR-283 versionableNode.setProperty(versionSession, props.create(JcrLexicon.PREDECESSORS, new Object[] {})); // Now process the children of the versionable node, and add them under the frozen node ... MutableCachedNode frozenNode = frozen.get(); for (ChildReference childRef : versionableNode.getChildReferences(versionSession)) { AbstractJcrNode child = session.node(childRef.getKey(), null, versionedKey); versionNodeAt(child, childRef.getName(), frozenNode, false, versionSession, systemSession); } // Now save all of the changes. // the system session must be saved first so that its nodes are cleared first from the shared ws cache // this is required because there are references from the other session pointing towards the system session systemSession.save(versionSession, null); } finally { // TODO: Versioning: may want to catch this block and retry, if the new version name couldn't be created } // return the version node from the system cache, not this session's cache because this session is asynchronously // notified of system ws changes and may not always get the latest data immediately return (JcrVersionNode)session.node(version, systemSession, Type.VERSION, null); } /** * Create a version record for the given node under the given parent path with the given batch. * * @param node the node for which the frozen version record should be created * @param nodeName the name of the node which we're versioning * @param parentInVersionHistory the node in the version history under which the frozen version should be recorded * @param forceCopy true if the OPV should be ignored and a COPY is to be performed, or false if the OPV should be used * @param nodeCache the session cache used to access the node information; may not be null * @param versionHistoryCache the session cache used to create nodes in the version history; may not be null @throws RepositoryException if an error occurs accessing the repository */ @SuppressWarnings( "fallthrough" ) private void versionNodeAt( AbstractJcrNode node, Name nodeName, MutableCachedNode parentInVersionHistory, boolean forceCopy, SessionCache nodeCache, SessionCache versionHistoryCache ) throws RepositoryException { int onParentVersion = 0; if (forceCopy) { onParentVersion = OnParentVersionAction.COPY; } else { onParentVersion = node.getDefinition().getOnParentVersion(); } NodeKey key = parentInVersionHistory.getKey().withRandomId(); switch (onParentVersion) { case OnParentVersionAction.ABORT: throw new VersionException(JcrI18n.cannotCheckinNodeWithAbortChildNode.text(nodeName, node.getParent().getName())); case OnParentVersionAction.VERSION: if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) { // The frozen node should reference the version history of the node ... JcrVersionHistoryNode history = node.getVersionHistory(); org.modeshape.jcr.value.Property primaryType = propertyFactory().create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSIONED_CHILD); Reference childVersionHistoryValue = session.referenceFactory().create(history.key(), true); org.modeshape.jcr.value.Property childVersionHistory = propertyFactory().create( JcrLexicon.CHILD_VERSION_HISTORY, childVersionHistoryValue); parentInVersionHistory.createChild(versionHistoryCache, key, nodeName, primaryType, childVersionHistory); return; } // Otherwise, treat it as a copy, as per section 3.13.9 bullet item 5 in JSR-283, so DO NOT break ... case OnParentVersionAction.COPY: // Per section 3.13.9 item 5 in JSR-283, an OPV of COPY or VERSION (if not versionable) // results in COPY behavior "regardless of the OPV values of the sub-items". // We can achieve this by making the onParentVersionAction always COPY for the // recursive call ... forceCopy = true; PropertyFactory factory = propertyFactory(); List<Property> props = new LinkedList<Property>(); if (node.isShared()) { // This is a shared node, so we should store a proxy to the shareable node ... props.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE)); props.add(factory.create(JcrLexicon.FROZEN_PRIMARY_TYPE, ModeShapeLexicon.SHARE)); props.add(factory.create(JcrLexicon.FROZEN_UUID, node.getIdentifier())); props.add(factory.create(JcrLexicon.UUID, key)); parentInVersionHistory.createChild(versionHistoryCache, key, nodeName, props); // The proxies to shareable nodes never have children (nor versionable properties), so we're done ... return; } // But the copy needs to be a 'nt:frozenNode', so that it doesn't compete with the actual node // (outside of version history) ... Name primaryTypeName = node.getPrimaryTypeName(); Set<Name> mixinTypeNames = node.getMixinTypeNames(); props.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE)); props.add(factory.create(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName)); props.add(factory.create(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames)); props.add(factory.create(JcrLexicon.FROZEN_UUID, node.getIdentifier())); props.add(factory.create(JcrLexicon.UUID, key)); addVersionedPropertiesFor(node, forceCopy, props); MutableCachedNode newCopy = parentInVersionHistory.createChild(versionHistoryCache, key, nodeName, props); // Now process the children of the versionable node ... NodeKey parentKey = node.key(); for (ChildReference childRef : node.node().getChildReferences(nodeCache)) { AbstractJcrNode child = session.node(childRef.getKey(), null, parentKey); versionNodeAt(child, childRef.getName(), newCopy, forceCopy, nodeCache, versionHistoryCache); } return; case OnParentVersionAction.INITIALIZE: case OnParentVersionAction.COMPUTE: case OnParentVersionAction.IGNORE: // Do nothing for these. No built-in types require initialize or compute for child nodes. return; default: throw new IllegalStateException("Unexpected value: " + onParentVersion); } } /** * @param node the node for which the properties should be versioned * @param forceCopy true if all of the properties should be copied, regardless of the property's OPV setting * @param props the collection in which should be added the versioned properties for {@code node} (i.e., the properties to add * the the frozen version of {@code node}) * @throws RepositoryException if an error occurs accessing the repository */ private void addVersionedPropertiesFor( AbstractJcrNode node, boolean forceCopy, List<Property> props ) throws RepositoryException { for (PropertyIterator iter = node.getProperties(); iter.hasNext();) { AbstractJcrProperty property = (AbstractJcrProperty)iter.nextProperty(); // We want to skip the actual primary type, mixin types, and uuid since those are handled above ... Name name = property.name(); if (JcrLexicon.PRIMARY_TYPE.equals(name)) continue; if (JcrLexicon.MIXIN_TYPES.equals(name)) continue; if (JcrLexicon.UUID.equals(name)) continue; Property prop = property.property(); if (forceCopy) { props.add(prop); } else { JcrPropertyDefinition propDefn = property.propertyDefinition(); switch (propDefn.getOnParentVersion()) { case OnParentVersionAction.ABORT: I18n msg = JcrI18n.cannotCheckinNodeWithAbortProperty; throw new VersionException(msg.text(property.getName(), node.getName())); case OnParentVersionAction.COPY: case OnParentVersionAction.VERSION: props.add(prop); break; case OnParentVersionAction.INITIALIZE: case OnParentVersionAction.COMPUTE: case OnParentVersionAction.IGNORE: // Do nothing for these } } } } @Override public void checkout( String absPath ) throws LockException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.checkout('{0}')", absPath); checkout(session.getNode(absPath)); } /** * Checks out the given node, updating version-related properties on the node as needed. * * @param node the node to be checked out * @throws LockException if a lock prevents the node from being checked out * @throws RepositoryException if an error occurs during the checkout. See {@link javax.jcr.Node#checkout()} for a full * description of the possible error conditions. */ void checkout( AbstractJcrNode node ) throws LockException, RepositoryException { checkVersionable(node); // Check this separately since it throws a different type of exception if (node.isLocked() && !node.holdsLock()) { throw new LockException(JcrI18n.lockTokenNotHeld.text(node.getPath())); } if (!node.hasProperty(JcrLexicon.BASE_VERSION)) { // This happens when we've added mix:versionable, but not saved it to create the base // version (and the rest of the version storage graph). See MODE-704. return; } // Checking out an already checked-out node is supposed to return silently if (node.getProperty(JcrLexicon.IS_CHECKED_OUT).getBoolean()) { return; } // Create a session that we'll used to change the node ... SessionCache versionSession = session.spawnSessionCache(false); MutableCachedNode versionable = versionSession.mutable(node.key()); NodeKey baseVersionKey = node.getBaseVersion().key(); PropertyFactory props = propertyFactory(); Reference baseVersionRef = session.referenceFactory().create(baseVersionKey, true); versionable.setProperty(versionSession, props.create(JcrLexicon.PREDECESSORS, new Object[] {baseVersionRef})); versionable.setProperty(versionSession, props.create(JcrLexicon.IS_CHECKED_OUT, Boolean.TRUE)); versionSession.save(); } @Override public Version checkpoint( String absPath ) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { Version version = checkin(absPath); checkout(absPath); return version; } protected static interface PathAlgorithm { Path versionHistoryPathFor( String sha1OrUuid ); } protected static abstract class BasePathAlgorithm implements PathAlgorithm { protected final PathFactory paths; protected final NameFactory names; protected final Path versionStoragePath; protected BasePathAlgorithm( Path versionStoragePath, ExecutionContext context ) { this.paths = context.getValueFactories().getPathFactory(); this.names = context.getValueFactories().getNameFactory(); this.versionStoragePath = versionStoragePath; } } protected static class HiearchicalPathAlgorithm extends BasePathAlgorithm { protected HiearchicalPathAlgorithm( Path versionStoragePath, ExecutionContext context ) { super(versionStoragePath, context); } @Override public Path versionHistoryPathFor( String sha1OrUuid ) { Name p1 = names.create(sha1OrUuid.substring(0, 2)); Name p2 = names.create(sha1OrUuid.substring(2, 4)); Name p3 = names.create(sha1OrUuid.substring(4, 6)); Name p4 = names.create(sha1OrUuid); return paths.createAbsolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, p1, p2, p3, p4); } } protected static class FlatPathAlgorithm extends BasePathAlgorithm { protected FlatPathAlgorithm( Path versionStoragePath, ExecutionContext context ) { super(versionStoragePath, context); } @Override public Path versionHistoryPathFor( String sha1OrUuid ) { return paths.createAbsolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, names.create(sha1OrUuid)); } } @Override public void restore( Version[] versions, boolean removeExisting ) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.restore({0},{1})", versions, removeExisting); validateSessionLiveWithoutPendingChanges(); // Create a new session in which we'll perform the restore, so this session remains thread-safe ... JcrSession restoreSession = session.spawnSession(false); Map<JcrVersionNode, AbstractJcrNode> existingVersions = new HashMap<JcrVersionNode, AbstractJcrNode>(versions.length); Set<Path> versionRootPaths = new HashSet<Path>(versions.length); List<Version> nonExistingVersions = new ArrayList<Version>(versions.length); for (int i = 0; i < versions.length; i++) { VersionHistory history = versions[i].getContainingHistory(); if (history.getRootVersion().isSame(versions[i])) { throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(versions[i].getPath())); } try { AbstractJcrNode existingNode = restoreSession.getNonSystemNodeByIdentifier(history.getVersionableIdentifier()); existingVersions.put((JcrVersionNode)versions[i], existingNode); versionRootPaths.add(existingNode.path()); } catch (ItemNotFoundException infe) { nonExistingVersions.add(versions[i]); } } if (existingVersions.isEmpty()) { throw new VersionException(JcrI18n.noExistingVersionForRestore.text()); } // Now create and run the restore operation ... RestoreCommand op = new RestoreCommand(restoreSession, existingVersions, versionRootPaths, nonExistingVersions, null, removeExisting); op.execute(); restoreSession.save(); } @Override public void restore( String absPath, String versionName, boolean removeExisting ) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.restore('{0}','{1}',{2})", absPath, versionName, removeExisting); validateSessionLiveWithoutPendingChanges(); // Create a new session in which we'll finish the restore, so this session remains thread-safe ... JcrSession restoreSession = session.spawnSession(false); Version version = null; // See if the node at absPath exists and has version storage. Path path = restoreSession.absolutePathFor(absPath); AbstractJcrNode existingNode = restoreSession.node(path); VersionHistory historyNode = existingNode.getVersionHistory(); version = historyNode.getVersion(versionName); assert version != null; restore(restoreSession, path, version, null, removeExisting); } private void validateSessionLiveWithoutPendingChanges() throws RepositoryException { session.checkLive(); if (session.hasPendingChanges()) { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); } } @Override public void restore( Version version, boolean removeExisting ) throws VersionException, ItemExistsException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.restore({0},{1})", version, removeExisting); validateSessionLiveWithoutPendingChanges(); // Create a new session in which we'll finish the restore, so this session remains thread-safe ... JcrSession restoreSession = session.spawnSession(false); String identifier = version.getContainingHistory().getVersionableIdentifier(); AbstractJcrNode node = null; try { node = restoreSession.getNonSystemNodeByIdentifier(identifier); } catch (ItemNotFoundException e) { throw new VersionException(JcrI18n.invalidVersionForRestore.text()); } Path path = node.path(); restore(restoreSession, path, version, null, removeExisting); } @Override public void restore( String absPath, Version version, boolean removeExisting ) throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.restore('{0}',{1},{2})", absPath, version, removeExisting); restoreAtAbsPath(absPath, version, removeExisting, true); } protected void restoreAtAbsPath( String absPath, Version version, boolean removeExisting, boolean failIfNodeAlreadyExists ) throws RepositoryException { validateSessionLiveWithoutPendingChanges(); // Create a new session in which we'll finish the restore, so this session remains thread-safe ... JcrSession restoreSession = session.spawnSession(false); Path path = restoreSession.absolutePathFor(absPath); if (failIfNodeAlreadyExists) { try { AbstractJcrNode existingNode = restoreSession.node(path); String msg = JcrI18n.unableToRestoreAtAbsPathNodeAlreadyExists.text(absPath, existingNode.key()); throw new VersionException(msg); } catch (PathNotFoundException e) { // expected } } restore(restoreSession, path, version, null, removeExisting); } @Override public void restoreByLabel( String absPath, String versionLabel, boolean removeExisting ) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.restoreByLabel('{0}','{1}',{2})", absPath, versionLabel, removeExisting); validateSessionLiveWithoutPendingChanges(); // Create a new session in which we'll finish the restore, so this session remains thread-safe ... JcrSession restoreSession = session.spawnSession(false); restoreSession.getNode(absPath).restoreByLabel(versionLabel, removeExisting); } @Override public NodeIterator merge( String absPath, String srcWorkspace, boolean bestEffort ) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { return merge(absPath, srcWorkspace, bestEffort, false); } @Override public NodeIterator merge( String absPath, String srcWorkspace, boolean bestEffort, boolean isShallow ) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.merge('{0}','{1}',{2})", absPath, srcWorkspace, bestEffort); CheckArg.isNotNull(srcWorkspace, "source workspace name"); // Create a new session in which we'll finish the merge, so this session remains thread-safe ... JcrSession mergeSession = session.spawnSession(false); AbstractJcrNode node = mergeSession.getNode(absPath); return merge(node, srcWorkspace, bestEffort, isShallow); } @Override public void doneMerge( String absPath, Version version ) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.doneMerge('{0}',{1})", absPath, version); // Create a new session in which we'll finish the merge, so this session remains thread-safe ... JcrSession mergeSession = session.spawnSession(false); doneMerge(mergeSession.getNode(absPath), version); } @Override public void cancelMerge( String absPath, Version version ) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { // Create a new session in which we'll perform the cancel, so this session remains thread-safe ... JcrSession cancelSession = session.spawnSession(false); cancelMerge(cancelSession.getNode(absPath), version); } /** * Restores the given version to the given path. * * @param session the session that should be used; may not be null * @param path the path at which the version should be restored; may not be null * @param version the version to restore; may not be null * @param labelToRestore the label that was used to identify the version; may be null * @param removeExisting if UUID conflicts resulting from this restore should cause the conflicting node to be removed or an * exception to be thrown and the operation to fail * @throws RepositoryException if an error occurs accessing the repository * @see javax.jcr.Node#restore(Version, String, boolean) * @see javax.jcr.Node#restoreByLabel(String, boolean) */ void restore( JcrSession session, Path path, Version version, String labelToRestore, boolean removeExisting ) throws RepositoryException { // Ensure that the parent node exists - this will throw a PNFE if no node exists at that path AbstractJcrNode parentNode = session.node(path.getParent()); AbstractJcrNode existingNode = null; AbstractJcrNode nodeToCheckLock; JcrVersionNode jcrVersion = (JcrVersionNode)version; SessionCache cache = session.cache(); PropertyFactory propFactory = session.propertyFactory(); try { existingNode = parentNode.childNode(path.getLastSegment(), null); nodeToCheckLock = existingNode; // These checks only make sense if there is an existing node JcrVersionHistoryNode versionHistory = existingNode.getVersionHistory(); if (!versionHistory.isSame(jcrVersion.getParent())) { throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), versionHistory.getPath())); } if (!versionHistory.isSame(existingNode.getVersionHistory())) { throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), existingNode.getVersionHistory() .getPath())); } if (jcrVersion.isSame(versionHistory.getRootVersion())) { throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(existingNode.getPath())); } } catch (PathNotFoundException pnfe) { // This is allowable, but the node needs to be checked out if (!parentNode.isCheckedOut()) { String parentPath = path.getString(session.context().getNamespaceRegistry()); throw new VersionException(JcrI18n.nodeIsCheckedIn.text(parentPath)); } AbstractJcrNode sourceNode = session.workspace().getVersionManager().frozenNodeFor(version); Name primaryTypeName = session.nameFactory().create(sourceNode.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE) .property() .getFirstValue()); AbstractJcrProperty uuidProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID); String frozenUuidString = session.stringFactory().create(uuidProp.property().getFirstValue()); NodeKey desiredKey = parentNode.key().withId(frozenUuidString); Name name = path.getLastSegment().getName(); if (ModeShapeLexicon.SHARE.equals(primaryTypeName)) { // Need to link to the existing node with the identifier ... parentNode.mutable().linkChild(cache, desiredKey, name); existingNode = session.node(desiredKey, (Type)null, parentNode.key()); } else { // Otherwise recreate/restore the new child ... Property primaryType = propFactory.create(JcrLexicon.PRIMARY_TYPE, primaryTypeName); MutableCachedNode newChild = parentNode.mutable().createChild(cache, desiredKey, name, primaryType); existingNode = session.node(newChild, (Type)null, parentNode.key()); } nodeToCheckLock = parentNode; } // Check whether the node to check is locked if (nodeToCheckLock.isLocked() && !nodeToCheckLock.holdsLock()) { throw new LockException(JcrI18n.lockTokenNotHeld.text(nodeToCheckLock.getPath())); } RestoreCommand op = new RestoreCommand(session, Collections.singletonMap(jcrVersion, existingNode), Collections.singleton(existingNode.path()), Collections.<Version>emptySet(), labelToRestore, removeExisting); op.execute(); session.save(); } protected final void clearCheckoutStatus( MutableCachedNode node, NodeKey baseVersion, SessionCache cache, PropertyFactory propFactory ) { Reference baseVersionRef = session.referenceFactory().create(baseVersion); node.setProperty(cache, propFactory.create(JcrLexicon.IS_CHECKED_OUT, Boolean.FALSE)); node.setProperty(cache, propFactory.create(JcrLexicon.BASE_VERSION, baseVersionRef)); } /** * @param version the version for which the frozen node should be returned * @return the frozen node for the given version * @throws RepositoryException if an error occurs accessing the repository */ AbstractJcrNode frozenNodeFor( Version version ) throws RepositoryException { return ((AbstractJcrNode)version).getNode(JcrLexicon.FROZEN_NODE); } NodeIterator merge( AbstractJcrNode targetNode, String srcWorkspace, boolean bestEffort, boolean isShallow ) throws RepositoryException { targetNode.session().checkLive(); if (session().hasPendingChanges()) { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); } try { targetNode.correspondingNodePath(srcWorkspace); } catch (ItemNotFoundException infe) { // return immediately if no corresponding node exists in that workspace return JcrEmptyNodeIterator.INSTANCE; } JcrSession sourceSession = targetNode.session().spawnSession(srcWorkspace, true); MergeCommand op = new MergeCommand(targetNode, sourceSession, bestEffort, isShallow); op.execute(); targetNode.session().save(); return op.getFailures(); } void doneMerge( AbstractJcrNode targetNode, Version version ) throws RepositoryException { targetNode.session().checkLive(); checkVersionable(targetNode); if (targetNode.isNew() || targetNode.isModified()) { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowedForNode.text()); } if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { throw new VersionException(JcrI18n.requiresVersionable.text()); } AbstractJcrProperty prop = targetNode.getProperty(JcrLexicon.PREDECESSORS); JcrValue[] values = prop.getValues(); JcrValue[] newValues = new JcrValue[values.length + 1]; System.arraycopy(values, 0, newValues, 0, values.length); newValues[values.length] = targetNode.valueFrom(version); targetNode.setProperty(JcrLexicon.PREDECESSORS, newValues, PropertyType.REFERENCE, false); removeVersionFromMergeFailedProperty(targetNode, version); targetNode.session().save(); } void cancelMerge( AbstractJcrNode targetNode, Version version ) throws RepositoryException { targetNode.session().checkLive(); checkVersionable(targetNode); if (targetNode.isNew() || targetNode.isModified()) { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowedForNode.text()); } if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text()); } removeVersionFromMergeFailedProperty(targetNode, version); targetNode.session().save(); } @SuppressWarnings( "deprecation" ) private void removeVersionFromMergeFailedProperty( AbstractJcrNode targetNode, Version version ) throws RepositoryException { if (!targetNode.hasProperty(JcrLexicon.MERGE_FAILED)) { throw new VersionException(JcrI18n.versionNotInMergeFailed.text(version.getName(), targetNode.getPath())); } AbstractJcrProperty prop = targetNode.getProperty(JcrLexicon.MERGE_FAILED); Value[] values = prop.getValues(); List<Value> newValues = new ArrayList<Value>(); String uuidString = version.getUUID(); int matchIndex = -1; for (int i = 0; i < values.length; i++) { if (uuidString.equals(values[i].getString())) { matchIndex = i; } else { newValues.add(values[i]); } } if (matchIndex == -1) { throw new VersionException(JcrI18n.versionNotInMergeFailed.text(version.getName(), targetNode.getPath())); } if (newValues.isEmpty()) { // remove the property without looking at the node's "checked out" status targetNode.removeProperty(prop); } else { ((JcrMultiValueProperty)prop).internalSetValue(newValues.toArray(new Value[newValues.size()])); } } @Override public Node createConfiguration( String absPath ) throws UnsupportedRepositoryOperationException, RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public Node setActivity( Node activity ) throws UnsupportedRepositoryOperationException, RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public Node getActivity() throws UnsupportedRepositoryOperationException, RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public Node createActivity( String title ) throws UnsupportedRepositoryOperationException, RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public void removeActivity( Node activityNode ) throws UnsupportedRepositoryOperationException, /*VersionException,*/RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public NodeIterator merge( Node activityNode ) throws /*VersionException, AccessDeniedException, MergeException, LockException, InvalidItemStateException,*/ RepositoryException { throw new UnsupportedRepositoryOperationException(); } @Override public void remove( String absPath ) throws UnsupportedOperationException, PathNotFoundException, VersionException, RepositoryException { if (LOGGER.isDebugEnabled()) LOGGER.debug("VersionManager.remove('{0}')", absPath); JcrSession removeSession = session.spawnSession(false); AbstractJcrNode node = removeSession.getNode(absPath); checkVersionable(node); SessionCache systemCache = session.createSystemCache(false); removeHistories(node, systemCache); node.remove(); removeSession.cache().save(systemCache, null); } private void removeHistories( AbstractJcrNode node, SessionCache systemSession ) throws RepositoryException { JcrVersionHistoryNode versionHistory = null; if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) { if (node.isShareable()) { throw new UnsupportedRepositoryOperationException(JcrI18n.nodeIsShareable.text(node.getPath())); } versionHistory = getVersionHistory(node); if (versionHistory.getAllVersions().getSize() > 1) { throw new UnsupportedRepositoryOperationException(JcrI18n.versionHistoryNotEmpty.text(node.getPath())); } } NodeIterator nodeIterator = node.getNodesInternal(); while (nodeIterator.hasNext()) { removeHistories((AbstractJcrNode)nodeIterator.nextNode(), systemSession); } if (versionHistory != null) { Set<NodeKey> strongReferences = versionHistory.node().getReferrers(systemSession, CachedNode.ReferenceType.STRONG); // remove all incoming strong references from nt:versionedChild nodes, otherwise the version history cannot be removed strongReferences.stream() .map(systemSession::getNode) .filter(referrer -> JcrNtLexicon.VERSIONED_CHILD.equals(referrer.getPrimaryType(systemSession))) .forEach(versionedChild -> { NodeKey versionedChildParentKey = versionedChild.getParentKey(systemSession); NodeKey versionedChildKey = versionedChild.getKey(); MutableCachedNode mutableFrozenNode = systemSession.mutable(versionedChildParentKey); mutableFrozenNode.removeChild(systemSession, versionedChildKey); systemSession.destroy(versionedChildKey); }); MutableCachedNode historyParent = systemSession.mutable(versionHistory.parentKey()); historyParent.removeChild(systemSession, versionHistory.key()); systemSession.destroy(versionHistory.key()); } } @NotThreadSafe private class RestoreCommand { private final JcrSession session; private final SessionCache cache; private final PropertyFactory propFactory; private Map<JcrVersionNode, AbstractJcrNode> existingVersions; private Set<Path> versionRootPaths; private Collection<Version> nonExistingVersions; private boolean removeExisting; private String labelToRestore; private Map<AbstractJcrNode, AbstractJcrNode> changedNodes; public RestoreCommand( JcrSession session, Map<JcrVersionNode, AbstractJcrNode> existingVersions, Set<Path> versionRootPaths, Collection<Version> nonExistingVersions, String labelToRestore, boolean removeExisting ) { this.session = session; this.cache = session.cache(); this.propFactory = session.propertyFactory(); this.existingVersions = existingVersions; this.versionRootPaths = versionRootPaths; this.nonExistingVersions = nonExistingVersions; this.removeExisting = removeExisting; this.labelToRestore = labelToRestore; // The default size for a HashMap is pretty low and this could get big fast this.changedNodes = new HashMap<AbstractJcrNode, AbstractJcrNode>(100); } final String string( Object value ) { return session.stringFactory().create(value); } final Name name( Object value ) { return session.nameFactory().create(value); } final DateTime date( Calendar value ) { return session.dateFactory().create(value); } void execute() throws RepositoryException { Collection<JcrVersionNode> versionsToCheck = new ArrayList<JcrVersionNode>(existingVersions.keySet()); JcrVersionManager versionManager = session.workspace().getVersionManager(); for (JcrVersionNode version : versionsToCheck) { AbstractJcrNode root = existingVersions.get(version); // This can happen if the version was already restored in another node if (root == null) continue; // This updates the changedNodes and nonExistingVersions fields as a side effect AbstractJcrNode frozenNode = versionManager.frozenNodeFor(version); MutableCachedNode mutableRoot = root.mutable(); restoreNodeMixins(frozenNode.node(), mutableRoot, cache); restoreNode(frozenNode, root, date(version.getCreated())); clearCheckoutStatus(mutableRoot, version.key(), cache, propFactory); } if (!nonExistingVersions.isEmpty()) { StringBuilder versions = new StringBuilder(); boolean first = true; for (Version version : nonExistingVersions) { if (!first) { versions.append(", "); } else { first = false; } versions.append(version.getName()); } throw new VersionException(JcrI18n.unrootedVersionsInRestore.text(versions.toString())); } for (Map.Entry<AbstractJcrNode, AbstractJcrNode> changedNode : changedNodes.entrySet()) { restoreProperties(changedNode.getKey(), changedNode.getValue()); } } /** * Restores the child nodes and mixin types for {@code targetNode} based on the frozen version stored at * {@code sourceNode}. This method will remove and add child nodes as necessary based on the documentation in the JCR 2.0 * specification (sections 15.7), but this method will not modify properties (other than jcr:mixinTypes, jcr:baseVersion, * and jcr:isCheckedOut). * * @param sourceNode a node in the subgraph of frozen nodes under a version; may not be null, but may be a node with * primary type of nt:version or nt:versionedChild * @param targetNode the node to be updated based on {@code sourceNode}; may not be null * @param checkinTime the time at which the version that instigated this restore was checked in; may not be null * @throws RepositoryException if an error occurs accessing the repository */ private void restoreNode( AbstractJcrNode sourceNode, AbstractJcrNode targetNode, DateTime checkinTime ) throws RepositoryException { changedNodes.put(sourceNode, targetNode); MutableCachedNode target = targetNode.mutable(); CachedNode source = sourceNode.node(); Set<CachedNode> versionedChildrenThatShouldNotBeRestored = new HashSet<CachedNode>(); // Try to match the existing nodes with nodes from the version to be restored Map<NodeKey, CachedNode> presentInBoth = new HashMap<NodeKey, CachedNode>(); // Start with all target children in this set and pull them out as matches are found List<NodeKey> inTargetOnly = asList(target.getChildReferences(cache)); // Start with no source children in this set, but add them in when no match is found Map<CachedNode, CachedNode> inSourceOnly = new HashMap<CachedNode, CachedNode>(); // Map the source children to existing target children where possible for (ChildReference sourceChild : source.getChildReferences(cache)) { CachedNode child = cache.getNode(sourceChild); Name primaryTypeName = name(child.getPrimaryType(cache)); CachedNode resolvedFrozenNode = resolveSourceNode(child, checkinTime, cache); CachedNode match = findMatchFor(resolvedFrozenNode, cache); if (match != null) { if (JcrNtLexicon.VERSIONED_CHILD.equals(primaryTypeName)) { // This is a versioned child ... if (!removeExisting) { throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(match.getKey(), session.workspace().getName(), match.getPath(cache))); } // use match directly versionedChildrenThatShouldNotBeRestored.add(match); } inTargetOnly.remove(match.getKey()); presentInBoth.put(child.getKey(), match); } else { inSourceOnly.put(child, resolvedFrozenNode); } } // Remove all the extraneous children of the target node for (NodeKey childKey : inTargetOnly) { AbstractJcrNode child = session.node(childKey, null); switch (child.getDefinition().getOnParentVersion()) { case OnParentVersionAction.ABORT: case OnParentVersionAction.VERSION: case OnParentVersionAction.COPY: // The next call *might* remove some children below "child" which are also present on the source, but // higher in the hierarchy child.doRemove(); // Otherwise we're going to reuse the existing node break; case OnParentVersionAction.COMPUTE: // Technically, this should reinitialize the node per its defaults. case OnParentVersionAction.INITIALIZE: case OnParentVersionAction.IGNORE: // Do nothing } } LinkedList<ChildReference> reversedChildren = new LinkedList<ChildReference>(); for (ChildReference sourceChildRef : source.getChildReferences(cache)) { reversedChildren.addFirst(sourceChildRef); } // Now walk through the source node children (in reversed order), inserting children as needed // The order is reversed because SessionCache$NodeEditor supports orderBefore, but not orderAfter NodeKey prevChildKey = null; for (ChildReference sourceChildRef : reversedChildren) { CachedNode sourceChild = cache.getNode(sourceChildRef); CachedNode targetChild = presentInBoth.get(sourceChildRef.getKey()); CachedNode resolvedChild = null; Name resolvedPrimaryTypeName = null; AbstractJcrNode sourceChildNode = null; AbstractJcrNode targetChildNode = null; Property frozenPrimaryType = sourceChild.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE, cache); Name sourceFrozenPrimaryType = frozenPrimaryType != null ? name(frozenPrimaryType.getFirstValue()) : null; boolean isShared = ModeShapeLexicon.SHARE.equals(sourceFrozenPrimaryType); boolean shouldRestore = !versionedChildrenThatShouldNotBeRestored.contains(targetChild); boolean shouldRestoreMixinsAndUuid = false; Path targetPath = target.getPath(cache); boolean restoreTargetUnderSamePath = targetChild != null && targetChild.getPath(cache).getParent().isSameAs(targetPath); if (targetChild != null) { resolvedChild = resolveSourceNode(sourceChild, checkinTime, cache); resolvedPrimaryTypeName = name(resolvedChild.getPrimaryType(cache)); sourceChildNode = session.node(resolvedChild, (Type)null); targetChildNode = session.node(targetChild, (Type)null); if (isShared && !restoreTargetUnderSamePath) { // This is a shared node that already exists in the workspace ... restoredSharedChild(target, sourceChild, targetChildNode); continue; } } if (!restoreTargetUnderSamePath) { if (targetChild != null) { if (!cache.isDestroyed(targetChild.getKey())) { // the target child exists but is under a different path in the source than the target // so we need to remove it from its parent in the target to avoid the case when later on, it might be // destroyed MutableCachedNode targetChildParent = cache.mutable(targetChild.getParentKey(cache)); targetChildParent.removeChild(cache, targetChild.getKey()); } resolvedChild = resolveSourceNode(sourceChild, checkinTime, cache); } else { // Pull the resolved node resolvedChild = inSourceOnly.get(sourceChild); } resolvedPrimaryTypeName = name(resolvedChild.getPrimaryType(cache)); sourceChildNode = session.node(resolvedChild, (Type)null); shouldRestoreMixinsAndUuid = true; Name primaryTypeName = null; NodeKey desiredKey = null; Name desiredName = null; if (isShared && sourceChildNode != null) { // This is a shared node that already exists in the workspace ... AbstractJcrNode resolvedChildNode = session.node(resolvedChild, (Type)null); restoredSharedChild(target, sourceChild, resolvedChildNode); continue; } if (JcrNtLexicon.FROZEN_NODE.equals(resolvedPrimaryTypeName)) { primaryTypeName = name(resolvedChild.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE, cache).getFirstValue()); Property idProp = resolvedChild.getProperty(JcrLexicon.FROZEN_UUID, cache); String frozenUuid = string(idProp.getFirstValue()); desiredKey = target.getKey().withId(frozenUuid); // the name should be that of the versioned child desiredName = session.node(sourceChild, (Type)null).name(); } else { primaryTypeName = resolvedChild.getPrimaryType(cache); Property idProp = resolvedChild.getProperty(JcrLexicon.UUID, cache); if (idProp == null || idProp.isEmpty()) { desiredKey = target.getKey().withRandomId(); } else { String uuid = string(idProp.getFirstValue()); desiredKey = target.getKey().withId(uuid); } assert sourceChildNode != null; desiredName = sourceChildNode.name(); } Property primaryType = propFactory.create(JcrLexicon.PRIMARY_TYPE, primaryTypeName); targetChild = target.createChild(cache, desiredKey, desiredName, primaryType); targetChildNode = session.node(targetChild, (Type)null); assert shouldRestore; } if (shouldRestore) { assert targetChild != null; MutableCachedNode mutableTarget = targetChild instanceof MutableCachedNode ? (MutableCachedNode)targetChild : cache.mutable(targetChild.getKey()); // Have to do this first, as the properties below only exist for mix:versionable nodes if (shouldRestoreMixinsAndUuid) { if (JcrNtLexicon.FROZEN_NODE.equals(resolvedPrimaryTypeName)) { // if we're dealing with a nt:versionedChild (and therefore the resolved node is a frozen node), we // need the mixins from the frozen node restoreNodeMixinsFromProperty(resolvedChild, mutableTarget, cache, JcrLexicon.FROZEN_MIXIN_TYPES); } else { restoreNodeMixins(sourceChild, mutableTarget, cache); } } assert sourceChildNode != null; AbstractJcrNode parent = sourceChildNode.getParent(); if (parent.isNodeType(JcrNtLexicon.VERSION)) { // Clear the checkout status ... clearCheckoutStatus(mutableTarget, parent.key(), cache, propFactory); } restoreNode(sourceChildNode, targetChildNode, checkinTime); } assert targetChildNode != null; if (prevChildKey != null) target.reorderChild(cache, targetChildNode.key(), prevChildKey); prevChildKey = targetChildNode.key(); } } /** * @param target * @param sourceChild * @param existingShareableForChild * @throws RepositoryException */ private void restoredSharedChild( MutableCachedNode target, CachedNode sourceChild, AbstractJcrNode existingShareableForChild ) throws RepositoryException { // The node is shared and exists at another location ... Property idProp = sourceChild.getProperty(JcrLexicon.FROZEN_UUID, cache); String frozenUuid = string(idProp.getFirstValue()); NodeKey desiredKey = target.getKey().withId(frozenUuid); // the name should be that of the versioned child Name desiredName = session.node(sourceChild, (Type)null).name(); // Now link the existing node as a child of the target node ... target.linkChild(cache, desiredKey, desiredName); // If we're to remove existing nodes, then the other places where the node is shared should be removed ... if (removeExisting) { // Remove the other parents ... NodeKey targetKey = target.getKey(); MutableCachedNode shareable = cache.mutable(desiredKey); Set<NodeKey> allParents = new HashSet<NodeKey>(shareable.getAdditionalParentKeys(cache)); NodeKey primaryParentKey = shareable.getParentKey(cache); if (primaryParentKey != null) allParents.add(primaryParentKey); for (NodeKey parentKey : allParents) { if (parentKey.equals(targetKey)) continue; // skip the new target ... MutableCachedNode parent = cache.mutable(parentKey); if (parent != null) { parent.removeChild(cache, desiredKey); } } } } /** * Adds any missing mixin types from the source node to the target node * * @param sourceNode the frozen source node; may not be be null * @param targetNode the target node; may not be null * @param cache the session cache; may not be null * @throws RepositoryException if an error occurs while accessing the repository or adding the mixin types */ private void restoreNodeMixins( CachedNode sourceNode, MutableCachedNode targetNode, SessionCache cache ) throws RepositoryException { restoreNodeMixinsFromProperty(sourceNode, targetNode, cache, JcrLexicon.FROZEN_MIXIN_TYPES); } private void restoreNodeMixinsFromProperty( CachedNode sourceNode, MutableCachedNode targetNode, SessionCache cache, Name sourceNodeMixinTypesPropertyName ) { Property mixinTypesProp = sourceNode.getProperty(sourceNodeMixinTypesPropertyName, cache); if (mixinTypesProp == null || mixinTypesProp.isEmpty()) return; Object[] mixinTypeNames = mixinTypesProp.getValuesAsArray(); Collection<Name> currentMixinTypes = new HashSet<Name>(targetNode.getMixinTypes(cache)); for (Object mixinTypeName1 : mixinTypeNames) { Name mixinTypeName = name(mixinTypeName1); if (!currentMixinTypes.remove(mixinTypeName)) { targetNode.addMixin(cache, mixinTypeName); } } } /** * Restores the properties on the target node based on the stored properties on the source node. The restoration process * is based on the documentation in sections 8.2.7 and 8.2.11 of the JCR 1.0.1 specification. * * @param sourceNode the frozen source node; may not be be null * @param targetNode the target node; may not be null * @throws RepositoryException if an error occurs while accessing the repository or modifying the properties */ private void restoreProperties( AbstractJcrNode sourceNode, AbstractJcrNode targetNode ) throws RepositoryException { Map<Name, Property> sourceProperties = new HashMap<Name, Property>(); Iterator<Property> iter = sourceNode.node().getProperties(cache); while (iter.hasNext()) { Property property = iter.next(); if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(property.getName())) { sourceProperties.put(property.getName(), property); } } MutableCachedNode mutable = targetNode.mutable(); PropertyIterator existingPropIter = targetNode.getProperties(); while (existingPropIter.hasNext()) { AbstractJcrProperty jcrProp = (AbstractJcrProperty)existingPropIter.nextProperty(); Name propName = jcrProp.name(); Property prop = sourceProperties.remove(propName); if (prop != null) { // Overwrite the current property with the property from the version mutable.setProperty(cache, prop); } else { JcrPropertyDefinition propDefn = jcrProp.getDefinition(); switch (propDefn.getOnParentVersion()) { case OnParentVersionAction.COPY: case OnParentVersionAction.ABORT: case OnParentVersionAction.VERSION: // Use the internal method, which bypasses the checks // and removes the AbstractJcrProperty object from the node's internal cache targetNode.removeProperty(jcrProp); break; case OnParentVersionAction.COMPUTE: case OnParentVersionAction.INITIALIZE: case OnParentVersionAction.IGNORE: // Do nothing } } } // Write any properties that were on the source that weren't on the target ... for (Property sourceProperty : sourceProperties.values()) { mutable.setProperty(cache, sourceProperty); } } /** * Resolves the given source node into a frozen node. This may be as simple as returning the node itself (if it has a * primary type of nt:frozenNode) or converting the node to a version history, finding the best match from the versions in * that version history, and returning the frozen node for the best match (if the original source node has a primary type * of nt:versionedChild). * * @param sourceNode the node for which the corresponding frozen node should be returned; may not be null * @param checkinTime the checkin time against which the versions in the version history should be matched; may not be * null * @param cache the cache for the source node; may not be null * @return the frozen node that corresponds to the give source node; may not be null * @throws RepositoryException if an error occurs while accessing the repository * @see #closestMatchFor(JcrVersionHistoryNode, DateTime) */ private CachedNode resolveSourceNode( CachedNode sourceNode, DateTime checkinTime, NodeCache cache ) throws RepositoryException { Name sourcePrimaryTypeName = name(sourceNode.getPrimaryType(cache)); if (JcrNtLexicon.FROZEN_NODE.equals(sourcePrimaryTypeName)) return sourceNode; if (!JcrNtLexicon.VERSIONED_CHILD.equals(sourcePrimaryTypeName)) { return sourceNode; } // Must be a versioned child - try to see if it's one of the versions we're restoring org.modeshape.jcr.value.Property historyRefProp = sourceNode.getProperty(JcrLexicon.CHILD_VERSION_HISTORY, cache); String keyStr = string(historyRefProp.getFirstValue()); assert keyStr != null; /* * First try to find a match among the rootless versions in this restore operation */ for (Version version : nonExistingVersions) { if (keyStr.equals(version.getContainingHistory().getIdentifier())) { JcrVersionNode versionNode = (JcrVersionNode)version; nonExistingVersions.remove(version); return versionNode.getFrozenNode().node(); } } /* * Then check the rooted versions in this restore operation */ for (Version version : existingVersions.keySet()) { if (keyStr.equals(version.getContainingHistory().getIdentifier())) { JcrVersionNode versionNode = (JcrVersionNode)version; existingVersions.remove(version); return versionNode.getFrozenNode().node(); } } /* * If there was a label for this restore operation, try to match that way */ JcrVersionHistoryNode versionHistory = (JcrVersionHistoryNode)session.node(new NodeKey(keyStr), null); if (labelToRestore != null) { try { JcrVersionNode versionNode = versionHistory.getVersionByLabel(labelToRestore); return versionNode.getFrozenNode().node(); } catch (VersionException noVersionWithThatLabel) { // This can happen if there's no version with that label - valid } } /* * If all else fails, find the last version checked in before the checkin time for the version being restored */ AbstractJcrNode match = closestMatchFor(versionHistory, checkinTime); return match.node(); } /** * Finds a node that has the same UUID as is specified in the jcr:frozenUuid property of {@code sourceNode}. If a match * exists and it is a descendant of one of the {@link #versionRootPaths root paths} for this restore operation, it is * returned. If a match exists but is not a descendant of one of the root paths for this restore operation, either an * exception is thrown (if {@link #removeExisting} is false) or the match is deleted and null is returned (if * {@link #removeExisting} is true). * * @param sourceNode the node for which the match should be checked; may not be null * @param cache the cache containing the source node; may not be null * @return the existing node with the same UUID as is specified in the jcr:frozenUuid property of {@code sourceNode}; null * if no node exists with that UUID * @throws ItemExistsException if {@link #removeExisting} is false and the node is not a descendant of any of the * {@link #versionRootPaths root paths} for this restore command * @throws RepositoryException if any other error occurs while accessing the repository */ private CachedNode findMatchFor( CachedNode sourceNode, NodeCache cache ) throws ItemExistsException, RepositoryException { org.modeshape.jcr.value.Property idProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID, cache); if (idProp == null) return null; String idStr = string(idProp.getFirstValue()); try { AbstractJcrNode match = session.getNonSystemNodeByIdentifier(idStr); if (nodeIsOutsideRestoredForest(match)) return null; return match.node(); } catch (ItemNotFoundException infe) { return null; } } /** * Creates a list that is a copy of the supplied ChildReferences object. * * @param references the child references * @return a list containing the same elements as {@code references} in the same order; never null */ private List<NodeKey> asList( ChildReferences references ) { assert references.size() < Integer.MAX_VALUE; List<NodeKey> newList = new ArrayList<NodeKey>((int)references.size()); for (ChildReference ref : references) { newList.add(ref.getKey()); } return newList; } /** * Checks if the given node is outside any of the root paths (and is not shareable) for this restore command. If this * occurs, a special check of the {@link #removeExisting} flag must be performed. If the node is shareable, then the * restore can be completed successfully. * * @param node the node to check; may not be null * @return true if the node is not a descendant of any of the {@link #versionRootPaths root paths} for this restore * command, false otherwise. * @throws ItemExistsException if {@link #removeExisting} is false and the node is not a descendant of any of the * {@link #versionRootPaths root paths} for this restore command * @throws RepositoryException if any other error occurs while accessing the repository */ private boolean nodeIsOutsideRestoredForest( AbstractJcrNode node ) throws ItemExistsException, RepositoryException { if (node.isSystem()) { // System nodes are always outside the restored forest ... return true; } if (node.isShareable()) return false; // Check the path ... Path nodePath = node.path(); for (Path rootPath : versionRootPaths) { if (nodePath.isAtOrBelow(rootPath)) return false; } if (!removeExisting) { throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(node.key(), session.workspace().getName(), node.getPath())); } node.remove(); return true; } /** * Returns the most recent version for the given version history that was checked in before the given time. * * @param versionHistory the version history to check; may not be null * @param checkinTime the checkin time against which the versions in the version history should be matched; may not be * null * @return the {@link JcrVersionNode#getFrozenNode() frozen node} under the most recent {@link Version version} for the * version history that was checked in before {@code checkinTime}; never null * @throws RepositoryException if an error occurs accessing the repository */ private AbstractJcrNode closestMatchFor( JcrVersionHistoryNode versionHistory, DateTime checkinTime ) throws RepositoryException { DateTimeFactory dateFactory = session.context().getValueFactories().getDateFactory(); VersionIterator iter = versionHistory.getAllVersions(); Map<DateTime, Version> versions = new HashMap<DateTime, Version>((int)iter.getSize()); while (iter.hasNext()) { Version version = iter.nextVersion(); versions.put(dateFactory.create(version.getCreated()), version); } List<DateTime> versionDates = new ArrayList<DateTime>(versions.keySet()); Collections.sort(versionDates); Version versionSameDateTime = null; for (int i = versionDates.size() - 1; i >= 0; i--) { DateTime versionDate = versionDates.get(i); if (versionDate.isBefore(checkinTime)) { Version version = versions.get(versionDate); return ((JcrVersionNode)version).getFrozenNode(); } else if (versionDate.equals(checkinTime)) { versionSameDateTime = versions.get(versionDate); } } // we weren't able to find a version with a "before" timestamp, so check for one with the same timestamp if (versionSameDateTime != null) { return ((JcrVersionNode)versionSameDateTime).getFrozenNode(); } throw new IllegalStateException("First checkin must be before the checkin time of the node to be restored"); } } @NotThreadSafe private class MergeCommand { private final Collection<AbstractJcrNode> failures; private final AbstractJcrNode targetNode; private final boolean bestEffort; private final boolean isShallow; private final JcrSession sourceSession; private final SessionCache cache; private final String sourceWorkspaceName; public MergeCommand( AbstractJcrNode targetNode, JcrSession sourceSession, boolean bestEffort, boolean isShallow ) { this.targetNode = targetNode; this.sourceSession = sourceSession; this.cache = this.sourceSession.cache(); this.bestEffort = bestEffort; this.isShallow = isShallow; this.sourceWorkspaceName = sourceSession.getWorkspace().getName(); this.failures = new LinkedList<AbstractJcrNode>(); } final NodeIterator getFailures() { return new JcrNodeListIterator(failures.iterator(), failures.size()); } void execute() throws RepositoryException { doMerge(targetNode); } /* let n' be the corresponding node of n in ws'. if no such n' doleave(n). else if n is not versionable doupdate(n, n'). else if n' is not versionable doleave(n). let v be base version of n. let v' be base version of n'. if v' is an eventual successor of v and n is not checked-in doupdate(n, n'). else if v is equal to or an eventual predecessor of v' doleave(n). else dofail(n, v'). */ private void doMerge( AbstractJcrNode targetNode ) throws RepositoryException { // n is targetNode // n' is sourceNode Path sourcePath = targetNode.correspondingNodePath(sourceWorkspaceName); AbstractJcrNode sourceNode; try { sourceNode = sourceSession.node(sourcePath); } catch (ItemNotFoundException infe) { doLeave(targetNode); return; } if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { doUpdate(targetNode, sourceNode); return; } else if (!sourceNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { doLeave(targetNode); return; } JcrVersionNode sourceVersion = sourceNode.getBaseVersion(); JcrVersionNode targetVersion = targetNode.getBaseVersion(); if (sourceVersion.isEventualSuccessorOf(targetVersion) && !targetNode.isCheckedOut()) { doUpdate(targetNode, sourceNode); return; } if (targetVersion.key().equals(sourceVersion.key())) { doUpdate(targetNode, sourceNode); return; } if (targetVersion.isEventualSuccessorOf(sourceVersion)) { doLeave(targetNode); return; } doFail(targetNode, sourceVersion); } /* if isShallow = false for each child node c of n domerge(c). */ private void doLeave( AbstractJcrNode targetNode ) throws RepositoryException { if (!isShallow) { for (NodeIterator iter = targetNode.getNodesInternal(); iter.hasNext();) { doMerge((AbstractJcrNode)iter.nextNode()); } } } /* replace set of properties of n with those of n'. let S be the set of child nodes of n. let S' be the set of child nodes of n'. judging by node correspondence rules for each child node: let C be the set of nodes in S and in S' let D be the set of nodes in S but not in S'. let D' be the set of nodes in S' but not in S. remove from n all child nodes in D. for each child node of n' in D' copy it (and its subtree) to n as a new child node (if an incoming node has the same UUID as a node already existing in this workspace, the already existing node is removed). for each child node m of n in C domerge(m). */ private void doUpdate( AbstractJcrNode targetNode, AbstractJcrNode sourceNode ) throws RepositoryException { restoreProperties(sourceNode, targetNode); Set<AbstractJcrNode> sourceOnly = new LinkedHashSet<AbstractJcrNode>(); Set<AbstractJcrNode> targetOnly = new LinkedHashSet<AbstractJcrNode>(); Set<AbstractJcrNode> targetNodesPresentInBoth = new LinkedHashSet<AbstractJcrNode>(); Set<AbstractJcrNode> sourceNodesPresentInBoth = new LinkedHashSet<AbstractJcrNode>(); for (NodeIterator iter = targetNode.getNodesInternal(); iter.hasNext();) { AbstractJcrNode targetChild = (AbstractJcrNode)iter.nextNode(); try { Path srcPath = targetChild.correspondingNodePath(sourceWorkspaceName); AbstractJcrNode sourceChild = sourceSession.node(srcPath); targetNodesPresentInBoth.add(targetChild); sourceNodesPresentInBoth.add(sourceChild); } catch (ItemNotFoundException infe) { targetOnly.add(targetChild); } catch (PathNotFoundException pnfe) { targetOnly.add(targetChild); } } for (NodeIterator iter = sourceNode.getNodesInternal(); iter.hasNext();) { AbstractJcrNode sourceChild = (AbstractJcrNode)iter.nextNode(); if (!sourceNodesPresentInBoth.contains(sourceChild)) { sourceOnly.add(sourceChild); } } // D set in algorithm above for (AbstractJcrNode node : targetOnly) { node.internalRemove(true); } // D' set in algorithm above for (AbstractJcrNode node : sourceOnly) { workspace().internalClone(sourceWorkspaceName, node.getPath(), targetNode.getPath() + "/" + node.getName(), false, true); } // C set in algorithm above for (AbstractJcrNode node : targetNodesPresentInBoth) { if (isShallow && node.isNodeType(JcrMixLexicon.VERSIONABLE)) continue; doMerge(node); } } /* if bestEffort = false throw MergeException. else add identifier of v' (if not already present) to the jcr:mergeFailed property of n, add identifier of n to failedset, if isShallow = false for each versionable child node c of n domerge(c) */ private void doFail( AbstractJcrNode targetNode, JcrVersionNode sourceVersion ) throws RepositoryException { if (!bestEffort) { throw new MergeException(); } if (targetNode.hasProperty(JcrLexicon.MERGE_FAILED)) { JcrValue[] existingValues = targetNode.getProperty(JcrLexicon.MERGE_FAILED).getValues(); boolean found = false; String sourceKeyString = sourceVersion.getIdentifier(); for (int i = 0; i < existingValues.length; i++) { if (sourceKeyString.equals(existingValues[i].getString())) { found = true; break; } } if (!found) { JcrValue[] newValues = new JcrValue[existingValues.length + 1]; System.arraycopy(existingValues, 0, newValues, 0, existingValues.length); newValues[newValues.length - 1] = targetNode.valueFrom(sourceVersion); targetNode.setProperty(JcrLexicon.MERGE_FAILED, newValues, PropertyType.REFERENCE, true, false, false, true); } } else { JcrValue[] newValues = new JcrValue[] {targetNode.valueFrom(sourceVersion)}; targetNode.setProperty(JcrLexicon.MERGE_FAILED, newValues, PropertyType.REFERENCE, true, false, false, true); } failures.add(targetNode); if (!isShallow) { for (NodeIterator iter = targetNode.getNodesInternal(); iter.hasNext();) { AbstractJcrNode childNode = (AbstractJcrNode)iter.nextNode(); if (childNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { doMerge(childNode); } } } } /** * Restores the properties on the target node based on the stored properties on the source node. The restoration process * involves copying over all of the properties on the source to the target. * * @param sourceNode the source node; may not be be null * @param targetNode the target node; may not be null * @throws RepositoryException if an error occurs while accessing the repository or modifying the properties */ private void restoreProperties( AbstractJcrNode sourceNode, AbstractJcrNode targetNode ) throws RepositoryException { Map<Name, Property> sourceProperties = new HashMap<Name, Property>(); Iterator<Property> iter = sourceNode.node().getProperties(cache); while (iter.hasNext()) { Property property = iter.next(); if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(property.getName())) { sourceProperties.put(property.getName(), property); } } MutableCachedNode mutable = targetNode.mutable(); SessionCache mutableCache = targetNode.session().cache(); PropertyIterator existingPropIter = targetNode.getProperties(); while (existingPropIter.hasNext()) { AbstractJcrProperty jcrProp = (AbstractJcrProperty)existingPropIter.nextProperty(); Name propName = jcrProp.name(); Property prop = sourceProperties.remove(propName); if (prop != null) { // Overwrite the current property with the property from the version mutable.setProperty(mutableCache, prop); } else { JcrPropertyDefinition propDefn = jcrProp.getDefinition(); switch (propDefn.getOnParentVersion()) { case OnParentVersionAction.COPY: case OnParentVersionAction.ABORT: case OnParentVersionAction.VERSION: // Use the internal method, which bypasses the checks // and removes the AbstractJcrProperty object from the node's internal cache targetNode.removeProperty(jcrProp); break; case OnParentVersionAction.COMPUTE: case OnParentVersionAction.INITIALIZE: case OnParentVersionAction.IGNORE: // Do nothing } } } // Write any properties that were on the source that weren't on the target ... for (Property sourceProperty : sourceProperties.values()) { mutable.setProperty(mutableCache, sourceProperty); } } } }