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