View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.jcr;
25  
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Calendar;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.LinkedHashMap;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.UUID;
39  import javax.jcr.AccessDeniedException;
40  import javax.jcr.InvalidItemStateException;
41  import javax.jcr.ItemExistsException;
42  import javax.jcr.ItemNotFoundException;
43  import javax.jcr.MergeException;
44  import javax.jcr.NoSuchWorkspaceException;
45  import javax.jcr.NodeIterator;
46  import javax.jcr.PathNotFoundException;
47  import javax.jcr.Property;
48  import javax.jcr.PropertyIterator;
49  import javax.jcr.PropertyType;
50  import javax.jcr.RepositoryException;
51  import javax.jcr.UnsupportedRepositoryOperationException;
52  import javax.jcr.Value;
53  import javax.jcr.lock.LockException;
54  import javax.jcr.nodetype.ConstraintViolationException;
55  import javax.jcr.nodetype.NodeDefinition;
56  import javax.jcr.nodetype.PropertyDefinition;
57  import javax.jcr.version.OnParentVersionAction;
58  import javax.jcr.version.Version;
59  import javax.jcr.version.VersionException;
60  import javax.jcr.version.VersionHistory;
61  import javax.jcr.version.VersionIterator;
62  import javax.jcr.version.VersionManager;
63  import net.jcip.annotations.NotThreadSafe;
64  import org.modeshape.common.i18n.I18n;
65  import org.modeshape.common.text.Jsr283Encoder;
66  import org.modeshape.common.text.TextEncoder;
67  import org.modeshape.common.util.CheckArg;
68  import org.modeshape.common.util.Logger;
69  import org.modeshape.graph.ExecutionContext;
70  import org.modeshape.graph.Graph;
71  import org.modeshape.graph.Location;
72  import org.modeshape.graph.Graph.Batch;
73  import org.modeshape.graph.property.DateTime;
74  import org.modeshape.graph.property.DateTimeFactory;
75  import org.modeshape.graph.property.Name;
76  import org.modeshape.graph.property.Path;
77  import org.modeshape.graph.property.PropertyFactory;
78  import org.modeshape.graph.property.Reference;
79  import org.modeshape.graph.property.ValueFactories;
80  import org.modeshape.graph.property.ValueFactory;
81  import org.modeshape.graph.property.Path.Segment;
82  import org.modeshape.graph.session.GraphSession.Node;
83  import org.modeshape.graph.session.GraphSession.PropertyInfo;
84  import org.modeshape.jcr.SessionCache.JcrNodePayload;
85  import org.modeshape.jcr.SessionCache.JcrPropertyPayload;
86  import org.modeshape.jcr.SessionCache.NodeEditor;
87  
88  /**
89   * Local implementation of version management code, comparable to an implementation of the JSR-283 {@code VersionManager}
90   * interface. Valid instances of this class can be obtained by calling {@link JcrWorkspace#versionManager()}.
91   */
92  final class JcrVersionManager implements VersionManager {
93  
94      private static final Logger LOGGER = Logger.getLogger(JcrVersionManager.class);
95  
96      private static final TextEncoder NODE_ENCODER = new Jsr283Encoder();
97  
98      static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
99  
100     /**
101      * Property names from nt:frozenNode that should never be copied directly to a node when the frozen node is restored.
102      */
103     static final Set<Name> IGNORED_PROP_NAMES_FOR_RESTORE = Collections.unmodifiableSet(new HashSet<Name>(
104                                                                                                           Arrays.asList(new Name[] {
105                                                                                                               JcrLexicon.FROZEN_PRIMARY_TYPE,
106                                                                                                               JcrLexicon.FROZEN_MIXIN_TYPES,
107                                                                                                               JcrLexicon.FROZEN_UUID,
108                                                                                                               JcrLexicon.PRIMARY_TYPE,
109                                                                                                               JcrLexicon.MIXIN_TYPES,
110                                                                                                               JcrLexicon.UUID})));
111 
112     private final JcrSession session;
113 
114     public JcrVersionManager( JcrSession session ) {
115         super();
116         this.session = session;
117     }
118 
119     ExecutionContext context() {
120         return session.getExecutionContext();
121     }
122 
123     private ValueFactories factories() {
124         return context().getValueFactories();
125     }
126 
127     UUID uuid( Object ob ) {
128         return factories().getUuidFactory().create(ob);
129     }
130 
131     Name name( String s ) {
132         return factories().getNameFactory().create(s);
133     }
134 
135     Name name( Object ob ) {
136         return factories().getNameFactory().create(ob);
137     }
138 
139     final Path path( Path root,
140                      Name child ) {
141         return factories().getPathFactory().create(root, child);
142     }
143 
144     Path path( Path root,
145                Path.Segment childSegment ) {
146         return factories().getPathFactory().create(root, childSegment);
147     }
148 
149     private Path absolutePath( Name... absolutePathSegments ) {
150         return factories().getPathFactory().createAbsolutePath(absolutePathSegments);
151     }
152 
153     DateTime dateTime( Calendar cal ) {
154         return factories().getDateFactory().create(cal);
155     }
156 
157     private PropertyFactory propertyFactory() {
158         return context().getPropertyFactory();
159     }
160 
161     SessionCache cache() {
162         return session.cache();
163     }
164 
165     private JcrRepository repository() {
166         return session.repository();
167     }
168 
169     JcrSession session() {
170         return session;
171     }
172 
173     JcrWorkspace workspace() {
174         return session.workspace();
175     }
176 
177     /**
178      * @param uuid the value of the {@code jcr:uuid} property (as a UUID) for the node for which the version history should be
179      *        returned
180      * @return the path to the version history node that corresponds to the node with the given UUID. This does not guarantee that
181      *         a node exists at the returned path. In fact, if the node with the given UUID is not versionable (i.e., {@code
182      *         node.getUUID().equals(uuid.toString()) && !node.isNodeType("mix:versionable")}), there will most likely not be a
183      *         node at the path returned by this method.
184      */
185     Path versionHistoryPathFor( UUID uuid ) {
186         return absolutePath(JcrLexicon.SYSTEM, JcrLexicon.VERSION_STORAGE, name(uuid.toString()));
187     }
188 
189     /**
190      * Returns the version history (if one exists) for the given node.
191      * 
192      * @param node the node for which the history should be returned
193      * @return the version history for the node
194      * @throws ItemNotFoundException if there is no version history for the given UUID
195      * @throws RepositoryException if any other error occurs accessing the repository
196      * @see AbstractJcrNode#getVersionHistory()
197      */
198     JcrVersionHistoryNode getVersionHistory( AbstractJcrNode node ) throws RepositoryException {
199         session.checkLive();
200         checkVersionable(node);
201 
202         Location historyLocation = Location.create(versionHistoryPathFor(node.uuid()));
203         try {
204             return (JcrVersionHistoryNode)cache().findJcrNode(historyLocation);
205         } catch (ItemNotFoundException infe) {
206             initializeVersionHistoryFor(node);
207 
208             // This will throw an ItemNotFoundException if the history node still doesn't exist
209             JcrVersionHistoryNode historyNode = (JcrVersionHistoryNode)cache().findJcrNode(historyLocation);
210 
211             LOGGER.warn(JcrI18n.repairedVersionStorage, historyLocation);
212 
213             return historyNode;
214         }
215     }
216 
217     /**
218      * Returns the node definition for the given node
219      * 
220      * @param node the node for which the definition should be returned
221      * @return the active node definition for the given node
222      * @throws RepositoryException if an error occurs accessing the repository
223      * @see JcrNodeTypeManager#getNodeDefinition(NodeDefinitionId)
224      */
225     JcrNodeDefinition nodeDefinitionFor( Node<JcrNodePayload, JcrPropertyPayload> node ) throws RepositoryException {
226         NodeDefinitionId nodeDefnId = node.getPayload().getDefinitionId();
227         return session().nodeTypeManager().getNodeDefinition(nodeDefnId);
228     }
229 
230     /**
231      * Checks in the given node, creating (and returning) a new {@link Version}.
232      * 
233      * @param node the node to be checked in
234      * @return the {@link Version} object created as a result of this checkin
235      * @throws RepositoryException if an error occurs during the checkin. See {@link javax.jcr.Node#checkin()} for a full
236      *         description of the possible error conditions.
237      */
238     JcrVersionNode checkin( AbstractJcrNode node ) throws RepositoryException {
239 
240         session.checkLive();
241 
242         checkVersionable(node);
243 
244         if (node.isNew() || node.isModified()) {
245             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
246         }
247 
248         // Check this separately since it throws a different type of exception
249         if (node.isLocked() && !node.holdsLock()) {
250             throw new LockException(JcrI18n.lockTokenNotHeld.text(node.getPath()));
251         }
252 
253         if (node.getProperty(JcrLexicon.MERGE_FAILED) != null) {
254             throw new VersionException(JcrI18n.pendingMergeConflicts.text(node.getPath()));
255         }
256 
257         Property isCheckedOut = node.getProperty(JcrLexicon.IS_CHECKED_OUT);
258 
259         if (!isCheckedOut.getBoolean()) {
260             return node.getBaseVersion();
261         }
262 
263         Name primaryTypeName = node.getPrimaryTypeName();
264         List<Name> mixinTypeNames = node.getMixinTypeNames();
265 
266         UUID jcrUuid = node.uuid();
267         UUID versionUuid = UUID.randomUUID();
268 
269         AbstractJcrNode historyNode = getVersionHistory(node);
270         Path historyPath = historyNode.path();
271 
272         Graph systemGraph = repository().createSystemGraph(context());
273         Graph.Batch systemBatch = systemGraph.batch();
274         DateTime now = context().getValueFactories().getDateFactory().create();
275 
276         Path versionPath = path(historyPath, name(NODE_ENCODER.encode(now.getString())));
277         AbstractJcrProperty predecessorsProp = node.getProperty(JcrLexicon.PREDECESSORS);
278 
279         systemBatch.create(versionPath)
280                    .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION)
281                    .and(JcrLexicon.CREATED, now)
282                    .and(JcrLexicon.UUID, versionUuid)
283                    .and(predecessorsProp.property())
284                    .and();
285         Path frozenVersionPath = path(versionPath, JcrLexicon.FROZEN_NODE);
286         systemBatch.create(frozenVersionPath)
287                    .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE)
288                    .and(JcrLexicon.FROZEN_UUID, jcrUuid)
289                    .and(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName)
290                    .and(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames)
291                    .and(versionedPropertiesFor(node))
292                    .and();
293         int onParentVersion = node.getDefinition().getOnParentVersion();
294         for (NodeIterator childNodes = node.getNodes(); childNodes.hasNext();) {
295             AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode();
296             versionNodeAt(childNode, frozenVersionPath, systemBatch, onParentVersion);
297         }
298 
299         PropertyFactory propFactory = propertyFactory();
300 
301         for (Object ob : predecessorsProp.property()) {
302             UUID predUuid = uuid(ob);
303 
304             org.modeshape.graph.property.Property successorsProp = systemGraph.getNodeAt(predUuid)
305                                                                               .getProperty(JcrLexicon.SUCCESSORS);
306 
307             List<Object> newSuccessors = new LinkedList<Object>();
308             boolean alreadySuccessor = false;
309             if (successorsProp != null) {
310                 for (Object successor : successorsProp) {
311                     newSuccessors.add(successor);
312                     if (uuid(successor).equals(predUuid)) alreadySuccessor = true;
313                 }
314             }
315 
316             if (!alreadySuccessor) {
317                 newSuccessors.add(versionUuid);
318 
319                 org.modeshape.graph.property.Property newSuccessorsProp = propFactory.create(JcrLexicon.SUCCESSORS,
320                                                                                              newSuccessors.toArray());
321                 systemBatch.set(newSuccessorsProp).on(predUuid).and();
322             }
323         }
324 
325         systemBatch.execute();
326         historyNode.refresh(false);
327 
328         AbstractJcrNode newVersion = cache().findJcrNode(Location.create(versionUuid));
329 
330         NodeEditor editor = node.editor();
331         editor.setProperty(JcrLexicon.PREDECESSORS,
332                            node.valuesFrom(PropertyType.REFERENCE, EMPTY_OBJECT_ARRAY),
333                            PropertyType.REFERENCE,
334                            false);
335         editor.setProperty(JcrLexicon.BASE_VERSION, node.valueFrom(newVersion), false, false);
336         editor.setProperty(JcrLexicon.IS_CHECKED_OUT, node.valueFrom(PropertyType.BOOLEAN, false), false, false);
337         node.save();
338 
339         return (JcrVersionNode)newVersion;
340     }
341 
342     /**
343      * Create a version record for the given node under the given parent path with the given batch.
344      * 
345      * @param node the node for which the frozen version record should be created
346      * @param verisonedParentPath the parent for the frozen version record for this node
347      * @param batch the batch with which the frozen version should be created
348      * @param onParentVersionAction the {@link OnParentVersionAction} of the node whose {@link #checkin} resulted in this node
349      *        being versioned
350      * @throws RepositoryException if an error occurs accessing the repository
351      */
352     @SuppressWarnings( "fallthrough" )
353     private void versionNodeAt( AbstractJcrNode node,
354                                 Path verisonedParentPath,
355                                 Graph.Batch batch,
356                                 int onParentVersionAction ) throws RepositoryException {
357         Path childPath = path(verisonedParentPath, node.path().getLastSegment());
358 
359         Name primaryTypeName = node.getPrimaryTypeName();
360         List<Name> mixinTypeNames = node.getMixinTypeNames();
361         UUID uuid = UUID.randomUUID();
362         if (node.isReferenceable()) uuid = node.uuid();
363 
364         switch (onParentVersionAction) {
365             case OnParentVersionAction.ABORT:
366                 throw new VersionException(JcrI18n.cannotCheckinNodeWithAbortChildNode.text(node.getName(), node.getParent()
367                                                                                                                 .getName()));
368             case OnParentVersionAction.VERSION:
369                 if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) {
370                     JcrVersionHistoryNode history = node.getVersionHistory();
371                     UUID historyUuid = history.uuid();
372                     batch.create(childPath)
373                          .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSIONED_CHILD)
374                          .with(JcrLexicon.CHILD_VERSION_HISTORY, historyUuid)
375                          .and();
376 
377                     break;
378                 }
379 
380                 // Otherwise, treat it as a copy, as per 8.2.11.2 in the 1.0.1 Spec
381             case OnParentVersionAction.COPY:
382                 batch.create(childPath)
383                      .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE)
384                      .and(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName)
385                      .and(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames)
386                      .and(JcrLexicon.FROZEN_UUID, uuid)
387                      .and(versionedPropertiesFor(node))
388                      .and();
389                 break;
390             case OnParentVersionAction.INITIALIZE:
391             case OnParentVersionAction.COMPUTE:
392             case OnParentVersionAction.IGNORE:
393                 // Do nothing for these. No built-in types require initialize or compute for child nodes.
394                 return;
395             default:
396                 throw new IllegalStateException("Unexpected value: " + onParentVersionAction);
397         }
398 
399         for (NodeIterator childNodes = node.getNodes(); childNodes.hasNext();) {
400             AbstractJcrNode childNode = (AbstractJcrNode)childNodes.nextNode();
401             versionNodeAt(childNode, childPath, batch, onParentVersionAction);
402         }
403 
404     }
405 
406     /**
407      * @param node the node for which the properties should be versioned
408      * @return the versioned properties for {@code node} (i.e., the properties to add the the frozen version of {@code node}
409      * @throws RepositoryException if an error occurs accessing the repository
410      */
411     private Collection<org.modeshape.graph.property.Property> versionedPropertiesFor( AbstractJcrNode node )
412         throws RepositoryException {
413 
414         Collection<org.modeshape.graph.property.Property> props = new LinkedList<org.modeshape.graph.property.Property>();
415 
416         // Have to add this directly as it's not returned by AbstractJcrNode.getProperties
417         AbstractJcrProperty multiValuedProperties = node.getProperty(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES);
418         if (multiValuedProperties != null) props.add(multiValuedProperties.property());
419 
420         for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
421             AbstractJcrProperty property = (AbstractJcrProperty)iter.nextProperty();
422 
423             org.modeshape.graph.property.Property prop = property.property();
424             PropertyDefinitionId propDefnId = property.propertyInfo().getPayload().getPropertyDefinitionId();
425             JcrPropertyDefinition propDefn = cache().nodeTypes().getPropertyDefinition(propDefnId);
426 
427             switch (propDefn.getOnParentVersion()) {
428                 case OnParentVersionAction.ABORT:
429                     I18n msg = JcrI18n.cannotCheckinNodeWithAbortProperty;
430                     throw new VersionException(msg.text(property.getName(), node.getName()));
431                 case OnParentVersionAction.COPY:
432                 case OnParentVersionAction.VERSION:
433                     props.add(prop);
434                     break;
435                 case OnParentVersionAction.INITIALIZE:
436                 case OnParentVersionAction.COMPUTE:
437                 case OnParentVersionAction.IGNORE:
438                     // Do nothing for these
439             }
440         }
441 
442         return props;
443     }
444 
445     /**
446      * Checks out the given node, updating version-related properties on the node as needed.
447      * 
448      * @param node the node to be checked out
449      * @throws LockException if a lock prevents the node from being checked out
450      * @throws RepositoryException if an error occurs during the checkout. See {@link javax.jcr.Node#checkout()} for a full
451      *         description of the possible error conditions.
452      */
453     void checkout( AbstractJcrNode node ) throws LockException, RepositoryException {
454         session.checkLive();
455         checkVersionable(node);
456 
457         // Check this separately since it throws a different type of exception
458         if (node.isLocked() && !node.holdsLock()) {
459             throw new LockException(JcrI18n.lockTokenNotHeld.text(node.getPath()));
460         }
461 
462         /*
463          * This happens when we've added mix:versionable, but not saved it to create the base
464          * version (and the rest of the version storage graph).  See MODE-704.
465          */
466         if (!node.hasProperty(JcrLexicon.BASE_VERSION)) {
467             return;
468         }
469 
470         // Checking out an already checked-out node is supposed to return silently
471         if (node.getProperty(JcrLexicon.IS_CHECKED_OUT).getBoolean()) {
472             return;
473         }
474 
475         PropertyFactory propFactory = propertyFactory();
476 
477         PropertyInfo<JcrPropertyPayload> mvProp = node.nodeInfo().getProperty(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES);
478         org.modeshape.graph.property.Property multiValuedProps = mvProp != null ? mvProp.getProperty() : null;
479 
480         if (multiValuedProps == null) {
481             multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, JcrLexicon.PREDECESSORS);
482         } else if (!Arrays.<Object>asList(multiValuedProps.getValues()).contains(JcrLexicon.PREDECESSORS)) {
483             List<Object> values = new LinkedList<Object>();
484 
485             for (Object value : multiValuedProps) {
486                 values.add(value);
487             }
488 
489             values.add(JcrLexicon.PREDECESSORS);
490             multiValuedProps = propFactory.create(ModeShapeIntLexicon.MULTI_VALUED_PROPERTIES, values);
491         }
492 
493         ValueFactory<Reference> refFactory = context().getValueFactories().getReferenceFactory();
494         Object[] oldPreds = EMPTY_OBJECT_ARRAY;
495 
496         AbstractJcrProperty oldPredsProperty = node.getBaseVersion().getProperty(JcrLexicon.PREDECESSORS);
497         if (oldPredsProperty != null) {
498             oldPreds = oldPredsProperty.property().getValuesAsArray();
499         }
500 
501         Object[] newPreds = new Object[oldPreds.length + 1];
502         newPreds[0] = refFactory.create(node.getBaseVersion().uuid());
503         System.arraycopy(oldPreds, 0, newPreds, 1, oldPreds.length);
504 
505         org.modeshape.graph.property.Property isCheckedOut = propFactory.create(JcrLexicon.IS_CHECKED_OUT, true);
506         org.modeshape.graph.property.Property predecessors = propFactory.create(JcrLexicon.PREDECESSORS, newPreds);
507 
508         Graph graph = workspace().graph();
509         Location location = Location.create(node.uuid());
510         graph.set(isCheckedOut, predecessors, multiValuedProps).on(location).and();
511 
512         cache().refreshProperties(location);
513     }
514 
515     /**
516      * See {@link javax.jcr.Workspace#restore(Version[], boolean)} for details of this operation.
517      * 
518      * @param versions the versions to be restored
519      * @param removeExisting if UUID conflicts resulting from this restore should cause the conflicting node to be removed or an
520      *        exception to be thrown and the operation to fail
521      * @throws RepositoryException if an error occurs accessing the repository
522      * @see javax.jcr.Workspace#restore(Version[], boolean)
523      */
524     @Override
525     public void restore( Version[] versions,
526                          boolean removeExisting ) throws RepositoryException {
527         session.checkLive();
528         if (session.hasPendingChanges()) {
529             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
530         }
531 
532         Map<Version, AbstractJcrNode> existingVersions = new HashMap<Version, AbstractJcrNode>(versions.length);
533         Set<Path> versionRootPaths = new HashSet<Path>(versions.length);
534         List<Version> nonExistingVersions = new ArrayList<Version>(versions.length);
535 
536         for (int i = 0; i < versions.length; i++) {
537             VersionHistory history = versions[i].getContainingHistory();
538 
539             if (history.getRootVersion().isSame(versions[i])) {
540                 throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(versions[i].getPath()));
541             }
542 
543             try {
544                 AbstractJcrNode existingNode = session.getNodeByIdentifier(history.getVersionableIdentifier());
545                 existingVersions.put(versions[i], existingNode);
546                 versionRootPaths.add(existingNode.path());
547             } catch (ItemNotFoundException infe) {
548                 nonExistingVersions.add(versions[i]);
549             }
550         }
551 
552         if (existingVersions.isEmpty()) {
553             throw new VersionException(JcrI18n.noExistingVersionForRestore.text());
554         }
555 
556         RestoreCommand op = new RestoreCommand(existingVersions, versionRootPaths, nonExistingVersions, null, removeExisting);
557         op.execute();
558 
559         session.save();
560     }
561 
562     /**
563      * Restores the given version to the given path.
564      * 
565      * @param path the path at which the version should be restored; may not be null
566      * @param version the version to restore; may not be null
567      * @param labelToRestore the label that was used to identify the version; may be null
568      * @param removeExisting if UUID conflicts resulting from this restore should cause the conflicting node to be removed or an
569      *        exception to be thrown and the operation to fail
570      * @throws RepositoryException if an error occurs accessing the repository
571      * @see javax.jcr.Node#restore(Version, String, boolean)
572      * @see javax.jcr.Node#restoreByLabel(String, boolean)
573      */
574     void restore( Path path,
575                   Version version,
576                   String labelToRestore,
577                   boolean removeExisting ) throws RepositoryException {
578         session.checkLive();
579 
580         if (session().hasPendingChanges()) {
581             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
582         }
583 
584         // Ensure that the parent node exists - this will throw a PNFE if no node exists at that path
585         AbstractJcrNode parentNode = cache().findJcrNode(null, path.getParent());
586         AbstractJcrNode existingNode = null;
587         AbstractJcrNode nodeToCheckLock;
588 
589         JcrVersionNode jcrVersion = (JcrVersionNode)version;
590 
591         try {
592             existingNode = cache().findJcrNode(null, path);
593             nodeToCheckLock = existingNode;
594 
595             // These checks only make sense if there is an existing node
596             JcrVersionHistoryNode versionHistory = existingNode.getVersionHistory();
597             if (!versionHistory.isSame(jcrVersion.getParent())) {
598                 throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), versionHistory.getPath()));
599             }
600 
601             if (!versionHistory.isSame(existingNode.getVersionHistory())) {
602                 throw new VersionException(JcrI18n.invalidVersion.text(version.getPath(), existingNode.getVersionHistory()
603                                                                                                       .getPath()));
604             }
605 
606             if (jcrVersion.isSame(versionHistory.getRootVersion())) {
607                 throw new VersionException(JcrI18n.cannotRestoreRootVersion.text(existingNode.getPath()));
608             }
609 
610         } catch (ItemNotFoundException pnfe) {
611             // This is allowable, but the node needs to be checked out
612             if (!parentNode.isCheckedOut()) {
613                 String parentPath = path.getString(context().getNamespaceRegistry());
614                 throw new VersionException(JcrI18n.nodeIsCheckedIn.text(parentPath));
615             }
616 
617             AbstractJcrNode sourceNode = frozenNodeFor(version);
618             Name primaryTypeName = name(sourceNode.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE).property().getFirstValue());
619             AbstractJcrProperty uuidProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID);
620             UUID desiredUuid = uuid(uuidProp.property().getFirstValue());
621 
622             existingNode = parentNode.editor().createChild(path.getLastSegment().getName(), desiredUuid, primaryTypeName);
623 
624             nodeToCheckLock = parentNode;
625         }
626 
627         if (nodeToCheckLock.isLocked() && !nodeToCheckLock.holdsLock()) {
628             throw new LockException(JcrI18n.lockTokenNotHeld.text(nodeToCheckLock.getPath()));
629         }
630 
631         RestoreCommand op = new RestoreCommand(Collections.singletonMap(version, existingNode),
632                                                Collections.singleton(existingNode.path()), Collections.<Version>emptySet(),
633                                                labelToRestore, removeExisting);
634         op.execute();
635 
636         NodeEditor editor = existingNode.editor();
637         editor.setProperty(JcrLexicon.IS_CHECKED_OUT, existingNode.valueFrom(PropertyType.BOOLEAN, false), false, false);
638         editor.setProperty(JcrLexicon.BASE_VERSION, existingNode.valueFrom(jcrVersion), false, false);
639 
640         session().save();
641 
642     }
643 
644     /**
645      * @param version the version for which the frozen node should be returned
646      * @return the frozen node for the given version
647      * @throws RepositoryException if an error occurs accessing the repository
648      */
649     AbstractJcrNode frozenNodeFor( Version version ) throws RepositoryException {
650         return ((AbstractJcrNode)version).getNode(JcrLexicon.FROZEN_NODE);
651     }
652 
653     void doneMerge( AbstractJcrNode targetNode,
654                     Version version ) throws RepositoryException {
655         session.checkLive();
656         checkVersionable(targetNode);
657 
658         if (targetNode.isNew() || targetNode.isModified()) {
659             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowedForNode.text());
660         }
661 
662         if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) {
663             throw new VersionException(JcrI18n.requiresVersionable.text());
664         }
665 
666         AbstractJcrProperty prop = targetNode.getProperty(JcrLexicon.PREDECESSORS);
667 
668         JcrValue[] values = (JcrValue[])prop.getValues();
669         JcrValue[] newValues = new JcrValue[values.length + 1];
670         System.arraycopy(values, 0, newValues, 0, values.length);
671         newValues[values.length] = targetNode.valueFrom(version);
672 
673         targetNode.editor().setProperty(JcrLexicon.PREDECESSORS, newValues, PropertyType.REFERENCE, false);
674 
675         removeVersionFromMergeFailedProperty(targetNode, version);
676 
677         targetNode.save();
678     }
679 
680     void cancelMerge( AbstractJcrNode targetNode,
681                       Version version ) throws RepositoryException {
682         session.checkLive();
683         checkVersionable(targetNode);
684 
685         if (targetNode.isNew() || targetNode.isModified()) {
686             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowedForNode.text());
687         }
688 
689         if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) {
690             throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text());
691         }
692 
693         removeVersionFromMergeFailedProperty(targetNode, version);
694 
695         targetNode.save();
696     }
697 
698     @SuppressWarnings( "deprecation" )
699     private void removeVersionFromMergeFailedProperty( AbstractJcrNode targetNode,
700                                                        Version version ) throws RepositoryException {
701 
702         if (!targetNode.hasProperty(JcrLexicon.MERGE_FAILED)) {
703             throw new VersionException(JcrI18n.versionNotInMergeFailed.text(version.getName(), targetNode.getPath()));
704         }
705 
706         AbstractJcrProperty prop = targetNode.getProperty(JcrLexicon.MERGE_FAILED);
707         Value[] values = prop.getValues();
708 
709         String uuidString = version.getUUID();
710         int matchIndex = -1;
711         for (int i = 0; i < values.length; i++) {
712             if (uuidString.equals(values[i].getString())) {
713                 matchIndex = i;
714                 break;
715             }
716         }
717 
718         if (matchIndex == -1) {
719             throw new VersionException(JcrI18n.versionNotInMergeFailed.text(version.getName(), targetNode.getPath()));
720         }
721 
722         if (values.length == 1) {
723             prop.remove();
724         } else {
725             Value[] newValues = new JcrValue[values.length - 2];
726 
727             if (matchIndex == 0) {
728                 System.arraycopy(values, 1, newValues, 0, values.length - 1);
729             } else if (matchIndex == values.length - 1) {
730                 System.arraycopy(values, 0, newValues, 0, values.length - 2);
731             } else {
732                 System.arraycopy(values, 0, newValues, 0, matchIndex);
733                 System.arraycopy(values, matchIndex + 1, newValues, matchIndex, values.length - matchIndex - 1);
734             }
735 
736             prop.setValue(newValues);
737         }
738 
739     }
740 
741     NodeIterator merge( AbstractJcrNode targetNode,
742                         String srcWorkspace,
743                         boolean bestEffort,
744                         boolean isShallow ) throws RepositoryException {
745         session.checkLive();
746 
747         if (session().hasPendingChanges()) {
748             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
749         }
750 
751         try {
752             targetNode.correspondingNodePath(srcWorkspace);
753         } catch (ItemNotFoundException infe) {
754             // return immediately if no corresponding node exists in that workspace
755             return new JcrChildNodeIterator(Collections.<AbstractJcrNode>emptySet(), 0);
756         }
757 
758         JcrSession sourceSession = session().with(srcWorkspace);
759         MergeCommand op = new MergeCommand(targetNode, sourceSession, bestEffort, isShallow);
760         op.execute();
761 
762         session.save();
763 
764         return op.getFailures();
765     }
766 
767     /**
768      * Restores the given property onto the node managed by the given editor
769      * 
770      * @param property the property to restore; may not be null
771      * @param editor the {@link NodeEditor editor} for the node that is to be modified; may not be null
772      * @throws RepositoryException if an error occurs while accessing the repository or setting the property
773      */
774     void restoreProperty( AbstractJcrProperty property,
775                           NodeEditor editor ) throws RepositoryException {
776         Name propName = property.name();
777         editor.removeProperty(propName);
778 
779         if (property.isMultiple()) {
780             JcrValue[] values = (JcrValue[])property.getValues();
781             editor.setProperty(propName, values, property.getType(), false);
782         } else {
783             JcrValue value = (JcrValue)property.getValue();
784             editor.setProperty(propName, value, false, false);
785         }
786     }
787 
788     void initializeVersionHistoryFor( AbstractJcrNode node ) throws RepositoryException {
789         initializeVersionHistoryFor(node, null);
790     }
791 
792     void initializeVersionHistoryFor( AbstractJcrNode node,
793                                       UUID originalVersionUuid ) throws RepositoryException {
794         Batch batch = session().createBatch();
795 
796         initializeVersionHistoryFor(batch, node.nodeInfo(), originalVersionUuid, true);
797 
798         batch.execute();
799     }
800 
801     void initializeVersionHistoryFor( Graph.Batch batch,
802                                       Node<JcrNodePayload, JcrPropertyPayload> node,
803                                       UUID originalVersionUuid,
804                                       boolean forceWrite ) throws RepositoryException {
805 
806         if (!cache().isVersionable(node)) return;
807 
808         /*
809          * Determine if the node has already had its version history initialized based on whether the protected property
810          * jcr:isCheckedOut exists.
811          */
812 
813         boolean initialized = node.getProperty(JcrLexicon.IS_CHECKED_OUT) != null;
814         if (!forceWrite && initialized) return;
815 
816         UUID historyUuid = UUID.randomUUID();
817         UUID versionUuid = UUID.randomUUID();
818 
819         initializeVersionStorageFor(node, historyUuid, originalVersionUuid, versionUuid);
820 
821         PropertyInfo<JcrPropertyPayload> jcrUuidProp = node.getProperty(JcrLexicon.UUID);
822         UUID jcrUuid = uuid(jcrUuidProp.getProperty().getFirstValue());
823         Path historyPath = versionHistoryPathFor(jcrUuid);
824 
825         ValueFactory<Reference> refFactory = context().getValueFactories().getReferenceFactory();
826         org.modeshape.graph.property.Property isCheckedOut = propertyFactory().create(JcrLexicon.IS_CHECKED_OUT, true);
827         org.modeshape.graph.property.Property versionHistory = propertyFactory().create(JcrLexicon.VERSION_HISTORY,
828                                                                                         refFactory.create(historyUuid));
829         org.modeshape.graph.property.Property baseVersion = propertyFactory().create(JcrLexicon.BASE_VERSION,
830                                                                                      refFactory.create(versionUuid));
831         org.modeshape.graph.property.Property predecessors = propertyFactory().create(JcrLexicon.PREDECESSORS,
832                                                                                       new Object[] {refFactory.create(versionUuid)});
833 
834         // This batch will get executed as part of the save
835         batch.set(isCheckedOut, versionHistory, baseVersion, predecessors).on(node.getPath()).and();
836 
837         Path storagePath = historyPath.getParent();
838         Node<JcrNodePayload, JcrPropertyPayload> storageNode = cache().findNode(null, storagePath);
839 
840         cache().refresh(storageNode.getNodeId(), storagePath, false);
841     }
842 
843     void initializeVersionStorageFor( Node<JcrNodePayload, JcrPropertyPayload> node,
844                                       UUID historyUuid,
845                                       UUID originalVersionUuid,
846                                       UUID versionUuid ) {
847         JcrNodePayload payload = node.getPayload();
848 
849         Graph systemGraph = session().repository().createSystemGraph(context());
850         Batch systemBatch = systemGraph.batch();
851 
852         Name primaryTypeName = payload.getPrimaryTypeName();
853         List<Name> mixinTypeNames = payload.getMixinTypeNames();
854 
855         PropertyInfo<JcrPropertyPayload> jcrUuidProp = node.getProperty(JcrLexicon.UUID);
856 
857         UUID jcrUuid = uuid(jcrUuidProp.getProperty().getFirstValue());
858         Path historyPath = versionHistoryPathFor(jcrUuid);
859 
860         systemBatch.create(historyPath)
861                    .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION_HISTORY)
862                    .and(JcrLexicon.VERSIONABLE_UUID, jcrUuid)
863                    .and(JcrLexicon.UUID, historyUuid)
864                    .and();
865 
866         if (originalVersionUuid != null) {
867             systemBatch.set(JcrLexicon.COPIED_FROM).on(historyPath).to(originalVersionUuid).and();
868         }
869 
870         Path versionLabelsPath = path(historyPath, JcrLexicon.VERSION_LABELS);
871         systemBatch.create(versionLabelsPath).with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION_LABELS).and();
872 
873         Path rootVersionPath = path(historyPath, JcrLexicon.ROOT_VERSION);
874         DateTime now = context().getValueFactories().getDateFactory().create();
875         systemBatch.create(rootVersionPath)
876                    .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.VERSION)
877                    .and(JcrLexicon.CREATED, now)
878                    .and(JcrLexicon.UUID, versionUuid)
879                    .and();
880 
881         Path frozenVersionPath = path(rootVersionPath, JcrLexicon.FROZEN_NODE);
882         systemBatch.create(frozenVersionPath)
883                    .with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FROZEN_NODE)
884                    .and(JcrLexicon.FROZEN_UUID, jcrUuid)
885                    .and(JcrLexicon.FROZEN_PRIMARY_TYPE, primaryTypeName)
886                    .and(JcrLexicon.FROZEN_MIXIN_TYPES, mixinTypeNames)
887                    .and();
888 
889         systemBatch.execute();
890 
891     }
892 
893     @NotThreadSafe
894     private class RestoreCommand {
895 
896         private Map<Version, AbstractJcrNode> existingVersions;
897         private Set<Path> versionRootPaths;
898         private Collection<Version> nonExistingVersions;
899         private boolean removeExisting;
900         private String labelToRestore;
901         private Map<AbstractJcrNode, AbstractJcrNode> changedNodes;
902 
903         public RestoreCommand( Map<Version, AbstractJcrNode> existingVersions,
904                                Set<Path> versionRootPaths,
905                                Collection<Version> nonExistingVersions,
906                                String labelToRestore,
907                                boolean removeExisting ) {
908             super();
909             this.existingVersions = existingVersions;
910             this.versionRootPaths = versionRootPaths;
911             this.nonExistingVersions = nonExistingVersions;
912             this.removeExisting = removeExisting;
913             this.labelToRestore = labelToRestore;
914 
915             // The default size for a HashMap is pretty low and this could get big fast
916             this.changedNodes = new HashMap<AbstractJcrNode, AbstractJcrNode>(100);
917         }
918 
919         void execute() throws RepositoryException {
920             Collection<Version> versionsToCheck = new ArrayList<Version>(existingVersions.keySet());
921             for (Version version : versionsToCheck) {
922                 AbstractJcrNode root = existingVersions.get(version);
923                 // This can happen if the version was already restored in another node
924                 if (root == null) continue;
925 
926                 // This updates the changedNodes and nonExistingVersions fields as a side effect
927                 restoreNodeMixins(frozenNodeFor(version), root);
928                 restoreNode(frozenNodeFor(version), root, dateTime(version.getCreated()));
929                 clearCheckoutStatus(frozenNodeFor(version), root);
930             }
931 
932             if (!nonExistingVersions.isEmpty()) {
933                 StringBuilder versions = new StringBuilder();
934                 boolean first = true;
935                 for (Version version : nonExistingVersions) {
936                     if (!first) {
937                         versions.append(", ");
938                     } else {
939                         first = false;
940                     }
941                     versions.append(version.getName());
942                 }
943                 throw new VersionException(JcrI18n.unrootedVersionsInRestore.text(versions.toString()));
944             }
945 
946             for (Map.Entry<AbstractJcrNode, AbstractJcrNode> changedNode : changedNodes.entrySet()) {
947                 restoreProperties(changedNode.getKey(), changedNode.getValue());
948             }
949         }
950 
951         /**
952          * Restores the child nodes and mixin types for {@code targetNode} based on the frozen version stored at {@code
953          * sourceNode}. This method will remove and add child nodes as necessary based on the documentation in the JCR 2.0
954          * specification (sections 15.7), but this method will not modify properties (other than jcr:mixinTypes, jcr:baseVersion,
955          * and jcr:isCheckedOut).
956          * 
957          * @param sourceNode a node in the subgraph of frozen nodes under a version; may not be null, but may be a node with
958          *        primary type of nt:version or nt:versionedChild
959          * @param targetNode the node to be updated based on {@code sourceNode}; may not be null
960          * @param checkinTime the time at which the version that instigated this restore was checked in; may not be null
961          * @throws RepositoryException if an error occurs accessing the repository
962          */
963         private void restoreNode( AbstractJcrNode sourceNode,
964                                   AbstractJcrNode targetNode,
965                                   DateTime checkinTime ) throws RepositoryException {
966             changedNodes.put(sourceNode, targetNode);
967 
968             NodeEditor targetEditor = targetNode.editor();
969             Node<JcrNodePayload, JcrPropertyPayload> targetNodeInfo = targetNode.nodeInfo();
970             Node<JcrNodePayload, JcrPropertyPayload> sourceNodeInfo = sourceNode.nodeInfo();
971 
972             Set<Node<JcrNodePayload, JcrPropertyPayload>> versionedChildrenThatShouldNotBeRestored = new HashSet<Node<JcrNodePayload, JcrPropertyPayload>>();
973 
974             // Try to match the existing nodes with nodes from the version to be restored
975             Map<Node<JcrNodePayload, JcrPropertyPayload>, Node<JcrNodePayload, JcrPropertyPayload>> presentInBoth = new HashMap<Node<JcrNodePayload, JcrPropertyPayload>, Node<JcrNodePayload, JcrPropertyPayload>>();
976 
977             // Start with all target children in this set and pull them out as matches are found
978             List<Node<JcrNodePayload, JcrPropertyPayload>> inTargetOnly = copyOf(targetNodeInfo.getChildren(),
979                                                                                  targetNodeInfo.getChildrenCount());
980 
981             // Start with no source children in this set, but add them in when no match is found
982             Map<Node<JcrNodePayload, JcrPropertyPayload>, Node<JcrNodePayload, JcrPropertyPayload>> inSourceOnly = new HashMap<Node<JcrNodePayload, JcrPropertyPayload>, Node<JcrNodePayload, JcrPropertyPayload>>();
983 
984             // Map the source children to existing target children where possible
985             for (Node<JcrNodePayload, JcrPropertyPayload> sourceChild : sourceNodeInfo.getChildren()) {
986                 boolean isVersionedChild = JcrNtLexicon.VERSIONED_CHILD.equals(name(sourceChild.getProperty(JcrLexicon.PRIMARY_TYPE)
987                                                                                                .getProperty()
988                                                                                                .getFirstValue()));
989                 Node<JcrNodePayload, JcrPropertyPayload> resolvedNode = resolveSourceNode(sourceChild, checkinTime);
990                 Node<JcrNodePayload, JcrPropertyPayload> match = findMatchFor(resolvedNode);
991 
992                 if (match != null) {
993                     if (isVersionedChild) {
994                         if (!removeExisting) {
995                             Object rawUuid = match.getProperty(JcrLexicon.UUID).getProperty().getFirstValue();
996                             String uuid = rawUuid == null ? null : rawUuid.toString();
997                             throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(uuid,
998                                                                                                  workspace().getName(),
999                                                                                                  match.getPath()));
1000                         }
1001                         // use match directly
1002                         versionedChildrenThatShouldNotBeRestored.add(match);
1003                     }
1004                     inTargetOnly.remove(match);
1005                     presentInBoth.put(sourceChild, match);
1006 
1007                 } else {
1008                     inSourceOnly.put(sourceChild, resolvedNode);
1009                 }
1010             }
1011 
1012             // Remove all the extraneous children of the target node
1013             for (Node<JcrNodePayload, JcrPropertyPayload> targetChild : inTargetOnly) {
1014                 switch (nodeDefinitionFor(targetChild).getOnParentVersion()) {
1015                     case OnParentVersionAction.COPY:
1016                     case OnParentVersionAction.ABORT:
1017                     case OnParentVersionAction.VERSION:
1018                         targetEditor.destroyChild(targetChild);
1019                         break;
1020 
1021                     case OnParentVersionAction.COMPUTE:
1022                         // Technically, this should reinitialize the node per its defaults.
1023                     case OnParentVersionAction.INITIALIZE:
1024                     case OnParentVersionAction.IGNORE:
1025                         // Do nothing
1026                 }
1027             }
1028 
1029             LinkedList<Node<JcrNodePayload, JcrPropertyPayload>> reversedChildren = new LinkedList<Node<JcrNodePayload, JcrPropertyPayload>>();
1030             for (Node<JcrNodePayload, JcrPropertyPayload> sourceChild : sourceNodeInfo.getChildren()) {
1031                 reversedChildren.addFirst(sourceChild);
1032             }
1033 
1034             // Now walk through the source node children (in reversed order), inserting children as needed
1035             // The order is reversed because SessionCache$NodeEditor supports orderBefore, but not orderAfter
1036             Node<JcrNodePayload, JcrPropertyPayload> prevChild = null;
1037             for (Node<JcrNodePayload, JcrPropertyPayload> sourceChild : reversedChildren) {
1038                 Node<JcrNodePayload, JcrPropertyPayload> targetChild = presentInBoth.get(sourceChild);
1039                 Node<JcrNodePayload, JcrPropertyPayload> resolvedChild;
1040 
1041                 AbstractJcrNode sourceChildNode;
1042                 AbstractJcrNode targetChildNode;
1043 
1044                 boolean shouldRestore = !versionedChildrenThatShouldNotBeRestored.contains(targetChild);
1045 
1046                 if (targetChild != null) {
1047                     // Reorder if necessary
1048                     resolvedChild = resolveSourceNode(sourceChild, checkinTime);
1049 
1050                     sourceChildNode = cache().findJcrNode(resolvedChild.getNodeId(), resolvedChild.getPath());
1051                     targetChildNode = cache().findJcrNode(targetChild.getNodeId(), targetChild.getPath());
1052 
1053                 } else {
1054                     // Pull the resolved node
1055                     resolvedChild = inSourceOnly.get(sourceChild);
1056                     sourceChildNode = cache().findJcrNode(resolvedChild.getNodeId(), resolvedChild.getPath());
1057 
1058                     Name primaryTypeName = name(resolvedChild.getProperty(JcrLexicon.FROZEN_PRIMARY_TYPE)
1059                                                              .getProperty()
1060                                                              .getFirstValue());
1061                     PropertyInfo<JcrPropertyPayload> uuidProp = resolvedChild.getProperty(JcrLexicon.FROZEN_UUID);
1062                     UUID desiredUuid = uuid(uuidProp.getProperty().getFirstValue());
1063 
1064                     targetChildNode = targetEditor.createChild(sourceChild.getName(), desiredUuid, primaryTypeName);
1065 
1066                     assert shouldRestore == true;
1067                 }
1068 
1069                 if (shouldRestore) {
1070                     // Have to do this first, as the properties below only exist for mix:versionable nodes
1071                     restoreNodeMixins(sourceChildNode, targetChildNode);
1072 
1073                     if (sourceChildNode.getParent().isNodeType(JcrNtLexicon.VERSION)) {
1074                         clearCheckoutStatus(sourceChildNode, targetChildNode);
1075                     }
1076                     restoreNode(sourceChildNode, targetChildNode, checkinTime);
1077                 }
1078 
1079                 orderBefore(sourceChild, prevChild, targetEditor);
1080                 prevChild = sourceChild;
1081             }
1082         }
1083 
1084         private void clearCheckoutStatus( AbstractJcrNode sourceChildNode,
1085                                           AbstractJcrNode targetChildNode ) throws RepositoryException {
1086 
1087             NodeEditor editor = targetChildNode.editor();
1088             editor.setProperty(JcrLexicon.IS_CHECKED_OUT, targetChildNode.valueFrom(PropertyType.BOOLEAN, false), false, false);
1089             editor.setProperty(JcrLexicon.BASE_VERSION, targetChildNode.valueFrom(sourceChildNode.getParent()), false, false);
1090 
1091         }
1092 
1093         /**
1094          * Moves {@code targetNode} immediately before {@code beforeNode} under their shared parent. This version is very
1095          * inefficient in that it always tries to move the node, regardless of whether a move is actually required.
1096          * <p>
1097          * The key postcondition for this method is that {@code targetNode} must be the last "versioned" child node before {@code
1098          * beforeNode}, although {@code targetNode} need not be the immediate predecessor of {@code beforeNode} if all intervening
1099          * nodes are not "versioned". That is, there can be nodes between {@code targetNode} and {@code beforeNode} as long as
1100          * these nodes all have a {@link NodeDefinition node definition} with an {@link NodeDefinition#getOnParentVersion()
1101          * onParentVersionAction} of IGNORE, COMPUTE, or INITIALIZE.
1102          * </p>
1103          * 
1104          * @param targetNode the node to be reordered; may not be null
1105          * @param beforeNode the node that must succeed {@code targetNode}; null indicates that {@code targetNode} comes last in
1106          *        the list of "versionable" child nodes
1107          * @param parentEditor the {@link NodeEditor editor} for the parent node
1108          * @throws RepositoryException if an error occurs while accessing the repository
1109          */
1110         private void orderBefore( Node<JcrNodePayload, JcrPropertyPayload> targetNode,
1111                                   Node<JcrNodePayload, JcrPropertyPayload> beforeNode,
1112                                   NodeEditor parentEditor ) throws RepositoryException {
1113             Segment beforeSegment = beforeNode == null ? null : beforeNode.getSegment();
1114 
1115             parentEditor.orderChildBefore(targetNode.getSegment(), beforeSegment);
1116 
1117         }
1118 
1119         /**
1120          * Adds any missing mixin types from the source node to the target node
1121          * 
1122          * @param sourceNode the frozen source node; may not be be null
1123          * @param targetNode the target node; may not be null
1124          * @throws RepositoryException if an error occurs while accessing the repository or adding the mixin types
1125          */
1126         private void restoreNodeMixins( AbstractJcrNode sourceNode,
1127                                         AbstractJcrNode targetNode ) throws RepositoryException {
1128             AbstractJcrProperty mixinTypesProp = sourceNode.getProperty(JcrLexicon.FROZEN_MIXIN_TYPES);
1129             NodeEditor childEditor = targetNode.editor();
1130             Object[] mixinTypeNames = mixinTypesProp == null ? EMPTY_OBJECT_ARRAY : mixinTypesProp.property().getValuesAsArray();
1131 
1132             Collection<Name> currentMixinTypes = new HashSet<Name>(targetNode.getMixinTypeNames());
1133 
1134             for (int i = 0; i < mixinTypeNames.length; i++) {
1135                 Name mixinTypeName = name(mixinTypeNames[i]);
1136 
1137                 if (!currentMixinTypes.remove(mixinTypeName)) {
1138                     JcrNodeType mixinType = session().nodeTypeManager().getNodeType(mixinTypeName);
1139                     childEditor.addMixin(mixinType);
1140                 }
1141             }
1142 
1143         }
1144 
1145         /**
1146          * Restores the properties on the target node based on the stored properties on the source node. The restoration process
1147          * is based on the documentation in sections 8.2.7 and 8.2.11 of the JCR 1.0.1 specification.
1148          * 
1149          * @param sourceNode the frozen source node; may not be be null
1150          * @param targetNode the target node; may not be null
1151          * @throws RepositoryException if an error occurs while accessing the repository or modifying the properties
1152          */
1153         private void restoreProperties( AbstractJcrNode sourceNode,
1154                                         AbstractJcrNode targetNode ) throws RepositoryException {
1155             NodeEditor childEditor = targetNode.editor();
1156             Map<Name, PropertyInfo<JcrPropertyPayload>> sourcePropertyNames = new HashMap<Name, PropertyInfo<JcrPropertyPayload>>();
1157             for (PropertyInfo<JcrPropertyPayload> propInfo : sourceNode.nodeInfo().getProperties()) {
1158                 if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(propInfo.getName())) {
1159                     sourcePropertyNames.put(propInfo.getName(), propInfo);
1160                 }
1161             }
1162 
1163             Collection<PropertyInfo<JcrPropertyPayload>> targetProps = new ArrayList<PropertyInfo<JcrPropertyPayload>>(
1164                                                                                                                        targetNode.nodeInfo()
1165                                                                                                                                  .getProperties());
1166             for (PropertyInfo<JcrPropertyPayload> propInfo : targetProps) {
1167                 Name propName = propInfo.getName();
1168 
1169                 if (sourcePropertyNames.containsKey(propName)) {
1170                     // Overwrite the current property with the property from the version
1171                     restoreProperty(sourcePropertyNames.get(propName).getPayload().getJcrProperty(), childEditor);
1172                     sourcePropertyNames.remove(propName);
1173                 } else {
1174                     PropertyDefinitionId propDefnId = propInfo.getPayload().getPropertyDefinitionId();
1175                     PropertyDefinition propDefn = session().nodeTypeManager().getPropertyDefinition(propDefnId);
1176 
1177                     switch (propDefn.getOnParentVersion()) {
1178                         case OnParentVersionAction.COPY:
1179                         case OnParentVersionAction.ABORT:
1180                         case OnParentVersionAction.VERSION:
1181                             childEditor.removeProperty(propName);
1182                             break;
1183 
1184                         case OnParentVersionAction.COMPUTE:
1185                         case OnParentVersionAction.INITIALIZE:
1186                         case OnParentVersionAction.IGNORE:
1187                             // Do nothing
1188                     }
1189                 }
1190             }
1191 
1192             for (Map.Entry<Name, PropertyInfo<JcrPropertyPayload>> sourceProperty : sourcePropertyNames.entrySet()) {
1193                 restoreProperty(sourceProperty.getValue().getPayload().getJcrProperty(), childEditor);
1194             }
1195         }
1196 
1197         /**
1198          * Resolves the given source node into a frozen node. This may be as simple as returning the node itself (if it has a
1199          * primary type of nt:frozenNode) or converting the node to a version history, finding the best match from the versions in
1200          * that version history, and returning the frozen node for the best match (if the original source node has a primary type
1201          * of nt:versionedChild).
1202          * 
1203          * @param sourceNode the node for which the corresponding frozen node should be returned; may not be null
1204          * @param checkinTime the checkin time against which the versions in the version history should be matched; may not be
1205          *        null
1206          * @return the frozen node that corresponds to the give source node; may not be null
1207          * @throws RepositoryException if an error occurs while accessing the repository
1208          * @see #closestMatchFor(JcrVersionHistoryNode, DateTime)
1209          */
1210         private Node<JcrNodePayload, JcrPropertyPayload> resolveSourceNode( Node<JcrNodePayload, JcrPropertyPayload> sourceNode,
1211                                                                             DateTime checkinTime ) throws RepositoryException {
1212             Name sourcePrimaryTypeName = name(sourceNode.getProperty(JcrLexicon.PRIMARY_TYPE).getProperty().getFirstValue());
1213 
1214             if (JcrNtLexicon.FROZEN_NODE.equals(sourcePrimaryTypeName)) return sourceNode;
1215             assert JcrNtLexicon.VERSIONED_CHILD.equals(sourcePrimaryTypeName);
1216 
1217             // Must be a versioned child - try to see if it's one of the versions we're restoring
1218             PropertyInfo<JcrPropertyPayload> historyUuidProp = sourceNode.getProperty(JcrLexicon.CHILD_VERSION_HISTORY);
1219             UUID uuid = uuid(historyUuidProp.getProperty().getFirstValue());
1220             assert uuid != null;
1221             String uuidString = uuid.toString();
1222 
1223             /*
1224              * First try to find a match among the rootless versions in this restore operation
1225              */
1226             for (Version version : nonExistingVersions) {
1227                 if (uuidString.equals(version.getContainingHistory().getIdentifier())) {
1228                     JcrVersionNode versionNode = (JcrVersionNode)version;
1229                     nonExistingVersions.remove(version);
1230                     return versionNode.getFrozenNode().nodeInfo();
1231                 }
1232             }
1233 
1234             /*
1235              * Then check the rooted versions in this restore operation
1236              */
1237             for (Version version : existingVersions.keySet()) {
1238                 if (uuidString.equals(version.getContainingHistory().getIdentifier())) {
1239                     JcrVersionNode versionNode = (JcrVersionNode)version;
1240                     existingVersions.remove(version);
1241                     return versionNode.getFrozenNode().nodeInfo();
1242                 }
1243             }
1244 
1245             /*
1246              * If there was a label for this restore operation, try to match that way
1247              */
1248             JcrVersionHistoryNode versionHistory = (JcrVersionHistoryNode)cache().findJcrNode(Location.create(uuid));
1249 
1250             if (labelToRestore != null) {
1251                 try {
1252                     JcrVersionNode versionNode = versionHistory.getVersionByLabel(labelToRestore);
1253                     return versionNode.getFrozenNode().nodeInfo();
1254                 } catch (VersionException noVersionWithThatLabel) {
1255                     // This can happen if there's no version with that label - valid
1256                 }
1257             }
1258 
1259             /*
1260              * If all else fails, find the last version checked in before the checkin time for the version being restored
1261              */
1262             AbstractJcrNode match = closestMatchFor(versionHistory, checkinTime);
1263 
1264             return match.nodeInfo();
1265         }
1266 
1267         /**
1268          * Finds a node that has the same UUID as is specified in the jcr:frozenUuid property of {@code sourceNode}. If a match
1269          * exists and it is a descendant of one of the {@link #versionRootPaths root paths} for this restore operation, it is
1270          * returned. If a match exists but is not a descendant of one of the root paths for this restore operation, either an
1271          * exception is thrown (if {@link #removeExisting} is false) or the match is deleted and null is returned (if
1272          * {@link #removeExisting} is true).
1273          * 
1274          * @param sourceNode the node for which the match should be checked; may not be null
1275          * @return the existing node with the same UUID as is specified in the jcr:frozenUuid property of {@code sourceNode}; null
1276          *         if no node exists with that UUID
1277          * @throws ItemExistsException if {@link #removeExisting} is false and the node is not a descendant of any of the
1278          *         {@link #versionRootPaths root paths} for this restore command
1279          * @throws RepositoryException if any other error occurs while accessing the repository
1280          */
1281         private Node<JcrNodePayload, JcrPropertyPayload> findMatchFor( Node<JcrNodePayload, JcrPropertyPayload> sourceNode )
1282             throws ItemExistsException, RepositoryException {
1283 
1284             PropertyInfo<JcrPropertyPayload> uuidProp = sourceNode.getProperty(JcrLexicon.FROZEN_UUID);
1285             UUID sourceUuid = uuid(uuidProp.getProperty().getFirstValue());
1286 
1287             try {
1288                 AbstractJcrNode match = cache().findJcrNode(Location.create(sourceUuid));
1289 
1290                 if (nodeIsOutsideRestoredForest(match)) return null;
1291 
1292                 return match.nodeInfo();
1293             } catch (ItemNotFoundException infe) {
1294                 return null;
1295             }
1296         }
1297 
1298         /**
1299          * Copies the given {@link Iterable} into a {@link List} and returns that list.
1300          * 
1301          * @param rawElements the iterator containing the items that should be copied; may not be null
1302          * @param size the number of elements in the iterator; may not be negative, but inaccurate values will lead to a badly
1303          *        sized list
1304          * @return a list containing the same elements as {@code rawElements} in the same order; never null
1305          */
1306         private List<Node<JcrNodePayload, JcrPropertyPayload>> copyOf( Iterable<Node<JcrNodePayload, JcrPropertyPayload>> rawElements,
1307                                                                        int size ) {
1308             List<Node<JcrNodePayload, JcrPropertyPayload>> newList = new ArrayList<Node<JcrNodePayload, JcrPropertyPayload>>(size);
1309             for (Node<JcrNodePayload, JcrPropertyPayload> node : rawElements) {
1310                 newList.add(node);
1311             }
1312             return newList;
1313         }
1314 
1315         /**
1316          * Checks if the given node is outside any of the root paths for this restore command. If this occurs, a special check of
1317          * the {@link #removeExisting} flag must be performed.
1318          * 
1319          * @param node the node to check; may not be null
1320          * @return true if the node is not a descendant of any of the {@link #versionRootPaths root paths} for this restore
1321          *         command, false otherwise.
1322          * @throws ItemExistsException if {@link #removeExisting} is false and the node is not a descendant of any of the
1323          *         {@link #versionRootPaths root paths} for this restore command
1324          * @throws RepositoryException if any other error occurs while accessing the repository
1325          */
1326         private boolean nodeIsOutsideRestoredForest( AbstractJcrNode node ) throws ItemExistsException, RepositoryException {
1327             Path nodePath = node.path();
1328 
1329             for (Path rootPath : versionRootPaths) {
1330                 if (nodePath.isAtOrBelow(rootPath)) return false;
1331             }
1332 
1333             if (!removeExisting) {
1334                 throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(node.uuid(),
1335                                                                                      workspace().getName(),
1336                                                                                      node.getPath()));
1337             }
1338 
1339             node.remove();
1340             return true;
1341         }
1342 
1343         /**
1344          * Returns the most recent version for the given version history that was checked in before the given time.
1345          * 
1346          * @param versionHistory the version history to check; may not be null
1347          * @param checkinTime the checkin time against which the versions in the version history should be matched; may not be
1348          *        null
1349          * @return the {@link JcrVersionNode#getFrozenNode() frozen node} under the most recent {@link Version version} for the
1350          *         version history that was checked in before {@code checkinTime}; never null
1351          * @throws RepositoryException if an error occurs accessing the repository
1352          */
1353         private AbstractJcrNode closestMatchFor( JcrVersionHistoryNode versionHistory,
1354                                                  DateTime checkinTime ) throws RepositoryException {
1355             DateTimeFactory dateFactory = context().getValueFactories().getDateFactory();
1356 
1357             VersionIterator iter = versionHistory.getAllVersions();
1358             Map<DateTime, Version> versions = new HashMap<DateTime, Version>((int)iter.getSize());
1359 
1360             while (iter.hasNext()) {
1361                 Version version = iter.nextVersion();
1362                 versions.put(dateFactory.create(version.getCreated()), version);
1363             }
1364 
1365             List<DateTime> versionDates = new ArrayList<DateTime>(versions.keySet());
1366             Collections.sort(versionDates);
1367 
1368             for (int i = versionDates.size() - 1; i >= 0; i--) {
1369                 if (versionDates.get(i).isBefore(checkinTime)) {
1370                     Version version = versions.get(versionDates.get(i));
1371                     return ((JcrVersionNode)version).getFrozenNode();
1372                 }
1373             }
1374 
1375             throw new IllegalStateException("First checkin must be before the checkin time of the node to be restored");
1376         }
1377     }
1378 
1379     @NotThreadSafe
1380     private class MergeCommand {
1381         private final Collection<AbstractJcrNode> failures;
1382         private final AbstractJcrNode targetNode;
1383         private final boolean bestEffort;
1384         private final boolean isShallow;
1385         private final JcrSession sourceSession;
1386         private final String workspaceName;
1387 
1388         public MergeCommand( AbstractJcrNode targetNode,
1389                              JcrSession sourceSession,
1390                              boolean bestEffort,
1391                              boolean isShallow ) {
1392             super();
1393             this.targetNode = targetNode;
1394             this.sourceSession = sourceSession;
1395             this.bestEffort = bestEffort;
1396             this.isShallow = isShallow;
1397 
1398             this.workspaceName = sourceSession.getWorkspace().getName();
1399             this.failures = new LinkedList<AbstractJcrNode>();
1400         }
1401 
1402         final JcrChildNodeIterator getFailures() {
1403             return new JcrChildNodeIterator(failures, failures.size());
1404         }
1405 
1406         void execute() throws RepositoryException {
1407             doMerge(targetNode);
1408         }
1409 
1410         /*
1411         let n' be the corresponding node of n in ws'. 
1412         if no such n' doleave(n).
1413 
1414         else if n is not versionable doupdate(n, n'). 
1415         else if n' is not versionable doleave(n). 
1416         let v be base version of n. 
1417         let v' be base version of n'.
1418         if v' is an eventual successor of v and n is not checked-in doupdate(n, n').
1419         else if v is equal to or an eventual predecessor of v' doleave(n). 
1420         else dofail(n, v').
1421          */
1422         private void doMerge( AbstractJcrNode targetNode ) throws RepositoryException {
1423             // n is targetNode
1424             // n' is sourceNode
1425             Path sourcePath = targetNode.correspondingNodePath(workspaceName);
1426 
1427             AbstractJcrNode sourceNode;
1428             try {
1429                 sourceNode = sourceSession.getNode(sourcePath);
1430             } catch (ItemNotFoundException infe) {
1431                 doLeave(targetNode);
1432                 return;
1433             }
1434 
1435             if (!targetNode.isNodeType(JcrMixLexicon.VERSIONABLE)) {
1436                 doUpdate(targetNode, sourceNode);
1437                 return;
1438             } else if (!sourceNode.isNodeType(JcrMixLexicon.VERSIONABLE)) {
1439                 doLeave(targetNode);
1440                 return;
1441             }
1442 
1443             JcrVersionNode sourceVersion = sourceNode.getBaseVersion();
1444             JcrVersionNode targetVersion = targetNode.getBaseVersion();
1445 
1446             if (sourceVersion.isSuccessorOf(targetVersion) && !targetNode.isCheckedOut()) {
1447                 doUpdate(targetNode, sourceNode);
1448                 return;
1449             }
1450 
1451             if (targetVersion.isSuccessorOf(sourceVersion) || targetVersion.uuid().equals(sourceVersion.uuid())) {
1452                 doLeave(targetNode);
1453                 return;
1454             }
1455 
1456             doFail(targetNode, sourceVersion);
1457         }
1458 
1459         /*
1460         if isShallow = false
1461             for each child node c of n domerge(c).
1462          */
1463         private void doLeave( AbstractJcrNode targetNode ) throws RepositoryException {
1464             if (isShallow == false) {
1465 
1466                 for (NodeIterator iter = targetNode.getNodes(); iter.hasNext();) {
1467                     doMerge((AbstractJcrNode)iter.nextNode());
1468                 }
1469             }
1470         }
1471 
1472         /*
1473         replace set of properties of n with those of n'. 
1474         let S be the set of child nodes of n. 
1475         let S' be the set of child nodes of n'. 
1476 
1477         judging by the name of the child node:
1478         let C be the set of nodes in S and in S' 
1479         let D be the set of nodes in S but not in S'. 
1480         let D' be the set of nodes in S' but not in S.
1481         remove from n all child nodes in D. 
1482         for each child node of n' in D' copy it (and its subtree) to n
1483         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).
1484         for each child node m of n in C domerge(m).
1485          */
1486         private void doUpdate( AbstractJcrNode targetNode,
1487                                AbstractJcrNode sourceNode ) throws RepositoryException {
1488             restoreProperties(sourceNode, targetNode);
1489 
1490             LinkedHashMap<String, AbstractJcrNode> sourceNodes = childNodeMapFor(sourceNode);
1491             LinkedHashMap<String, AbstractJcrNode> targetNodes = childNodeMapFor(targetNode);
1492 
1493             // D' set in algorithm above
1494             Map<String, AbstractJcrNode> sourceOnly = new LinkedHashMap<String, AbstractJcrNode>(sourceNodes);
1495             sourceOnly.keySet().removeAll(targetNodes.keySet());
1496 
1497             for (AbstractJcrNode node : sourceOnly.values()) {
1498                 workspace().copy(workspaceName, node.getPath(), targetNode.getPath() + "/" + node.getName());
1499             }
1500 
1501             // D set in algorithm above
1502             LinkedHashMap<String, AbstractJcrNode> targetOnly = new LinkedHashMap<String, AbstractJcrNode>(targetNodes);
1503             targetOnly.keySet().removeAll(targetOnly.keySet());
1504 
1505             for (AbstractJcrNode node : targetOnly.values()) {
1506                 node.remove();
1507             }
1508 
1509             // C set in algorithm above
1510             Map<String, AbstractJcrNode> presentInBoth = new HashMap<String, AbstractJcrNode>(targetNodes);
1511             presentInBoth.keySet().retainAll(sourceNodes.keySet());
1512             for (AbstractJcrNode node : presentInBoth.values()) {
1513                 if (isShallow && node.isNodeType(JcrMixLexicon.VERSIONABLE)) continue;
1514                 doMerge(node);
1515             }
1516         }
1517 
1518         private LinkedHashMap<String, AbstractJcrNode> childNodeMapFor( AbstractJcrNode node ) throws RepositoryException {
1519             LinkedHashMap<String, AbstractJcrNode> childNodes = new LinkedHashMap<String, AbstractJcrNode>();
1520 
1521             for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
1522                 AbstractJcrNode child = (AbstractJcrNode)iter.nextNode();
1523                 childNodes.put(child.getName(), child);
1524             }
1525 
1526             return childNodes;
1527         }
1528 
1529         /*
1530         if bestEffort = false throw MergeException. 
1531         else add identifier of v' (if not already present) to the
1532             jcr:mergeFailed property of n, 
1533             add identifier of n to failedset, 
1534             if isShallow = false 
1535                 for each versionable child node c of n domerge(c)
1536          */
1537 
1538         private void doFail( AbstractJcrNode targetNode,
1539                              JcrVersionNode sourceVersion ) throws RepositoryException {
1540             if (!bestEffort) {
1541                 throw new MergeException();
1542             }
1543 
1544             NodeEditor targetEditor = targetNode.editor();
1545             if (targetNode.hasProperty(JcrLexicon.MERGE_FAILED)) {
1546                 JcrValue[] existingValues = (JcrValue[])targetNode.getProperty(JcrLexicon.MERGE_FAILED).getValues();
1547 
1548                 boolean found = false;
1549                 String sourceUuidString = sourceVersion.uuid().toString();
1550                 for (int i = 0; i < existingValues.length; i++) {
1551                     if (sourceUuidString.equals(existingValues[i].getString())) {
1552                         found = true;
1553                         break;
1554                     }
1555                 }
1556 
1557                 if (!found) {
1558                     JcrValue[] newValues = new JcrValue[existingValues.length + 1];
1559                     System.arraycopy(existingValues, 0, newValues, 0, existingValues.length);
1560                     newValues[newValues.length - 1] = targetNode.valueFrom(sourceVersion);
1561                     targetEditor.setProperty(JcrLexicon.MERGE_FAILED, newValues, PropertyType.REFERENCE, false);
1562                 }
1563 
1564             } else {
1565                 targetEditor.setProperty(JcrLexicon.MERGE_FAILED, targetNode.valueFrom(sourceVersion), false, false);
1566             }
1567             failures.add(targetNode);
1568 
1569             if (isShallow == false) {
1570                 for (NodeIterator iter = targetNode.getNodes(); iter.hasNext();) {
1571                     AbstractJcrNode childNode = (AbstractJcrNode)iter.nextNode();
1572 
1573                     if (childNode.isNodeType(JcrMixLexicon.VERSIONABLE)) {
1574                         doMerge(childNode);
1575                     }
1576                 }
1577             }
1578         }
1579 
1580         /**
1581          * Restores the properties on the target node based on the stored properties on the source node. The restoration process
1582          * involves copying over all of the properties on the source to the target.
1583          * 
1584          * @param sourceNode the source node; may not be be null
1585          * @param targetNode the target node; may not be null
1586          * @throws RepositoryException if an error occurs while accessing the repository or modifying the properties
1587          */
1588         private void restoreProperties( AbstractJcrNode sourceNode,
1589                                         AbstractJcrNode targetNode ) throws RepositoryException {
1590             NodeEditor childEditor = targetNode.editor();
1591             Map<Name, PropertyInfo<JcrPropertyPayload>> sourcePropertyNames = new HashMap<Name, PropertyInfo<JcrPropertyPayload>>();
1592             for (PropertyInfo<JcrPropertyPayload> propInfo : sourceNode.nodeInfo().getProperties()) {
1593                 if (!IGNORED_PROP_NAMES_FOR_RESTORE.contains(propInfo.getName())) {
1594                     sourcePropertyNames.put(propInfo.getName(), propInfo);
1595                 }
1596             }
1597 
1598             Collection<PropertyInfo<JcrPropertyPayload>> targetProps = new ArrayList<PropertyInfo<JcrPropertyPayload>>(
1599                                                                                                                        targetNode.nodeInfo()
1600                                                                                                                                  .getProperties());
1601             for (PropertyInfo<JcrPropertyPayload> propInfo : targetProps) {
1602                 Name propName = propInfo.getName();
1603 
1604                 if (sourcePropertyNames.containsKey(propName)) {
1605                     // Overwrite the current property with the property from the version
1606                     restoreProperty(sourcePropertyNames.get(propName).getPayload().getJcrProperty(), childEditor);
1607                     sourcePropertyNames.remove(propName);
1608                 } else {
1609                     PropertyDefinitionId propDefnId = propInfo.getPayload().getPropertyDefinitionId();
1610                     PropertyDefinition propDefn = session().nodeTypeManager().getPropertyDefinition(propDefnId);
1611 
1612                     switch (propDefn.getOnParentVersion()) {
1613                         case OnParentVersionAction.COPY:
1614                         case OnParentVersionAction.ABORT:
1615                         case OnParentVersionAction.VERSION:
1616                             childEditor.removeProperty(propName);
1617                             break;
1618 
1619                         case OnParentVersionAction.COMPUTE:
1620                         case OnParentVersionAction.INITIALIZE:
1621                         case OnParentVersionAction.IGNORE:
1622                             // Do nothing
1623                     }
1624                 }
1625             }
1626 
1627             for (Map.Entry<Name, PropertyInfo<JcrPropertyPayload>> sourceProperty : sourcePropertyNames.entrySet()) {
1628                 restoreProperty(sourceProperty.getValue().getPayload().getJcrProperty(), childEditor);
1629             }
1630         }
1631 
1632     }
1633 
1634     @Override
1635     public void cancelMerge( String absPath,
1636                              Version version ) throws VersionException, InvalidItemStateException, RepositoryException {
1637         cancelMerge(session.getNode(absPath), version);
1638     }
1639 
1640     /**
1641      * Throw an {@link UnsupportedRepositoryOperationException} if the node is not versionable (i.e.,
1642      * isNodeType(JcrMixLexicon.VERSIONABLE) == false).
1643      * 
1644      * @param node the node to check
1645      * @throws UnsupportedRepositoryOperationException if <code>!isNodeType({@link JcrMixLexicon#VERSIONABLE})</code>
1646      * @throws RepositoryException if an error occurs reading the node types for this node
1647      */
1648     private void checkVersionable( AbstractJcrNode node ) throws UnsupportedRepositoryOperationException, RepositoryException {
1649         if (!node.isNodeType(JcrMixLexicon.VERSIONABLE)) {
1650             throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text());
1651         }
1652     }
1653 
1654     @Override
1655     public Version checkin( String absPath )
1656         throws VersionException, InvalidItemStateException, LockException, RepositoryException {
1657         return checkin(session.getNode(absPath));
1658     }
1659 
1660     @Override
1661     public void checkout( String absPath ) throws LockException, RepositoryException {
1662         checkout(session.getNode(absPath));
1663     }
1664 
1665     @Override
1666     public Version checkpoint( String absPath )
1667         throws VersionException, InvalidItemStateException, LockException, RepositoryException {
1668         Version version = checkin(absPath);
1669         checkout(absPath);
1670         return version;
1671     }
1672 
1673     @Override
1674     public void doneMerge( String absPath,
1675                            Version version ) throws VersionException, InvalidItemStateException, RepositoryException {
1676         doneMerge(session.getNode(absPath), version);
1677     }
1678 
1679     @Override
1680     public javax.jcr.Node getActivity() throws UnsupportedRepositoryOperationException, RepositoryException {
1681         throw new UnsupportedRepositoryOperationException();
1682     }
1683 
1684     @Override
1685     public Version getBaseVersion( String absPath ) throws RepositoryException {
1686         return session.getNode(absPath).getBaseVersion();
1687     }
1688 
1689     @Override
1690     public VersionHistory getVersionHistory( String absPath ) throws UnsupportedRepositoryOperationException, RepositoryException {
1691         return session.getNode(absPath).getVersionHistory();
1692     }
1693 
1694     @Override
1695     public boolean isCheckedOut( String absPath ) throws RepositoryException {
1696         AbstractJcrNode node = session.getNode(absPath);
1697         return node.isCheckedOut();
1698     }
1699 
1700     @SuppressWarnings( "unused" )
1701     @Override
1702     public NodeIterator merge( javax.jcr.Node activityNode )
1703         throws VersionException, AccessDeniedException, MergeException, LockException, InvalidItemStateException,
1704         RepositoryException {
1705         throw new UnsupportedRepositoryOperationException();
1706     }
1707 
1708     @Override
1709     public NodeIterator merge( String absPath,
1710                                String srcWorkspace,
1711                                boolean bestEffort,
1712                                boolean isShallow )
1713         throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException,
1714         RepositoryException {
1715         CheckArg.isNotNull(srcWorkspace, "source workspace name");
1716 
1717         AbstractJcrNode node = session.getNode(absPath);
1718 
1719         return merge(node, srcWorkspace, bestEffort, isShallow);
1720     }
1721 
1722     @Override
1723     public NodeIterator merge( String absPath,
1724                                String srcWorkspace,
1725                                boolean bestEffort )
1726         throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException,
1727         RepositoryException {
1728         return merge(absPath, srcWorkspace, bestEffort, false);
1729     }
1730 
1731     @Override
1732     public void restore( String absPath,
1733                          String versionName,
1734                          boolean removeExisting )
1735         throws VersionException, ItemExistsException, LockException, InvalidItemStateException, RepositoryException {
1736 
1737         Version version = null;
1738 
1739         // See if the node at absPath exists and has version storage.
1740         Path path = session.pathFor(absPath, "absPath");
1741 
1742         AbstractJcrNode existingNode = session.getNode(path);
1743         VersionHistory historyNode = existingNode.getVersionHistory();
1744         if (historyNode != null) {
1745             version = historyNode.getVersion(versionName);
1746         }
1747 
1748         assert version != null;
1749 
1750         // AbstractJcrNode versionStorage = session.getRootNode().getNode(JcrLexicon.SYSTEM).getNode(JcrLexicon.VERSION_STORAGE);
1751         // assert versionStorage != null;
1752 
1753         restore(path, version, null, removeExisting);
1754     }
1755 
1756     @Override
1757     public void restore( String absPath,
1758                          Version version,
1759                          boolean removeExisting )
1760         throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, LockException,
1761         InvalidItemStateException, RepositoryException {
1762         Path path = session.pathFor(absPath, "absPath");
1763 
1764         restore(path, version, null, removeExisting);
1765     }
1766 
1767     @Override
1768     public void restore( Version version,
1769                          boolean removeExisting )
1770         throws VersionException, ItemExistsException, InvalidItemStateException, LockException, RepositoryException {
1771         AbstractJcrNode node = session.getNodeByIdentifier(version.getContainingHistory().getVersionableIdentifier());
1772         Path path = node.path();
1773 
1774         restore(path, version, null, removeExisting);
1775     }
1776 
1777     @Override
1778     public void restoreByLabel( String absPath,
1779                                 String versionLabel,
1780                                 boolean removeExisting )
1781         throws VersionException, ItemExistsException, LockException, InvalidItemStateException, RepositoryException {
1782         session.getNode(absPath).restoreByLabel(versionLabel, removeExisting);
1783     }
1784 
1785     @Override
1786     public javax.jcr.Node setActivity( javax.jcr.Node activity )
1787         throws UnsupportedRepositoryOperationException, RepositoryException {
1788         throw new UnsupportedRepositoryOperationException();
1789     }
1790 
1791     @SuppressWarnings( "unused" )
1792     @Override
1793     public void removeActivity( javax.jcr.Node activityNode )
1794         throws UnsupportedRepositoryOperationException, VersionException, RepositoryException {
1795         throw new UnsupportedRepositoryOperationException();
1796     }
1797 
1798     @Override
1799     public javax.jcr.Node createActivity( String title ) throws UnsupportedRepositoryOperationException, RepositoryException {
1800         throw new UnsupportedRepositoryOperationException();
1801     }
1802 
1803     @Override
1804     public javax.jcr.Node createConfiguration( String absPath )
1805         throws UnsupportedRepositoryOperationException, RepositoryException {
1806         throw new UnsupportedRepositoryOperationException();
1807     }
1808 
1809 }