/*
* 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 : cachedNode.getChildReferences(versionSession)) {
AbstractJcrNode child = session.node(childRef.getKey(), null, versionedKey);
versionNodeAt(child, frozenNode, false, versionSession, systemSession);
}
// Now save all of the changes ...
versionSession.save(systemSession, null);
} finally {
// TODO: Versioning: may want to catch this block and retry, if the new version name couldn't be created
}
return (JcrVersionNode)session.node(version, Type.VERSION);
}
/**
* 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 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,
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(node.getName(), 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);
org.modeshape.jcr.value.Property childVersionHistory = propertyFactory().create(JcrLexicon.CHILD_VERSION_HISTORY,
history.key().toString());
parentInVersionHistory.createChild(versionHistoryCache, key, node.name(), 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 (iff 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, node.name(), 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, node.name(), 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, 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) {
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);
}
}
}
}