Index: modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java (revision 1716) +++ modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java (working copy) @@ -25,6 +25,7 @@ package org.modeshape.graph.session; import java.security.AccessControlException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -788,6 +789,7 @@ public class GraphSession { this.root.unload(); return; } + if (!root.isChanged(true)) { // Then a bunch of changes could have been made and rolled back manually, so recompute the change state ... root.recomputeChangedBelow(); @@ -3002,13 +3004,13 @@ public class GraphSession { public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getName()); - if (payload != null) sb.append(payload); + // if (payload != null) sb.append(payload); if (property.isSingle()) { sb.append(" with value "); } else { sb.append(" with values "); } - sb.append(property.getValuesAsArray()); + sb.append(Arrays.asList(property.getValuesAsArray())); return sb.toString(); } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (working copy) @@ -26,7 +26,6 @@ package org.modeshape.jcr; import java.io.InputStream; import java.security.AccessControlException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; @@ -62,16 +61,12 @@ import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.NodeTypeManager; import javax.jcr.query.Query; import javax.jcr.query.QueryResult; -import javax.jcr.version.OnParentVersionAction; import javax.jcr.version.Version; import javax.jcr.version.VersionException; import net.jcip.annotations.Immutable; import org.modeshape.common.i18n.I18n; -import org.modeshape.common.text.Jsr283Encoder; -import org.modeshape.common.text.TextEncoder; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.HashCode; -import org.modeshape.graph.Graph; import org.modeshape.graph.Location; import org.modeshape.graph.connector.RepositorySourceException; import org.modeshape.graph.property.Binary; @@ -80,11 +75,7 @@ import org.modeshape.graph.property.Name; import org.modeshape.graph.property.NamespaceRegistry; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathFactory; -import org.modeshape.graph.property.PropertyFactory; -import org.modeshape.graph.property.Reference; -import org.modeshape.graph.property.UuidFactory; import org.modeshape.graph.property.ValueFactories; -import org.modeshape.graph.property.ValueFactory; import org.modeshape.graph.query.QueryBuilder; import org.modeshape.graph.query.model.QueryCommand; import org.modeshape.graph.session.GraphSession.Node; @@ -102,10 +93,7 @@ import org.modeshape.jcr.SessionCache.NodeEditor; @Immutable abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node { - private static final TextEncoder NODE_ENCODER = new Jsr283Encoder(); - private static final NodeType[] EMPTY_NODE_TYPES = new NodeType[] {}; - private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; protected final NodeId nodeId; protected final Location location; @@ -676,6 +664,22 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node } /** + * A non-standard method to obtain a child node given the {@link Name ModeShape Name} object. This method is faster + * + * @param childNodeName the child node name + * @return the child node with the supplied name, or null if no child node exists with that name + * @throws RepositoryException if there is an error finding the child node with the supplied name + */ + public final AbstractJcrNode getNode( Name childNodeName ) throws RepositoryException { + try { + Path childPath = context().getValueFactories().getPathFactory().createRelativePath(childNodeName); + return cache.findJcrNode(nodeId, location.getPath(), childPath); + } catch (ItemNotFoundException infe) { + return null; + } + } + + /** * {@inheritDoc} * * @throws IllegalArgumentException if relativePath is empty or null. @@ -1422,10 +1426,12 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node private void checkNotProtected() throws ConstraintViolationException, RepositoryException { JcrNodeDefinition nodeDefn = cache.nodeTypes().getNodeDefinition(nodeInfo().getPayload().getDefinitionId()); if (nodeDefn.isProtected()) { - // TODO: add message throw new ConstraintViolationException(JcrI18n.cannotRemoveItemWithProtectedDefinition.text(getPath())); } + } + final JcrVersionManager versionManager() { + return session().workspace().versionManager(); } /** @@ -1442,198 +1448,10 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node * * @see javax.jcr.Node#checkin() */ - public final Version checkin() throws UnsupportedRepositoryOperationException, RepositoryException { + public final Version checkin() throws RepositoryException { checkVersionable(); - if (isNew() || isModified()) { - throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); - } - - // Check this separately since it throws a different type of exception - if (this.isLocked() && !holdsLock()) { - throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location)); - } - - if (getProperty(JcrLexicon.MERGE_FAILED) != null) { - throw new VersionException(JcrI18n.pendingMergeConflicts.text(getPath())); - } - - Property isCheckedOut = getProperty(JcrLexicon.IS_CHECKED_OUT); - - if (!isCheckedOut.getBoolean()) { - return getBaseVersion(); - } - - PathFactory pathFactory = context().getValueFactories().getPathFactory(); - Name primaryTypeName = getPrimaryTypeName(); - List mixinTypeNames = getMixinTypeNames(); - - UUID jcrUuid = uuid(); - UUID versionUuid = UUID.randomUUID(); - - Name nameSegment = context().getValueFactories().getNameFactory().create(jcrUuid.toString()); - Path historyPath = pathFactory.createAbsolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, nameSegment); - - Node historyNode = cache.findNode(null, historyPath); - - Graph systemGraph = session().repository().createSystemGraph(context()); - Graph.Batch systemBatch = systemGraph.batch(); - DateTime now = context().getValueFactories().getDateFactory().create(); - - Path versionPath = pathFactory.create(historyPath, nameFrom(NODE_ENCODER.encode(now.getString()))); - AbstractJcrProperty predecessorsProp = getProperty(JcrLexicon.PREDECESSORS); - - systemBatch.create(versionPath) - .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION) - .and(JcrLexicon.CREATED, now) - .and(JcrLexicon.UUID, versionUuid) - .and(predecessorsProp.property()) - .and(); - Path frozenVersionPath = pathFactory.create(versionPath, JcrLexicon.FROZEN_NODE); - systemBatch.create(frozenVersionPath) - .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE) - .and(JcrLexicon.FROZEN_UUID, jcrUuid) - .and(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName) - .and(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames) - .and(versionedPropertiesFor(this)) - .and(); - - int onParentVersion = getDefinition().getOnParentVersion(); - for (NodeIterator childNodes = this.getNodes(); childNodes.hasNext();) { - AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode(); - versionNodeAt(childNode, frozenVersionPath, systemBatch, onParentVersion); - } - - PropertyFactory propFactory = context().getPropertyFactory(); - UuidFactory uuidFactory = context().getValueFactories().getUuidFactory(); - - for (Object ob : predecessorsProp.property()) { - UUID predUuid = uuidFactory.create(ob); - - org.modeshape.graph.property.Property successorsProp = systemGraph.getNodeAt(predUuid) - .getProperty(JcrLexicon.SUCCESSORS); - - List newSuccessors = new LinkedList(); - if (successorsProp != null) { - for (Object successor : successorsProp) { - newSuccessors.add(successor); - } - } - - newSuccessors.add(versionUuid); - - org.modeshape.graph.property.Property newSuccessorsProp = propFactory.create(JcrLexicon.SUCCESSORS, - newSuccessors.toArray()); - systemBatch.set(newSuccessorsProp).on(predUuid).and(); - } - - systemBatch.execute(); - cache.refresh(historyNode.getNodeId(), historyPath, false); - - AbstractJcrNode newVersion = cache.findJcrNode(Location.create(versionUuid)); - - NodeEditor editor = editor(); - editor.setProperty(JcrLexicon.PREDECESSORS, - valuesFrom(PropertyType.REFERENCE, EMPTY_OBJECT_ARRAY), - PropertyType.REFERENCE, - false); - editor.setProperty(JcrLexicon.BASE_VERSION, valueFrom(newVersion), false); - editor.setProperty(JcrLexicon.IS_CHECKED_OUT, valueFrom(PropertyType.BOOLEAN, false), false); - save(); - - return new JcrVersionNode(newVersion); - } - - private void versionNodeAt( AbstractJcrNode node, - Path verisonedParentPath, - Graph.Batch batch, - int onParentVersionAction ) throws RepositoryException { - - Path childPath = context().getValueFactories().getPathFactory().create(verisonedParentPath, node.path().getLastSegment()); - - Name primaryTypeName = node.getPrimaryTypeName(); - List mixinTypeNames = node.getMixinTypeNames(); - UUID uuid = UUID.randomUUID(); - if (node.isReferenceable()) uuid = node.uuid(); - - switch (onParentVersionAction) { - case OnParentVersionAction.ABORT: - throw new VersionException(JcrI18n.cannotCheckinNodeWithAbortChildNode.text(node.getName(), node.getParent() - .getName())); - case OnParentVersionAction.VERSION: - if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) { - JcrVersionHistoryNode history = node.getVersionHistory(); - UUID historyUuid = history.uuid(); - batch.create(childPath) - .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSIONED_CHILD) - .with(JcrLexicon.CHILD_VERSION_HISTORY, historyUuid) - .and(); - - break; - } - - // Otherwise, treat it as a copy - case OnParentVersionAction.COPY: - batch.create(childPath) - .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE) - .and(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName) - .and(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames) - .and(JcrLexicon.FROZEN_UUID, uuid) - .and(versionedPropertiesFor(node)) - .and(); - break; - 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: " + onParentVersionAction); - } - - for (NodeIterator childNodes = node.getNodes(); childNodes.hasNext();) { - AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode(); - versionNodeAt(childNode, childPath, batch, onParentVersionAction); - } - - } - - // private Collection<> - - private Collection versionedPropertiesFor( AbstractJcrNode node ) - throws RepositoryException { - - Collection props = new LinkedList(); - PropertyFactory propFactory = context().getPropertyFactory(); - - for (PropertyIterator iter = node.getProperties(); iter.hasNext();) { - AbstractJcrProperty property = (AbstractJcrProperty)iter.nextProperty(); - - org.modeshape.graph.property.Property prop = property.property(); - PropertyDefinitionId propDefnId = property.propertyInfo().getPayload().getPropertyDefinitionId(); - JcrPropertyDefinition propDefn = cache.nodeTypes().getPropertyDefinition(propDefnId); - - 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: - Object[] defaultValues = propDefn.getDefaultValues(); - if (defaultValues != null && defaultValues.length > 0) { - props.add(propFactory.create(prop.getName(), defaultValues)); - } - break; - case OnParentVersionAction.COMPUTE: - case OnParentVersionAction.IGNORE: - // Do nothing for these - } - } - - return props; + return versionManager().checkin(this); } /** @@ -1644,83 +1462,43 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node public final void checkout() throws UnsupportedRepositoryOperationException, LockException, RepositoryException { checkVersionable(); - // Check this separately since it throws a different type of exception - if (this.isLocked() && !holdsLock()) { - throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location)); - } - - PropertyFactory propFactory = context().getPropertyFactory(); - - PropertyInfo mvProp = this.nodeInfo().getProperty(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES); - org.modeshape.graph.property.Property multiValuedProps = mvProp != null ? mvProp.getProperty() : null; - - if (multiValuedProps == null) { - multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, JcrLexicon.PREDECESSORS); - } else if (!Arrays.asList(multiValuedProps.getValues()).contains(JcrLexicon.PREDECESSORS)) { - List values = new LinkedList(); - - for (Object value : multiValuedProps) { - values.add(value); - } - - values.add(JcrLexicon.PREDECESSORS); - multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, values); - } - - ValueFactory refFactory = context().getValueFactories().getReferenceFactory(); - Object[] oldPreds = EMPTY_OBJECT_ARRAY; - - AbstractJcrProperty oldPredsProperty = getBaseVersion().getProperty(JcrLexicon.PREDECESSORS); - if (oldPredsProperty != null) { - oldPreds = oldPredsProperty.property().getValuesAsArray(); - } - - Object[] newPreds = new Object[oldPreds.length + 1]; - System.arraycopy(oldPreds, 0, newPreds, 0, oldPreds.length); - newPreds[oldPreds.length] = refFactory.create(getBaseVersion().uuid()); - - org.modeshape.graph.property.Property isCheckedOut = propFactory.create(JcrLexicon.IS_CHECKED_OUT, true); - org.modeshape.graph.property.Property predecessors = propFactory.create(JcrLexicon.PREDECESSORS, newPreds); - - Graph graph = session().workspace().graph(); - graph.set(isCheckedOut, predecessors, multiValuedProps).on(path()).and(); - - refresh(true); - + versionManager().checkout(this); } /** * {@inheritDoc} * - * @throws UnsupportedOperationException always * @see javax.jcr.Node#merge(java.lang.String, boolean) */ public final NodeIterator merge( String srcWorkspace, - boolean bestEffort ) - throws UnsupportedRepositoryOperationException, ConstraintViolationException, RepositoryException { + boolean bestEffort ) throws ConstraintViolationException, RepositoryException { + CheckArg.isNotNull(srcWorkspace, "source workspace name"); + checkNotProtected(); - throw new UnsupportedRepositoryOperationException(); + return versionManager().merge(this, srcWorkspace, bestEffort); } /** * {@inheritDoc} * - * @throws UnsupportedOperationException always * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version) */ - public final void cancelMerge( Version version ) throws UnsupportedRepositoryOperationException { - throw new UnsupportedRepositoryOperationException(); + public final void cancelMerge( Version version ) throws RepositoryException { + checkVersionable(); + + versionManager().cancelMerge(this, version); } /** * {@inheritDoc} * - * @throws UnsupportedOperationException always * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version) */ - public final void doneMerge( Version version ) throws UnsupportedRepositoryOperationException { - throw new UnsupportedRepositoryOperationException(); + public final void doneMerge( Version version ) throws RepositoryException { + checkVersionable(); + + versionManager().doneMerge(this, version); } /** @@ -1728,10 +1506,10 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node * * @see javax.jcr.Node#getVersionHistory() */ - public final JcrVersionHistoryNode getVersionHistory() throws UnsupportedRepositoryOperationException, RepositoryException { + public final JcrVersionHistoryNode getVersionHistory() throws RepositoryException { checkVersionable(); - return new JcrVersionHistoryNode(session().getNodeByUUID(getProperty(JcrLexicon.VERSION_HISTORY).getString())); + return versionManager().getVersionHistory(uuid()); } /** @@ -1739,10 +1517,10 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node * * @see javax.jcr.Node#getBaseVersion() */ - public final JcrVersionNode getBaseVersion() throws UnsupportedRepositoryOperationException, RepositoryException { + public final JcrVersionNode getBaseVersion() throws RepositoryException { checkVersionable(); - return new JcrVersionNode(session().getNodeByUUID(getProperty(JcrLexicon.BASE_VERSION).getString())); + return (JcrVersionNode)session().getNodeByUUID(getProperty(JcrLexicon.BASE_VERSION).getString()); } /** @@ -1767,7 +1545,7 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node } catch (ConstraintViolationException cve) { throw new UnsupportedRepositoryOperationException(cve); } - restore(version, ".", removeExisting); + versionManager().restore(path(), version, null, removeExisting); } /** @@ -1780,103 +1558,12 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node boolean removeExisting ) throws RepositoryException { checkNotProtected(); - if (isLocked() && !holdsLock()) { - throw new LockException(JcrI18n.lockTokenNotHeld.text(getPath())); - } - - if (session().hasPendingChanges()) { - throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); - } - - JcrVersionNode jcrVersion = (JcrVersionNode)version; - JcrVersionHistoryNode versionHistory = getVersionHistory(); - if (!versionHistory.isSame(jcrVersion.getParent())) { - throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), versionHistory.getPath())); - } - - if (jcrVersion.isSame(versionHistory.getRootVersion())) { - throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(getPath())); - } - PathFactory pathFactory = context().getValueFactories().getPathFactory(); Path relPathAsPath = pathFactory.create(relPath); if (relPathAsPath.isAbsolute()) throw new RepositoryException(JcrI18n.invalidRelativePath.text(relPath)); + Path actualPath = pathFactory.create(path(), relPathAsPath).getCanonicalPath(); - Path actualPath = pathFactory.create(path(), relPathAsPath); - - // Ensure that the parent node exists - this will throw a PNFE if no node exists at that path - AbstractJcrNode parentNode = cache.findJcrNode(null, actualPath.getParent()); - AbstractJcrNode existingNode = null; - - try { - if (path().equals(actualPath)) { - existingNode = this; - } else { - existingNode = cache.findJcrNode(null, actualPath); - if (!versionHistory.isSame(existingNode.getVersionHistory())) { - throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), existingNode.getVersionHistory() - .getPath())); - } - } - } catch (PathNotFoundException pnfe) { - // This is allowable, but the node needs to be checked out - if (!parentNode.isCheckedOut()) { - String path = actualPath.getString(context().getNamespaceRegistry()); - throw new VersionException(JcrI18n.nodeIsCheckedIn.text(path)); - } - } - - AbstractJcrNode frozenNode = jcrVersion.getNode(string(JcrLexicon.FROZEN_NODE)); - if (existingNode == null) { - restoreFrom(frozenNode, parentNode.editor(), removeExisting); - } - - NodeEditor editor = editor(); - editor.setProperty(JcrLexicon.IS_CHECKED_OUT, valueFrom(PropertyType.BOOLEAN, false), false); - editor.setProperty(JcrLexicon.BASE_VERSION, valueFrom(jcrVersion), false); - - session().save(); - } - - private String string( Name name ) { - return name.getString(context().getNamespaceRegistry()); - } - - private static final Set FROZEN_PROPERTY_NAMES = new HashSet(Arrays.asList(new Name[] { - JcrLexicon.FROZEN_PRIMARY_TYPE, JcrLexicon.FROZEN_MIXIN_TYPES, JcrLexicon.FROZEN_UUID})); - - private void restoreFrom( AbstractJcrNode storedNode, - NodeEditor parentEditor, - boolean removeExisting ) throws RepositoryException { - - AbstractJcrProperty uuidProp = storedNode.getProperty(JcrLexicon.FROZEN_UUID); - UUID uuid = uuidProp == null ? null : (UUID)uuidProp.property().getFirstValue(); - AbstractJcrProperty primaryTypeProp = storedNode.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE); - Name primaryTypeName = (Name)primaryTypeProp.property().getFirstValue(); - AbstractJcrProperty mixinTypesProp = storedNode.getProperty(JcrLexicon.FROZEN_MIXIN_TYPES); - Name[] mixinTypeNames = mixinTypesProp == null ? new Name[0] : (Name[])mixinTypesProp.property().getValuesAsArray(); - - JcrNode child = parentEditor.createChild(storedNode.name(), uuid, primaryTypeName); - NodeEditor childEditor = child.editor(); - - for (int i = 0; i < mixinTypeNames.length; i++) { - JcrNodeType mixinType = session().nodeTypeManager().getNodeType(mixinTypeNames[i]); - childEditor.addMixin(mixinType); - } - - for (PropertyInfo propInfo : storedNode.nodeInfo().getProperties()) { - if (FROZEN_PROPERTY_NAMES.contains(propInfo.getName())) continue; - - AbstractJcrProperty jcrProperty = propInfo.getPayload().getJcrProperty(); - if (propInfo.isMultiValued()) { - JcrValue[] values = (JcrValue[])jcrProperty.getValues(); - childEditor.setProperty(propInfo.getName(), values, jcrProperty.getType(), false); - } else { - JcrValue value = (JcrValue)jcrProperty.getValue(); - childEditor.setProperty(propInfo.getName(), value, false); - } - } - + versionManager().restore(actualPath, version, null, removeExisting); } /** @@ -1895,7 +1582,7 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node * @return false * @see javax.jcr.Node#holdsLock() */ - public final boolean holdsLock() /*throws RepositoryException*/{ + public final boolean holdsLock() { WorkspaceLockManager.ModeShapeLock lock = session().workspace().lockManager().lockFor(session(), this.location); return lock != null && cache.session().lockTokens().contains(lock.getLockToken()); Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (working copy) @@ -180,6 +180,7 @@ public final class JcrI18n { public static I18n nodeNotReferenceable; public static I18n nodeNotReferenceableUuid; public static I18n noPendingChangesAllowed; + public static I18n noPendingChangesAllowedForNode; public static I18n cannotUnregisterSupertype; public static I18n cannotUnregisterRequiredPrimaryType; @@ -218,6 +219,9 @@ public final class JcrI18n { public static I18n cannotRestoreRootVersion; public static I18n cannotCheckinNodeWithAbortProperty; public static I18n cannotCheckinNodeWithAbortChildNode; + public static I18n noExistingVersionForRestore; + public static I18n versionNotInMergeFailed; + public static I18n unrootedVersionsInRestore; static { try { Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNode.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNode.java (working copy) @@ -104,7 +104,7 @@ class JcrNode extends AbstractJcrNode { if (parentNode.isLocked()) { Lock parentLock = parentNode.getLock(); if (parentLock != null && parentLock.getLockToken() == null) { - throw new LockException(); + throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location)); } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (working copy) @@ -917,7 +917,24 @@ public class JcrRepository implements Repository { throw new javax.jcr.LoginException(error); } } + return sessionForContext(execContext, workspaceName, sessionAttributes); + } + /** + * Creates a new {@link JcrSession session} based on the given {@link ExecutionContext context} and its associated security + * context. + * + * @param execContext the execution context to use for the new session; may not be null and must have a non-null + * {@link ExecutionContext#getSecurityContext() security context} + * @param workspaceName the name of the workspace to connect to; null indicates that the default workspace should be used + * @param sessionAttributes the session attributes for this session; may not be null + * @return a valid session for the user to access the repository + * @throws RepositoryException if an error occurs creating the session + */ + JcrSession sessionForContext( ExecutionContext execContext, + String workspaceName, + Map sessionAttributes ) throws RepositoryException { + CheckArg.isNotNull(execContext.getSecurityContext(), "execContext.securityContext"); // Ensure valid workspace name by talking directly to the source ... boolean isDefault = false; Graph graph = Graph.create(sourceName, connectionFactory, executionContext); Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (working copy) @@ -30,6 +30,7 @@ import java.security.AccessControlException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -283,6 +284,13 @@ class JcrSession implements Session { } /** + * @return a copy of the session attributes for this session + */ + Map sessionAttributes() { + return new HashMap(sessionAttributes); + } + + /** * {@inheritDoc} * * @see javax.jcr.Session#getNamespacePrefix(java.lang.String) @@ -571,7 +579,9 @@ class JcrSession implements Session { * @see javax.jcr.Session#getNodeByUUID(java.lang.String) */ public AbstractJcrNode getNodeByUUID( String uuid ) throws ItemNotFoundException, RepositoryException { - return cache.findJcrNode(Location.create(UUID.fromString(uuid))); + AbstractJcrNode node = cache.findJcrNode(Location.create(UUID.fromString(uuid))); + + return node; } /** @@ -738,6 +748,18 @@ class JcrSession implements Session { } /** + * Returns a new {@link JcrSession session} that uses the same security information to create a session that points to the + * named workspace. + * + * @param workspaceName the name of the workspace to connect to + * @return a new session that uses the named workspace + * @throws RepositoryException if an error occurs creating the session + */ + JcrSession with( String workspaceName ) throws RepositoryException { + return repository.sessionForContext(executionContext, workspaceName, sessionAttributes); + } + + /** * {@inheritDoc} * * @see javax.jcr.Session#importXML(java.lang.String, java.io.InputStream, int) Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java (working copy) @@ -1,3 +1,26 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ package org.modeshape.jcr; import java.util.Collection; @@ -47,17 +70,23 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { return nodeInfo().getChild(segment).getPayload().getJcrNode(); } + /** + * @{inheritDoc + */ @Override public VersionIterator getAllVersions() throws RepositoryException { return new JcrVersionIterator(getNodes()); } + /** + * @{inheritDoc + */ @Override public Version getRootVersion() throws RepositoryException { // Copied from AbstractJcrNode.getNode(String) to avoid double conversion. Needs to be refactored. Segment segment = context().getValueFactories().getPathFactory().createSegment(JcrLexicon.ROOT_VERSION); try { - return new JcrVersionNode(nodeInfo().getChild(segment).getPayload().getJcrNode()); + return (JcrVersionNode)nodeInfo().getChild(segment).getPayload().getJcrNode(); } catch (org.modeshape.graph.property.PathNotFoundException e) { String msg = JcrI18n.childNotFoundUnderNode.text(segment, getPath(), cache.workspaceName()); throw new PathNotFoundException(msg); @@ -66,18 +95,24 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { } } + /** + * @{inheritDoc + */ @Override public JcrVersionNode getVersion( String versionName ) throws VersionException, RepositoryException { try { AbstractJcrNode version = getNode(versionName); - return new JcrVersionNode(version); + return (JcrVersionNode)version; } catch (PathNotFoundException pnfe) { throw new VersionException(JcrI18n.invalidVersionName.text(versionName, getPath())); } } + /** + * @{inheritDoc + */ @Override - public Version getVersionByLabel( String label ) throws VersionException, RepositoryException { + public JcrVersionNode getVersionByLabel( String label ) throws VersionException, RepositoryException { Property prop = versionLabels().getProperty(label); if (prop == null) throw new VersionException(JcrI18n.invalidVersionLabel.text(label, getPath())); @@ -85,9 +120,12 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { assert version != null; - return new JcrVersionNode(version); + return (JcrVersionNode)version; } + /** + * @{inheritDoc + */ @Override public String[] getVersionLabels() throws RepositoryException { PropertyIterator iter = versionLabels().getProperties(); @@ -128,21 +166,33 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { return labels; } + /** + * @{inheritDoc + */ @Override public String[] getVersionLabels( Version version ) throws RepositoryException { return versionLabelsFor(version).toArray(EMPTY_STRING_ARRAY); } + /** + * @{inheritDoc + */ @Override public String getVersionableUUID() throws RepositoryException { return getProperty(JcrLexicon.VERSIONABLE_UUID).getString(); } + /** + * @{inheritDoc + */ @Override public boolean hasVersionLabel( String label ) throws RepositoryException { return versionLabels().hasProperty(label); } + /** + * @{inheritDoc + */ @Override public boolean hasVersionLabel( Version version, String label ) throws RepositoryException { @@ -151,6 +201,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { return labels.contains(label); } + /** + * @{inheritDoc + */ @Override public void removeVersion( String versionName ) throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, @@ -221,6 +274,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { version.editor().destroy(); } + /** + * @{inheritDoc + */ @Override public void addVersionLabel( String versionName, String label, @@ -245,6 +301,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { } + /** + * @{inheritDoc + */ @Override public void removeVersionLabel( String label ) throws VersionException, RepositoryException { AbstractJcrNode versionLabels = versionLabels(); @@ -279,6 +338,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { this.nodeIterator = nodeIterator; } + /** + * @{inheritDoc + */ @Override public Version nextVersion() { Version next = this.next; @@ -310,18 +372,24 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { } if (!JcrLexicon.VERSION_LABELS.equals(nodeName)) { - return new JcrVersionNode(node); + return (JcrVersionNode)node; } } return null; } + /** + * @{inheritDoc + */ @Override public long getPosition() { return position; } + /** + * @{inheritDoc + */ @Override public long getSize() { // The number of version nodes is the number of child nodes of the version history - 1 @@ -329,6 +397,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { return nodeIterator.getSize() - 1; } + /** + * @{inheritDoc + */ @Override public void skip( long count ) { // Walk through the list to make sure that we don't accidentally count jcr:rootVersion or jcr:versionLabels as a @@ -338,6 +409,9 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { } } + /** + * @{inheritDoc + */ @Override public boolean hasNext() { if (this.next != null) return true; @@ -347,11 +421,17 @@ public class JcrVersionHistoryNode extends JcrNode implements VersionHistory { return this.next != null; } + /** + * @{inheritDoc + */ @Override public Object next() { return nextVersion(); } + /** + * @{inheritDoc + */ @Override public void remove() { throw new UnsupportedOperationException(); Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionManager.java new file mode 100644 =================================================================== --- /dev/null (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionManager.java (working copy) @@ -0,0 +1,1388 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +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.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.MergeException; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +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 net.jcip.annotations.NotThreadSafe; +import org.modeshape.common.i18n.I18n; +import org.modeshape.common.text.Jsr283Encoder; +import org.modeshape.common.text.TextEncoder; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.Graph; +import org.modeshape.graph.Location; +import org.modeshape.graph.property.DateTime; +import org.modeshape.graph.property.DateTimeFactory; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PropertyFactory; +import org.modeshape.graph.property.Reference; +import org.modeshape.graph.property.ValueFactories; +import org.modeshape.graph.property.ValueFactory; +import org.modeshape.graph.property.Path.Segment; +import org.modeshape.graph.session.GraphSession.Node; +import org.modeshape.graph.session.GraphSession.PropertyInfo; +import org.modeshape.jcr.SessionCache.JcrNodePayload; +import org.modeshape.jcr.SessionCache.JcrPropertyPayload; +import org.modeshape.jcr.SessionCache.NodeEditor; + +/** + * 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()}. + */ +public final class JcrVersionManager { + + private static final TextEncoder NODE_ENCODER = new Jsr283Encoder(); + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** + * Property names from nt:frozenNode that should never be copied directly to a node when the frozen node is restored. + */ + private static final Set IGNORED_PROP_NAMES_FOR_RESTORE = new HashSet(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; + + public JcrVersionManager( JcrSession session ) { + super(); + this.session = session; + } + + private ExecutionContext context() { + return session.getExecutionContext(); + } + + private ValueFactories factories() { + return context().getValueFactories(); + } + + private UUID uuid( Object ob ) { + return factories().getUuidFactory().create(ob); + } + + private Name name( String s ) { + return factories().getNameFactory().create(s); + } + + private Name name( Object ob ) { + return factories().getNameFactory().create(ob); + } + + private final Path path( Path root, + Name child ) { + return factories().getPathFactory().create(root, child); + } + + private Path path( Path root, + Path.Segment childSegment ) { + return factories().getPathFactory().create(root, childSegment); + } + + private Path absolutePath( Name... absolutePathSegments ) { + return factories().getPathFactory().createAbsolutePath(absolutePathSegments); + } + + private DateTime dateTime( Calendar cal ) { + return factories().getDateFactory().create(cal); + } + + private PropertyFactory propertyFactory() { + return context().getPropertyFactory(); + } + + private SessionCache cache() { + return session.cache(); + } + + private JcrRepository repository() { + return session.repository(); + } + + private JcrSession session() { + return session; + } + + private JcrWorkspace workspace() { + return session.workspace(); + } + + /** + * @param uuid the value of the {@code jcr:uuid} property (as a UUID) for the node for which the version history should be + * returned + * @return the path to the version history node that corresponds to the node with the given UUID. This does not guarantee that + * a node exists at the returned path. In fact, if the node with the given UUID is not versionable (i.e., {@code + * node.getUUID().equals(uuid.toString()) && !node.isNodeType("mix:versionable")}), there will most likely not be a + * node at the path returned by this method. + */ + Path versionHistoryPathFor( UUID uuid ) { + return absolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, name(uuid.toString())); + } + + /** + * Returns the version history (if one exists) for the node with the given UUID. + * + * @param uuid the UUID of the node for which the history should be returned + * @return the version history for the node + * @throws ItemNotFoundException if there is no version history for the given UUID + * @throws RepositoryException if any other error occurs accessing the repository + * @see AbstractJcrNode#getVersionHistory() + */ + JcrVersionHistoryNode getVersionHistory( UUID uuid ) throws RepositoryException { + return (JcrVersionHistoryNode)cache().findJcrNode(Location.create(versionHistoryPathFor(uuid))); + } + + /** + * Returns the node definition for the given node + * + * @param node the node for which the definition should be returned + * @return the active node definition for the given node + * @throws RepositoryException if an error occurs accessing the repository + * @see {@link JcrNodeTypeManager#getNodeDefinition(NodeDefinitionId)} + */ + JcrNodeDefinition nodeDefinitionFor( Node node ) throws RepositoryException { + NodeDefinitionId nodeDefnId = node.getPayload().getDefinitionId(); + return session().nodeTypeManager().getNodeDefinition(nodeDefnId); + } + + /** + * 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. + */ + JcrVersionNode checkin( AbstractJcrNode node ) throws RepositoryException { + + 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())); + } + + Property isCheckedOut = node.getProperty(JcrLexicon.IS_CHECKED_OUT); + + if (!isCheckedOut.getBoolean()) { + return node.getBaseVersion(); + } + + Name primaryTypeName = node.getPrimaryTypeName(); + List mixinTypeNames = node.getMixinTypeNames(); + + UUID jcrUuid = node.uuid(); + UUID versionUuid = UUID.randomUUID(); + + Path historyPath = versionHistoryPathFor(jcrUuid); + + Node historyNode = cache().findNode(null, historyPath); + + Graph systemGraph = repository().createSystemGraph(context()); + Graph.Batch systemBatch = systemGraph.batch(); + DateTime now = context().getValueFactories().getDateFactory().create(); + + Path versionPath = path(historyPath, name(NODE_ENCODER.encode(now.getString()))); + AbstractJcrProperty predecessorsProp = node.getProperty(JcrLexicon.PREDECESSORS); + + systemBatch.create(versionPath).with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION).and(JcrLexicon.CREATED, now).and(JcrLexicon.UUID, + versionUuid).and(predecessorsProp.property()).and(); + Path frozenVersionPath = path(versionPath, JcrLexicon.FROZEN_NODE); + systemBatch.create(frozenVersionPath).with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE).and(JcrLexicon.FROZEN_UUID, + jcrUuid).and(JcrLexicon.FROZEN_PRIMARY_TYPE, + primaryTypeName).and(JcrLexicon.FROZEN_MIXIN_TYPES, + mixinTypeNames).and(versionedPropertiesFor(node)).and(); + + int onParentVersion = node.getDefinition().getOnParentVersion(); + for (NodeIterator childNodes = node.getNodes(); childNodes.hasNext();) { + AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode(); + versionNodeAt(childNode, frozenVersionPath, systemBatch, onParentVersion); + } + + PropertyFactory propFactory = propertyFactory(); + + for (Object ob : predecessorsProp.property()) { + UUID predUuid = uuid(ob); + + org.modeshape.graph.property.Property successorsProp = systemGraph.getNodeAt(predUuid).getProperty(JcrLexicon.SUCCESSORS); + + List newSuccessors = new LinkedList(); + if (successorsProp != null) { + for (Object successor : successorsProp) { + newSuccessors.add(successor); + } + } + + newSuccessors.add(versionUuid); + + org.modeshape.graph.property.Property newSuccessorsProp = propFactory.create(JcrLexicon.SUCCESSORS, + newSuccessors.toArray()); + systemBatch.set(newSuccessorsProp).on(predUuid).and(); + } + + systemBatch.execute(); + cache().refresh(historyNode.getNodeId(), historyPath, false); + + AbstractJcrNode newVersion = cache().findJcrNode(Location.create(versionUuid)); + + NodeEditor editor = node.editor(); + editor.setProperty(JcrLexicon.PREDECESSORS, + node.valuesFrom(PropertyType.REFERENCE, EMPTY_OBJECT_ARRAY), + PropertyType.REFERENCE, + false); + editor.setProperty(JcrLexicon.BASE_VERSION, node.valueFrom(newVersion), false); + editor.setProperty(JcrLexicon.IS_CHECKED_OUT, node.valueFrom(PropertyType.BOOLEAN, false), false); + node.save(); + + return (JcrVersionNode)newVersion; + } + + /** + * 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 verisonedParentPath the parent for the frozen version record for this node + * @param batch the batch with which the frozen version should be created + * @param onParentVersionAction the {@link OnParentVersionAction} of the node whose {@link #checkin} resulted in this node + * being versioned + * @throws RepositoryException if an error occurs accessing the repository + */ + @SuppressWarnings( "fallthrough" ) + private void versionNodeAt( AbstractJcrNode node, + Path verisonedParentPath, + Graph.Batch batch, + int onParentVersionAction ) throws RepositoryException { + + Path childPath = path(verisonedParentPath, node.path().getLastSegment()); + + Name primaryTypeName = node.getPrimaryTypeName(); + List mixinTypeNames = node.getMixinTypeNames(); + UUID uuid = UUID.randomUUID(); + if (node.isReferenceable()) uuid = node.uuid(); + + switch (onParentVersionAction) { + case OnParentVersionAction.ABORT: + throw new VersionException(JcrI18n.cannotCheckinNodeWithAbortChildNode.text(node.getName(), + node.getParent().getName())); + case OnParentVersionAction.VERSION: + if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) { + JcrVersionHistoryNode history = node.getVersionHistory(); + UUID historyUuid = history.uuid(); + batch.create(childPath).with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSIONED_CHILD).with(JcrLexicon.CHILD_VERSION_HISTORY, + historyUuid).and(); + + break; + } + + // Otherwise, treat it as a copy, as per 8.2.11.2 in the 1.0.1 Spec + case OnParentVersionAction.COPY: + batch.create(childPath).with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE).and(JcrLexicon.FROZEN_PRIMARY_TYPE, + primaryTypeName).and(JcrLexicon.FROZEN_MIXIN_TYPES, + mixinTypeNames).and(JcrLexicon.FROZEN_UUID, + uuid).and(versionedPropertiesFor(node)).and(); + break; + 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: " + onParentVersionAction); + } + + for (NodeIterator childNodes = node.getNodes(); childNodes.hasNext();) { + AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode(); + versionNodeAt(childNode, childPath, batch, onParentVersionAction); + } + + } + + /** + * @param node the node for which the properties should be versioned + * @return 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 Collection versionedPropertiesFor( AbstractJcrNode node ) + throws RepositoryException { + + Collection props = new LinkedList(); + + for (PropertyIterator iter = node.getProperties(); iter.hasNext();) { + AbstractJcrProperty property = (AbstractJcrProperty)iter.nextProperty(); + + org.modeshape.graph.property.Property prop = property.property(); + PropertyDefinitionId propDefnId = property.propertyInfo().getPayload().getPropertyDefinitionId(); + JcrPropertyDefinition propDefn = cache().nodeTypes().getPropertyDefinition(propDefnId); + + 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 + } + } + + return props; + } + + /** + * 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 { + // Check this separately since it throws a different type of exception + if (node.isLocked() && !node.holdsLock()) { + throw new LockException(JcrI18n.lockTokenNotHeld.text(node.getPath())); + } + + PropertyFactory propFactory = propertyFactory(); + + PropertyInfo mvProp = node.nodeInfo().getProperty(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES); + org.modeshape.graph.property.Property multiValuedProps = mvProp != null ? mvProp.getProperty() : null; + + if (multiValuedProps == null) { + multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, JcrLexicon.PREDECESSORS); + } else if (!Arrays.asList(multiValuedProps.getValues()).contains(JcrLexicon.PREDECESSORS)) { + List values = new LinkedList(); + + for (Object value : multiValuedProps) { + values.add(value); + } + + values.add(JcrLexicon.PREDECESSORS); + multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, values); + } + + ValueFactory refFactory = context().getValueFactories().getReferenceFactory(); + Object[] oldPreds = EMPTY_OBJECT_ARRAY; + + AbstractJcrProperty oldPredsProperty = node.getBaseVersion().getProperty(JcrLexicon.PREDECESSORS); + if (oldPredsProperty != null) { + oldPreds = oldPredsProperty.property().getValuesAsArray(); + } + + Object[] newPreds = new Object[oldPreds.length + 1]; + System.arraycopy(oldPreds, 0, newPreds, 0, oldPreds.length); + newPreds[oldPreds.length] = refFactory.create(node.getBaseVersion().uuid()); + + org.modeshape.graph.property.Property isCheckedOut = propFactory.create(JcrLexicon.IS_CHECKED_OUT, true); + org.modeshape.graph.property.Property predecessors = propFactory.create(JcrLexicon.PREDECESSORS, newPreds); + + Graph graph = workspace().graph(); + graph.set(isCheckedOut, predecessors, multiValuedProps).on(node.path()).and(); + + node.refresh(true); + + } + + /** + * See {@link javax.jcr.Workspace#restore(Version[], boolean)} for details of this operation. + * + * @param versions the versions to be restored + * @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 {@link javax.jcr.Workspace#restore(Version[], boolean)} + */ + void restore( Version[] versions, + boolean removeExisting ) throws RepositoryException { + Map existingVersions = new HashMap(versions.length); + Set versionRootPaths = new HashSet(versions.length); + List nonExistingVersions = new ArrayList(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 = session.getNodeByUUID(history.getVersionableUUID()); + existingVersions.put(versions[i], existingNode); + versionRootPaths.add(existingNode.path()); + } catch (ItemNotFoundException infe) { + nonExistingVersions.add(versions[i]); + } + } + + if (existingVersions.isEmpty()) { + throw new VersionException(JcrI18n.noExistingVersionForRestore.text()); + } + + RestoreCommand op = new RestoreCommand(existingVersions, versionRootPaths, nonExistingVersions, null, removeExisting); + op.execute(); + + } + + /** + * Restores the given version to the given path. + * + * @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( Path path, + Version version, + String labelToRestore, + boolean removeExisting ) throws RepositoryException { + if (session().hasPendingChanges()) { + throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); + } + + // Ensure that the parent node exists - this will throw a PNFE if no node exists at that path + AbstractJcrNode parentNode = cache().findJcrNode(null, path.getParent()); + AbstractJcrNode existingNode = null; + AbstractJcrNode nodeToCheckLock; + + JcrVersionNode jcrVersion = (JcrVersionNode)version; + + try { + existingNode = cache().findJcrNode(null, path); + 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(context().getNamespaceRegistry()); + throw new VersionException(JcrI18n.nodeIsCheckedIn.text(parentPath)); + } + + AbstractJcrNode sourceNode = frozenNodeFor(version); + Name primaryTypeName = name(sourceNode.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE).property().getFirstValue()); + AbstractJcrProperty uuidProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID); + UUID desiredUuid = uuid(uuidProp.property().getFirstValue()); + + existingNode = parentNode.editor().createChild(path.getLastSegment().getName(), desiredUuid, primaryTypeName); + + nodeToCheckLock = parentNode; + } + + if (nodeToCheckLock.isLocked() && !nodeToCheckLock.holdsLock()) { + throw new LockException(JcrI18n.lockTokenNotHeld.text(nodeToCheckLock.getPath())); + } + + RestoreCommand op = new RestoreCommand(Collections.singletonMap(version, existingNode), + Collections.singleton(existingNode.path()), Collections.emptySet(), + labelToRestore, removeExisting); + op.execute(); + + NodeEditor editor = existingNode.editor(); + editor.setProperty(JcrLexicon.IS_CHECKED_OUT, existingNode.valueFrom(PropertyType.BOOLEAN, false), false); + editor.setProperty(JcrLexicon.BASE_VERSION, existingNode.valueFrom(jcrVersion), false); + + session().save(); + + } + + /** + * @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 + */ + private AbstractJcrNode frozenNodeFor( Version version ) throws RepositoryException { + return ((AbstractJcrNode)version).getNode(JcrLexicon.FROZEN_NODE); + } + + void doneMerge( AbstractJcrNode targetNode, + Version version ) throws RepositoryException { + 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 = (JcrValue[])prop.getValues(); + JcrValue[] newValues = new JcrValue[values.length + 1]; + System.arraycopy(values, 0, newValues, 0, values.length); + newValues[values.length] = targetNode.valueFrom(version); + + targetNode.editor().setProperty(JcrLexicon.PREDECESSORS, newValues, PropertyType.REFERENCE, false); + + removeVersionFromMergeFailedProperty(targetNode, version); + + targetNode.save(); + } + + void cancelMerge( AbstractJcrNode targetNode, + Version version ) throws RepositoryException { + if (targetNode.isNew() || targetNode.isModified()) { + throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowedForNode.text()); + } + + if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) { + throw new VersionException(JcrI18n.requiresVersionable.text()); + } + + removeVersionFromMergeFailedProperty(targetNode, version); + + targetNode.save(); + } + + 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(); + + String uuidString = version.getUUID(); + int matchIndex = -1; + for (int i = 0; i < values.length; i++) { + if (uuidString.equals(values[i].getString())) { + matchIndex = i; + break; + } + } + + if (matchIndex == -1) { + throw new VersionException(JcrI18n.versionNotInMergeFailed.text(version.getName(), targetNode.getPath())); + } + + if (values.length == 1) { + prop.remove(); + } else { + Value[] newValues = new JcrValue[values.length - 2]; + + if (matchIndex == 0) { + System.arraycopy(values, 1, newValues, 0, values.length - 1); + } else if (matchIndex == values.length - 1) { + System.arraycopy(values, 0, newValues, 0, values.length - 2); + } else { + System.arraycopy(values, 0, newValues, 0, matchIndex); + System.arraycopy(values, matchIndex + 1, newValues, matchIndex, values.length - matchIndex - 1); + } + + prop.setValue(newValues); + } + + } + + NodeIterator merge( AbstractJcrNode targetNode, + String srcWorkspace, + boolean bestEffort ) throws RepositoryException { + 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 new JcrChildNodeIterator(Collections.emptySet(), 0); + } + + JcrSession sourceSession = session().with(srcWorkspace); + MergeCommand op = new MergeCommand(targetNode, sourceSession, bestEffort); + op.execute(); + + session.save(); + + return op.getFailures(); + } + + /** + * Restores the given property onto the node managed by the given editor + * + * @param property the property to restore; may not be null + * @param editor the {@link NodeEditor editor} for the node that is to be modified; may not be null + * @throws RepositoryException if an error occurs while accessing the repository or setting the property + */ + private void restoreProperty( AbstractJcrProperty property, + NodeEditor editor ) throws RepositoryException { + Name propName = property.name(); + editor.removeProperty(propName); + + if (property.isMultiple()) { + JcrValue[] values = (JcrValue[])property.getValues(); + editor.setProperty(propName, values, property.getType(), false); + } else { + JcrValue value = (JcrValue)property.getValue(); + editor.setProperty(propName, value, false); + } + } + + @NotThreadSafe + private class RestoreCommand { + + private Map existingVersions; + private Set versionRootPaths; + private Collection nonExistingVersions; + private boolean removeExisting; + private String labelToRestore; + private Map changedNodes; + + public RestoreCommand( Map existingVersions, + Set versionRootPaths, + Collection nonExistingVersions, + String labelToRestore, + boolean removeExisting ) { + super(); + 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(100); + } + + void execute() throws RepositoryException { + Collection versionsToCheck = new ArrayList(existingVersions.keySet()); + for (Version 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 + restoreNodeMixins(frozenNodeFor(version), root); + restoreNode(frozenNodeFor(version), root, dateTime(version.getCreated())); + } + + 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 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 1.0.1 + * specification (sections 8.27 and 8.2.11), 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); + + NodeEditor targetEditor = targetNode.editor(); + Node targetNodeInfo = targetNode.nodeInfo(); + Node sourceNodeInfo = sourceNode.nodeInfo(); + + // Try to match the existing nodes with nodes from the version to be restored + Map, Node> presentInBoth = new HashMap, Node>(); + + // Start with all target children in this set and pull them out as matches are found + List> inTargetOnly = copyOf(targetNodeInfo.getChildren(), + targetNodeInfo.getChildrenCount()); + + // Start with no source children in this set, but add them in when no match is found + Map, Node> inSourceOnly = new HashMap, Node>(); + + // Map the source children to existing target children where possible + for (Node sourceChild : sourceNodeInfo.getChildren()) { + Node resolvedNode = resolveSourceNode(sourceChild, checkinTime); + Node match = findMatchFor(resolvedNode); + if (match != null) { + inTargetOnly.remove(match); + presentInBoth.put(sourceChild, match); + } else { + inSourceOnly.put(sourceChild, resolvedNode); + } + } + + // Remove all the extraneous children of the target node + for (Node targetChild : inTargetOnly) { + switch (nodeDefinitionFor(targetChild).getOnParentVersion()) { + case OnParentVersionAction.COPY: + case OnParentVersionAction.ABORT: + case OnParentVersionAction.VERSION: + targetEditor.destroyChild(targetChild); + break; + + case OnParentVersionAction.COMPUTE: + // Technically, this should reinitialize the node per its defaults. + case OnParentVersionAction.INITIALIZE: + case OnParentVersionAction.IGNORE: + // Do nothing + } + } + + LinkedList> reversedChildren = new LinkedList>(); + for (Node sourceChild : sourceNodeInfo.getChildren()) { + reversedChildren.addFirst(sourceChild); + } + + // 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 + Node prevChild = null; + for (Node sourceChild : reversedChildren) { + Node targetChild = presentInBoth.get(sourceChild); + Node resolvedChild; + + AbstractJcrNode sourceChildNode; + AbstractJcrNode targetChildNode; + + if (targetChild != null) { + // Reorder if necessary + resolvedChild = resolveSourceNode(sourceChild, checkinTime); + + sourceChildNode = cache().findJcrNode(resolvedChild.getNodeId(), resolvedChild.getPath()); + targetChildNode = cache().findJcrNode(targetChild.getNodeId(), targetChild.getPath()); + + } else { + // Pull the resolved node + resolvedChild = inSourceOnly.get(sourceChild); + sourceChildNode = cache().findJcrNode(resolvedChild.getNodeId(), resolvedChild.getPath()); + + Name primaryTypeName = name(resolvedChild.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE).getProperty().getFirstValue()); + PropertyInfo uuidProp = resolvedChild.getProperty(JcrLexicon.FROZEN_UUID); + UUID desiredUuid = uuid(uuidProp.getProperty().getFirstValue()); + + targetChildNode = targetEditor.createChild(sourceChild.getName(), desiredUuid, primaryTypeName); + } + + // Have to do this first, as the properties below only exist for mix:versionable nodes + restoreNodeMixins(sourceChildNode, targetChildNode); + + if (sourceChildNode.getParent().isNodeType(JcrNtLexicon.VERSION)) { + + NodeEditor editor = targetChildNode.editor(); + editor.setProperty(JcrLexicon.IS_CHECKED_OUT, targetChildNode.valueFrom(PropertyType.BOOLEAN, false), false); + editor.setProperty(JcrLexicon.BASE_VERSION, targetChildNode.valueFrom(sourceChildNode.getParent()), false); + } + + orderBefore(sourceChild, prevChild, targetEditor); + + restoreNode(sourceChildNode, targetChildNode, checkinTime); + prevChild = sourceChild; + } + } + + /** + * Moves {@code targetNode} immediately before {@code beforeNode} under their shared parent. This version is very + * inefficient in that it always tries to move the node, regardless of whether a move is actually required. + *

+ * The key postcondition for this method is that {@code targetNode} must be the last "versioned" child node before {@code + * beforeNode}, although {@code targetNode} need not be the immediate predecessor of {@code beforeNode} if all intervening + * nodes are not "versioned". That is, there can be nodes between {@code targetNode} and {@code beforeNode} as long as + * these nodes all have a {@link NodeDefinition node definition} with an {@link NodeDefinition#getOnParentVersion() + * onParentVersionAction} of IGNORE, COMPUTE, or INITIALIZE. + *

+ * + * @param targetNode the node to be reordered; may not be null + * @param beforeNode the node that must succeed {@code targetNode}; null indicates that {@code targetNode} comes last in + * the list of "versionable" child nodes + * @param parentEditor the {@link NodeEditor editor} for the parent node + * @throws RepositoryException if an error occurs while accessing the repository + */ + private void orderBefore( Node targetNode, + Node beforeNode, + NodeEditor parentEditor ) throws RepositoryException { + Segment beforeSegment = beforeNode == null ? null : beforeNode.getSegment(); + + parentEditor.orderChildBefore(targetNode.getSegment(), beforeSegment); + + } + + /** + * 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 + * @throws RepositoryException if an error occurs while accessing the repository or adding the mixin types + */ + private void restoreNodeMixins( AbstractJcrNode sourceNode, + AbstractJcrNode targetNode ) throws RepositoryException { + AbstractJcrProperty mixinTypesProp = sourceNode.getProperty(JcrLexicon.FROZEN_MIXIN_TYPES); + NodeEditor childEditor = targetNode.editor(); + Object[] mixinTypeNames = mixinTypesProp == null ? EMPTY_OBJECT_ARRAY : mixinTypesProp.property().getValuesAsArray(); + + Collection currentMixinTypes = new HashSet(targetNode.getMixinTypeNames()); + + for (int i = 0; i < mixinTypeNames.length; i++) { + Name mixinTypeName = name(mixinTypeNames[i]); + + if (!currentMixinTypes.remove(mixinTypeName)) { + JcrNodeType mixinType = session().nodeTypeManager().getNodeType(mixinTypeName); + childEditor.addMixin(mixinType); + } + } + + } + + /** + * 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 { + NodeEditor childEditor = targetNode.editor(); + Map> sourcePropertyNames = new HashMap>(); + for (PropertyInfo propInfo : sourceNode.nodeInfo().getProperties()) { + if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(propInfo.getName())) { + sourcePropertyNames.put(propInfo.getName(), propInfo); + } + } + + Collection> targetProps = new ArrayList>( + targetNode.nodeInfo().getProperties()); + for (PropertyInfo propInfo : targetProps) { + Name propName = propInfo.getName(); + + if (sourcePropertyNames.containsKey(propName)) { + // Overwrite the current property with the property from the version + restoreProperty(sourcePropertyNames.get(propName).getPayload().getJcrProperty(), childEditor); + sourcePropertyNames.remove(propName); + } else { + PropertyDefinitionId propDefnId = propInfo.getPayload().getPropertyDefinitionId(); + PropertyDefinition propDefn = session().nodeTypeManager().getPropertyDefinition(propDefnId); + + switch (propDefn.getOnParentVersion()) { + case OnParentVersionAction.COPY: + case OnParentVersionAction.ABORT: + case OnParentVersionAction.VERSION: + childEditor.removeProperty(propName); + break; + + case OnParentVersionAction.COMPUTE: + case OnParentVersionAction.INITIALIZE: + case OnParentVersionAction.IGNORE: + // Do nothing + } + } + } + + for (Map.Entry> sourceProperty : sourcePropertyNames.entrySet()) { + restoreProperty(sourceProperty.getValue().getPayload().getJcrProperty(), childEditor); + } + } + + /** + * 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 + * @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 Node resolveSourceNode( Node sourceNode, + DateTime checkinTime ) throws RepositoryException { + Name sourcePrimaryTypeName = name(sourceNode.getProperty(JcrLexicon.PRIMARY_TYPE).getProperty().getFirstValue()); + + if (JcrNtLexicon.FROZEN_NODE.equals(sourcePrimaryTypeName)) return sourceNode; + assert JcrNtLexicon.VERSIONED_CHILD.equals(sourcePrimaryTypeName); + + // Must be a versioned child - try to see if it's one of the versions we're restoring + PropertyInfo historyUuidProp = sourceNode.getProperty(JcrLexicon.CHILD_VERSION_HISTORY); + UUID uuid = uuid(historyUuidProp.getProperty().getFirstValue()); + assert uuid != null; + String uuidString = uuid.toString(); + + /* + * First try to find a match among the rootless versions in this restore operation + */ + for (Version version : nonExistingVersions) { + if (uuidString.equals(version.getContainingHistory().getUUID())) { + JcrVersionNode versionNode = (JcrVersionNode)version; + nonExistingVersions.remove(version); + return versionNode.getFrozenNode().nodeInfo(); + } + } + + /* + * Then check the rooted versions in this restore operation + */ + for (Version version : existingVersions.keySet()) { + if (uuidString.equals(version.getContainingHistory().getUUID())) { + JcrVersionNode versionNode = (JcrVersionNode)version; + existingVersions.remove(version); + return versionNode.getFrozenNode().nodeInfo(); + } + } + + /* + * If there was a label for this restore operation, try to match that way + */ + JcrVersionHistoryNode versionHistory = (JcrVersionHistoryNode)cache().findJcrNode(Location.create(uuid)); + + if (labelToRestore != null) { + try { + JcrVersionNode versionNode = versionHistory.getVersionByLabel(labelToRestore); + return versionNode.getFrozenNode().nodeInfo(); + } 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.nodeInfo(); + } + + /** + * 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 + * @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 Node findMatchFor( Node sourceNode ) + throws ItemExistsException, RepositoryException { + + PropertyInfo uuidProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID); + UUID sourceUuid = uuid(uuidProp.getProperty().getFirstValue()); + + try { + AbstractJcrNode match = cache().findJcrNode(Location.create(sourceUuid)); + + if (nodeIsOutsideRestoredForest(match)) return null; + + return match.nodeInfo(); + } catch (ItemNotFoundException infe) { + return null; + } + } + + /** + * Copies the given {@link Iterable} into a {@link List} and returns that list. + * + * @param rawElements the iterator containing the items that should be copied; may not be null + * @param size the number of elements in the iterator; may not be negative, but inaccurate values will lead to a badly + * sized list + * @return a list containing the same elements as {@code rawElements} in the same order; never null + */ + private List> copyOf( Iterable> rawElements, + int size ) { + List> newList = new ArrayList>(size); + for (Node node : rawElements) { + newList.add(node); + } + return newList; + } + + /** + * Checks if the given node is outside any of the root paths for this restore command. If this occurs, a special check of + * the {@link #removeExisting} flag must be performed. + * + * @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 { + Path nodePath = node.path(); + + for (Path rootPath : versionRootPaths) { + if (nodePath.isAtOrBelow(rootPath)) return false; + } + + if (!removeExisting) { + throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(node.uuid(), + 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 = context().getValueFactories().getDateFactory(); + + VersionIterator iter = versionHistory.getAllVersions(); + Map versions = new HashMap((int)iter.getSize()); + + while (iter.hasNext()) { + Version version = iter.nextVersion(); + versions.put(dateFactory.create(version.getCreated()), version); + } + + List versionDates = new ArrayList(versions.keySet()); + Collections.sort(versionDates); + + for (int i = versionDates.size() - 1; i >= 0; i--) { + if (versionDates.get(i).isBefore(checkinTime)) { + Version version = versions.get(versionDates.get(i)); + return ((JcrVersionNode)version).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 failures; + private final AbstractJcrNode targetNode; + private final boolean bestEffort; + private final JcrSession sourceSession; + private final String workspaceName; + + public MergeCommand( AbstractJcrNode targetNode, + JcrSession sourceSession, + boolean bestEffort ) { + super(); + this.targetNode = targetNode; + this.sourceSession = sourceSession; + this.bestEffort = bestEffort; + + this.workspaceName = sourceSession.getWorkspace().getName(); + this.failures = new LinkedList(); + } + + final JcrChildNodeIterator getFailures() { + return new JcrChildNodeIterator(failures, 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 a successor of v and n is not checked-in doupdate(n, n'). + else if v is equal to or a predecessor of v' doleave(n). + else dofail(n, v'). + */ + private void doMerge( AbstractJcrNode targetNode ) throws RepositoryException { + Path sourcePath = targetNode.correspondingNodePath(workspaceName); + + AbstractJcrNode sourceNode; + try { + sourceNode = sourceSession.getNode(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.isSuccessorOf(targetVersion) && !targetNode.isCheckedOut()) { + doUpdate(targetNode, sourceNode); + return; + } + + if (targetVersion.isSuccessorOf(sourceVersion) || targetVersion.uuid().equals(sourceVersion.uuid())) { + doLeave(targetNode); + return; + } + + doFail(targetNode, sourceVersion); + } + + /* + for each child node c of n domerge(c). + */ + private void doLeave( AbstractJcrNode targetNode ) throws RepositoryException { + for (NodeIterator iter = targetNode.getNodes(); 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 the name of the 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); + + LinkedHashMap sourceNodes = childNodeMapFor(sourceNode); + LinkedHashMap targetNodes = childNodeMapFor(targetNode); + + // D' set in algorithm above + Map sourceOnly = new LinkedHashMap(sourceNodes); + sourceOnly.keySet().removeAll(targetNodes.keySet()); + + for (AbstractJcrNode node : sourceOnly.values()) { + workspace().copy(workspaceName, node.getPath(), targetNode.getPath() + "/" + node.getName()); + } + + // D set in algorithm above + LinkedHashMap targetOnly = new LinkedHashMap(targetNodes); + targetOnly.keySet().removeAll(targetOnly.keySet()); + + for (AbstractJcrNode node : targetOnly.values()) { + node.remove(); + } + + // C set in algorithm above + Map presentInBoth = new HashMap(targetNodes); + presentInBoth.keySet().retainAll(sourceNodes.keySet()); + for (AbstractJcrNode node : presentInBoth.values()) { + doMerge(node); + } + } + + private LinkedHashMap childNodeMapFor( AbstractJcrNode node ) throws RepositoryException { + LinkedHashMap childNodes = new LinkedHashMap(); + + for (NodeIterator iter = node.getNodes(); iter.hasNext();) { + AbstractJcrNode child = (AbstractJcrNode)iter.nextNode(); + childNodes.put(child.getName(), child); + } + + return childNodes; + } + + /* + if bestEffort = false throw MergeException. + else add UUID of v' (if not already present) to the + jcr:mergeFailed property of n, + add UUID of n to failedset, + doleave(n). + */ + + private void doFail( AbstractJcrNode targetNode, + JcrVersionNode sourceVersion ) throws RepositoryException { + if (!bestEffort) { + throw new MergeException(); + } + + NodeEditor targetEditor = targetNode.editor(); + if (targetNode.hasProperty(JcrLexicon.MERGE_FAILED)) { + JcrValue[] existingValues = (JcrValue[])targetNode.getProperty(JcrLexicon.MERGE_FAILED).getValues(); + + boolean found = false; + String sourceUuidString = sourceVersion.uuid().toString(); + for (int i = 0; i < existingValues.length; i++) { + if (sourceUuidString.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); + targetEditor.setProperty(JcrLexicon.MERGE_FAILED, newValues, PropertyType.REFERENCE, false); + } + + } else { + targetEditor.setProperty(JcrLexicon.MERGE_FAILED, targetNode.valueFrom(sourceVersion), false); + } + failures.add(targetNode); + + doLeave(targetNode); + } + + /** + * 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 { + NodeEditor childEditor = targetNode.editor(); + Map> sourcePropertyNames = new HashMap>(); + for (PropertyInfo propInfo : sourceNode.nodeInfo().getProperties()) { + if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(propInfo.getName())) { + sourcePropertyNames.put(propInfo.getName(), propInfo); + } + } + + Collection> targetProps = new ArrayList>( + targetNode.nodeInfo().getProperties()); + for (PropertyInfo propInfo : targetProps) { + Name propName = propInfo.getName(); + + if (sourcePropertyNames.containsKey(propName)) { + // Overwrite the current property with the property from the version + restoreProperty(sourcePropertyNames.get(propName).getPayload().getJcrProperty(), childEditor); + sourcePropertyNames.remove(propName); + } else { + PropertyDefinitionId propDefnId = propInfo.getPayload().getPropertyDefinitionId(); + PropertyDefinition propDefn = session().nodeTypeManager().getPropertyDefinition(propDefnId); + + switch (propDefn.getOnParentVersion()) { + case OnParentVersionAction.COPY: + case OnParentVersionAction.ABORT: + case OnParentVersionAction.VERSION: + childEditor.removeProperty(propName); + break; + + case OnParentVersionAction.COMPUTE: + case OnParentVersionAction.INITIALIZE: + case OnParentVersionAction.IGNORE: + // Do nothing + } + } + } + + for (Map.Entry> sourceProperty : sourcePropertyNames.entrySet()) { + restoreProperty(sourceProperty.getValue().getPayload().getJcrProperty(), childEditor); + } + } + + } +} Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionNode.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionNode.java (working copy) @@ -1,14 +1,41 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ package org.modeshape.jcr; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +import javax.jcr.ItemNotFoundException; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.version.Version; import org.modeshape.graph.property.Name; +/** + * Convenience wrapper around a version {@link JcrNode node}. + */ public class JcrVersionNode extends JcrNode implements Version { private static final Version[] EMPTY_VERSION_ARRAY = new Version[0]; @@ -19,16 +46,37 @@ public class JcrVersionNode extends JcrNode implements Version { assert !node.isRoot() : "Versions should always be located in the /jcr:system/jcr:versionStorage subgraph"; } + /** + * @{inheritDoc + */ @Override public JcrVersionHistoryNode getContainingHistory() throws RepositoryException { return new JcrVersionHistoryNode(getParent()); } + /** + * @{inheritDoc + */ @Override public Calendar getCreated() throws RepositoryException { return getProperty(JcrLexicon.CREATED).getDate(); } + /** + * Returns the frozen node (the child node named {@code jcr:frozenNode}) for this version, if one exists. + * + * @return the frozen node for this version, if one exists + * @throws ItemNotFoundException if this version has no child with the name {@code jcr:frozenNode}. This should only happen + * for root versions in a version history. + * @throws RepositoryException if an error occurs accessing the repository + */ + AbstractJcrNode getFrozenNode() throws RepositoryException { + return getNode(JcrLexicon.FROZEN_NODE); + } + + /** + * @{inheritDoc + */ @Override public Version[] getPredecessors() throws RepositoryException { return getNodesForProperty(JcrLexicon.PREDECESSORS); @@ -58,10 +106,25 @@ public class JcrVersionNode extends JcrNode implements Version { String uuid = values[i].getString(); AbstractJcrNode node = session().getNodeByUUID(uuid); - versions.add(new JcrVersionNode(node)); + versions.add((JcrVersionNode)node); } return versions.toArray(EMPTY_VERSION_ARRAY); } + + boolean isSuccessorOf( JcrVersionNode other ) throws RepositoryException { + if (!other.hasProperty(JcrLexicon.SUCCESSORS)) return false; + + Value[] successors = other.getProperty(JcrLexicon.SUCCESSORS).getValues(); + + String uuidString = uuid().toString(); + for (int i = 0; i < successors.length; i++) { + if (uuidString.equals(successors[i].getString())) { + return true; + } + } + + return false; + } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (working copy) @@ -128,6 +128,11 @@ class JcrWorkspace implements Workspace { private final JcrNodeTypeManager nodeTypeManager; /** + * Reference to the version manager for this workspace. + */ + private final JcrVersionManager versionManager; + + /** * Reference to the JCR query manager for this workspace. */ private final JcrQueryManager queryManager; @@ -170,6 +175,7 @@ class JcrWorkspace implements Workspace { // This must be initialized after the session this.nodeTypeManager = new JcrNodeTypeManager(session, this.repository.getRepositoryTypeManager()); + this.versionManager = new JcrVersionManager(this.session); this.queryManager = new JcrQueryManager(this.session); this.observationManager = new JcrObservationManager(this.session, this.repository.getRepositoryObservable()); @@ -280,6 +286,10 @@ class JcrWorkspace implements Workspace { return queryManager; } + final JcrVersionManager versionManager() { + return versionManager; + } + /** * {@inheritDoc} * @@ -671,15 +681,7 @@ class JcrWorkspace implements Workspace { throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text()); } - for (int i = 0; i < versions.length; i++) { - JcrVersionNode jcrVersion = (JcrVersionNode)versions[i]; - if (JcrLexicon.ROOT.equals(jcrVersion)) { - throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(versions[i].getPath())); - } - - } - - throw new UnsupportedOperationException(); + versionManager().restore(versions, removeExisting); } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (revision 1716) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (working copy) @@ -68,6 +68,7 @@ import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathFactory; import org.modeshape.graph.property.Property; import org.modeshape.graph.property.PropertyFactory; +import org.modeshape.graph.property.Reference; import org.modeshape.graph.property.ValueFactories; import org.modeshape.graph.property.ValueFactory; import org.modeshape.graph.property.ValueFormatException; @@ -989,7 +990,7 @@ class SessionCache { */ boolean referencePropMissedConstraints = definition != null && definition.getRequiredType() == PropertyType.REFERENCE - && !definition.satisfiesConstraints(value); + && !definition.canCastToTypeAndSatisfyConstraints(value); if (definition == null || referencePropMissedConstraints) { throw new ConstraintViolationException(JcrI18n.noDefinition.text("property", readable(name), @@ -1098,7 +1099,11 @@ class SessionCache { assert name != null; assert values != null; - if (!isCheckedOut()) { + /* + * Skip this check for protected nodes. They can't be modified by users and, in some cases (e.g., jcr:isLocked), + * may be able to be modified for checked-in nodes. + */ + if (!isCheckedOut() && skipProtected) { String path = node.getLocation().getPath().getString(context().getNamespaceRegistry()); throw new VersionException(JcrI18n.nodeIsCheckedIn.text(path)); } @@ -1187,14 +1192,8 @@ class SessionCache { */ boolean referencePropMissedConstraints = definition != null && definition.getRequiredType() == PropertyType.REFERENCE - && !definition.satisfiesConstraints(values); + && !definition.canCastToTypeAndSatisfyConstraints(newValues); if (definition == null || referencePropMissedConstraints) { - definition = nodeTypes().findPropertyDefinition(payload.getPrimaryTypeName(), - payload.getMixinTypeNames(), - name, - newValues, - skipProtected); - throw new ConstraintViolationException(JcrI18n.noDefinition.text("property", readable(name), readable(node.getPath()), @@ -2573,7 +2572,7 @@ class SessionCache { PropertyInfo jcrUuidProp = node.getProperty(JcrLexicon.UUID); - UUID jcrUuid = (UUID)jcrUuidProp.getProperty().getFirstValue(); + UUID jcrUuid = factories().getUuidFactory().create(jcrUuidProp.getProperty().getFirstValue()); Name nameSegment = factories().getNameFactory().create(jcrUuid.toString()); Path historyPath = pathFactory().createAbsolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, nameSegment); @@ -2614,14 +2613,17 @@ class SessionCache { systemBatch.execute(); PropertyFactory propFactory = context().getPropertyFactory(); + ValueFactory refFactory = context().getValueFactories().getReferenceFactory(); Property isCheckedOut = propFactory.create(JcrLexicon.IS_CHECKED_OUT, true); - Property versionHistory = propFactory.create(JcrLexicon.VERSION_HISTORY, historyUuid); - Property baseVersion = propFactory.create(JcrLexicon.BASE_VERSION, versionUuid); - Property predecessors = propFactory.create(JcrLexicon.PREDECESSORS, new Object[] {versionUuid}); + Property versionHistory = propFactory.create(JcrLexicon.VERSION_HISTORY, refFactory.create(historyUuid)); + Property baseVersion = propFactory.create(JcrLexicon.BASE_VERSION, refFactory.create(versionUuid)); + Property predecessors = propFactory.create(JcrLexicon.PREDECESSORS, new Object[] {refFactory.create(versionUuid)}); // This batch will get executed as part of the save batch.set(isCheckedOut, versionHistory, baseVersion, predecessors).on(node.getPath()).and(); + System.out.println("Adding " + versionHistory + " to batch for " + node.getPath()); + Path storagePath = historyPath.getParent(); Node storageNode = findNode(null, storagePath); @@ -2859,6 +2861,14 @@ class SessionCache { } jcrNode = new SoftReference(node); } + + if (JcrNtLexicon.VERSION.equals(primaryTypeName)) { + return new JcrVersionNode(jcrNode.get()); + } + if (JcrNtLexicon.VERSION_HISTORY.equals(primaryTypeName)) { + return new JcrVersionHistoryNode(jcrNode.get()); + } + return jcrNode.get(); } Index: modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties =================================================================== --- modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (revision 1716) +++ modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (working copy) @@ -166,6 +166,7 @@ allNodeTypeTemplatesMustComeFromSameSession=All node type templates must be crea nodeNotReferenceable=Only referenceable nodes may be the value of reference properties nodeNotReferenceableUuid = Only referenceable nodes have a public UUID assigned noPendingChangesAllowed=This operation cannot be performed when the session has pending changes +noPendingChangesAllowedForNode=This operation cannot be performed when the node has pending changes cannotUnregisterSupertype=Cannot unregister type '{0}' which is supertype of type '{1}' cannotUnregisterRequiredPrimaryType=Cannot unregister type '{0}' which is the required primary type for child node '{2}' on type '{1}' @@ -204,4 +205,6 @@ requiresVersionable = This operation requires that the node be versionable (that cannotRestoreRootVersion = The versionable node at '{0}' cannot be restored to its root version cannotCheckinNodeWithAbortProperty = The property '{0}' on the node at '{1}' has an onParentVersionAction of ABORT, preventing checkin cannotCheckinNodeWithAbortChildNode = The child node '{0}' on the node at '{1}' has an onParentVersionAction of ABORT, preventing checkin - +noExistingVersionForRestore = Workspace.restore(...) requires that at least one of the provided versions represents an existing node in the workspace +versionNotInMergeFailed = The version '{0}' is not present in the jcr:mergeFailed property for the node at '{1}' +unrootedVersionsInRestore = The restore operation failed because no node exists in the target workspace for the following versions: {0} Index: modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrNodeTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrNodeTest.java (revision 1716) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrNodeTest.java (working copy) @@ -41,7 +41,6 @@ import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.Workspace; import javax.jcr.lock.LockException; import javax.jcr.nodetype.NodeType; -import javax.jcr.version.Version; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -297,7 +296,7 @@ public class AbstractJcrNodeTest extends AbstractJcrTest { @Test( expected = IllegalArgumentException.class ) public void shouldNotAllowGetNodeWithNoPath() throws Exception { - prius.getNode(null); + prius.getNode((String)null); } @Test @@ -418,48 +417,11 @@ public class AbstractJcrNodeTest extends AbstractJcrTest { assertThat(hybrid.isLocked(), is(false)); } - // Now tested in TCK - // @Test( expected = UnsupportedRepositoryOperationException.class ) - // public void shouldAllowLock() throws Exception { - // hybrid.lock(false, false); - // } - - @Test( expected = UnsupportedRepositoryOperationException.class ) - public void shouldNotAllowMergeOfNonVersionableNode() throws Exception { - hybrid.merge(null, false); - } - @Test( expected = NullPointerException.class ) public void shouldNotAllowOrderBeforeWithNullArgs() throws Exception { hybrid.orderBefore(null, null); } - @Test( expected = UnsupportedRepositoryOperationException.class ) - public void shouldNotAllowRestoreVersionName() throws Exception { - hybrid.restore((String)null, false); - } - - @Test( expected = UnsupportedRepositoryOperationException.class ) - public void shouldNotAllowRestoreVersion() throws Exception { - hybrid.restore((Version)null, false); - } - - @Test( expected = UnsupportedRepositoryOperationException.class ) - public void shouldNotAllowRestoreVersionAtPath() throws Exception { - hybrid.restore(null, null, false); - } - - @Test( expected = UnsupportedRepositoryOperationException.class ) - public void shouldNotAllowRestoreByLabel() throws Exception { - hybrid.restoreByLabel(null, false); - } - - // Now tested in TCK - // @Test( expected = UnsupportedRepositoryOperationException.class ) - // public void shouldNotAllowUnlock() throws Exception { - // hybrid.unlock(); - // } - /* * Primary-type and -item methods */ Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrTckTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrTckTest.java (revision 1716) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrTckTest.java (working copy) @@ -107,14 +107,27 @@ import org.apache.jackrabbit.test.api.version.GetCreatedTest; import org.apache.jackrabbit.test.api.version.GetPredecessorsTest; import org.apache.jackrabbit.test.api.version.GetReferencesNodeTest; import org.apache.jackrabbit.test.api.version.GetVersionableUUIDTest; +import org.apache.jackrabbit.test.api.version.MergeCancelMergeTest; +import org.apache.jackrabbit.test.api.version.MergeCheckedoutSubNodeTest; +import org.apache.jackrabbit.test.api.version.MergeDoneMergeTest; +import org.apache.jackrabbit.test.api.version.MergeNodeIteratorTest; +import org.apache.jackrabbit.test.api.version.MergeNodeTest; +import org.apache.jackrabbit.test.api.version.MergeNonVersionableSubNodeTest; +import org.apache.jackrabbit.test.api.version.MergeSubNodeTest; import org.apache.jackrabbit.test.api.version.OnParentVersionAbortTest; +import org.apache.jackrabbit.test.api.version.OnParentVersionComputeTest; +import org.apache.jackrabbit.test.api.version.OnParentVersionCopyTest; +import org.apache.jackrabbit.test.api.version.OnParentVersionIgnoreTest; +import org.apache.jackrabbit.test.api.version.OnParentVersionInitializeTest; import org.apache.jackrabbit.test.api.version.RemoveVersionTest; +import org.apache.jackrabbit.test.api.version.RestoreTest; import org.apache.jackrabbit.test.api.version.SessionMoveVersionExceptionTest; import org.apache.jackrabbit.test.api.version.VersionGraphTest; import org.apache.jackrabbit.test.api.version.VersionLabelTest; import org.apache.jackrabbit.test.api.version.VersionStorageTest; import org.apache.jackrabbit.test.api.version.VersionTest; import org.apache.jackrabbit.test.api.version.WorkspaceMoveVersionExceptionTest; +import org.apache.jackrabbit.test.api.version.WorkspaceRestoreTest; /** * Test suite to wrap Apache Jackrabbit JCR technology compatibility kit (TCK) unit tests. Note that technically these are not the @@ -139,11 +152,11 @@ public class JcrTckTest { // Or uncomment the following lines to execute the different sets/suites of tests ... TestSuite suite = new TestSuite("JCR 1.0 API tests"); - // + suite.addTest(new LevelOneFeatureTests()); suite.addTest(new LevelTwoFeatureTests()); suite.addTest(new OptionalFeatureTests()); - // suite.addTest(new VersioningTests()); // remove this and the ObservationTests inner class when all tests pass and + suite.addTest(new VersioningTests()); // remove this and the ObservationTests inner class when all tests pass and // uncomment return suite; @@ -352,14 +365,14 @@ public class JcrTckTest { addTestSuite(VersionGraphTest.class); addTestSuite(RemoveVersionTest.class); - // addTestSuite(RestoreTest.class); - // addTestSuite(WorkspaceRestoreTest.class); - // + addTestSuite(RestoreTest.class); + addTestSuite(WorkspaceRestoreTest.class); + addTestSuite(OnParentVersionAbortTest.class); - // addTestSuite(OnParentVersionComputeTest.class); - // addTestSuite(OnParentVersionCopyTest.class); - // addTestSuite(OnParentVersionIgnoreTest.class); - // addTestSuite(OnParentVersionInitializeTest.class); + addTestSuite(OnParentVersionComputeTest.class); + addTestSuite(OnParentVersionCopyTest.class); + addTestSuite(OnParentVersionIgnoreTest.class); + addTestSuite(OnParentVersionInitializeTest.class); addTestSuite(GetReferencesNodeTest.class); addTestSuite(GetPredecessorsTest.class); @@ -369,14 +382,14 @@ public class JcrTckTest { addTestSuite(SessionMoveVersionExceptionTest.class); addTestSuite(WorkspaceMoveVersionExceptionTest.class); - // addTestSuite(MergeCancelMergeTest.class); - // addTestSuite(MergeCheckedoutSubNodeTest.class); - // addTestSuite(MergeDoneMergeTest.class); - // addTestSuite(MergeNodeIteratorTest.class); - // addTestSuite(MergeNodeTest.class); - // addTestSuite(MergeNonVersionableSubNodeTest.class); - // addTestSuite(MergeSubNodeTest.class); - + addTestSuite(MergeCancelMergeTest.class); + addTestSuite(MergeCheckedoutSubNodeTest.class); + addTestSuite(MergeDoneMergeTest.class); + addTestSuite(MergeNodeIteratorTest.class); + addTestSuite(MergeNodeTest.class); + addTestSuite(MergeNonVersionableSubNodeTest.class); + addTestSuite(MergeSubNodeTest.class); + // // addTest(org.apache.jackrabbit.test.api.version.TestAll.suite()); } } Index: modeshape-jcr/src/test/java/org/modeshape/jcr/ModeShapeTckTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/ModeShapeTckTest.java (revision 1716) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/ModeShapeTckTest.java (working copy) @@ -674,4 +674,50 @@ public class ModeShapeTckTest extends AbstractJCRTest { assertThat(rootNode.getProperty("jcr:frozenNode/jcr:frozenPrimaryType").getString(), is("modetest:versionTest")); } + + public void testShouldRestorePropertiesOnVersionableNode() throws Exception { + session = helper.getReadWriteSession(); + Node node = session.getRootNode().addNode("/checkInTest", "modetest:versionTest"); + session.getRootNode().save(); + + /* + * Create /checkinTest/copyNode with copyNode being versionable. This should be able + * to be checked in, as the ABORT status of abortNode is ignored when copyNode is checked in. + */ + + Node copyNode = node.addNode("copyNode", "modetest:versionTest"); + copyNode.addMixin("mix:versionable"); + copyNode.setProperty("copyProp", "copyPropValue"); + copyNode.setProperty("ignoreProp", "ignorePropValue"); + copyNode.setProperty("computeProp", "computePropValue"); + node.save(); + + Version version = copyNode.checkin(); + + /* + * Make some changes + */ + copyNode.checkout(); + copyNode.addMixin("mix:lockable"); + copyNode.setProperty("copyProp", "copyPropValueNew"); + copyNode.setProperty("ignoreProp", "ignorePropValueNew"); + copyNode.setProperty("versionProp", "versionPropValueNew"); + copyNode.setProperty("computeProp", "computePropValueNew"); + copyNode.save(); + copyNode.checkin(); + + copyNode.restore(version, false); + + assertThat(copyNode.getProperty("copyProp").getString(), is("copyPropValue")); + assertThat(copyNode.getProperty("ignoreProp").getString(), is("ignorePropValueNew")); + assertThat(copyNode.getProperty("computeProp").getString(), is("computePropValueNew")); + + try { + copyNode.getProperty("versionProp"); + fail("Property with OnParentVersionAction of VERSION added after version should be removed during restore"); + } catch (PathNotFoundException pnfe) { + // Expected + } + + } }