001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.graph.connector.inmemory;
025    
026    import java.util.ArrayList;
027    import java.util.HashMap;
028    import java.util.Iterator;
029    import java.util.LinkedList;
030    import java.util.List;
031    import java.util.ListIterator;
032    import java.util.Map;
033    import java.util.Set;
034    import java.util.UUID;
035    import java.util.concurrent.locks.ReadWriteLock;
036    import java.util.concurrent.locks.ReentrantReadWriteLock;
037    import net.jcip.annotations.GuardedBy;
038    import net.jcip.annotations.NotThreadSafe;
039    import org.jboss.dna.common.util.CheckArg;
040    import org.jboss.dna.graph.ExecutionContext;
041    import org.jboss.dna.graph.property.Name;
042    import org.jboss.dna.graph.property.Path;
043    import org.jboss.dna.graph.property.Property;
044    import org.jboss.dna.graph.property.PropertyFactory;
045    import org.jboss.dna.graph.property.PropertyType;
046    import org.jboss.dna.graph.property.Reference;
047    import org.jboss.dna.graph.property.UuidFactory;
048    import org.jboss.dna.graph.property.ValueFactory;
049    import org.jboss.dna.graph.property.basic.RootPath;
050    import org.jboss.dna.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
051    
052    /**
053     * @author Randall Hauch
054     */
055    @NotThreadSafe
056    public class InMemoryRepository {
057    
058        protected final ReadWriteLock lock = new ReentrantReadWriteLock();
059        protected final UUID rootNodeUuid;
060        private final String sourceName;
061        private final String defaultWorkspaceName;
062        private final Map<String, Workspace> workspaces = new HashMap<String, Workspace>();
063    
064        public InMemoryRepository( String sourceName,
065                                   UUID rootNodeUuid ) {
066            this(sourceName, rootNodeUuid, null);
067        }
068    
069        public InMemoryRepository( String sourceName,
070                                   UUID rootNodeUuid,
071                                   String defaultWorkspaceName ) {
072            CheckArg.isNotEmpty(sourceName, "sourceName");
073            CheckArg.isNotNull(rootNodeUuid, "rootNodeUUID");
074            this.rootNodeUuid = rootNodeUuid;
075            this.sourceName = sourceName;
076            this.defaultWorkspaceName = defaultWorkspaceName != null ? defaultWorkspaceName : "";
077            // Create the default workspace ...
078            workspaces.put(this.defaultWorkspaceName, new Workspace(this.defaultWorkspaceName));
079        }
080    
081        /**
082         * @return sourceName
083         */
084        public String getSourceName() {
085            return sourceName;
086        }
087    
088        /**
089         * @return lock
090         */
091        public ReadWriteLock getLock() {
092            return lock;
093        }
094    
095        @GuardedBy( "getLock()" )
096        public Set<String> getWorkspaceNames() {
097            return workspaces.keySet();
098        }
099    
100        @GuardedBy( "getLock()" )
101        public Workspace getWorkspace( ExecutionContext context,
102                                       String name ) {
103            if (name == null) name = defaultWorkspaceName;
104            return workspaces.get(name);
105        }
106    
107        @GuardedBy( "getLock()" )
108        public Workspace createWorkspace( ExecutionContext context,
109                                          String name,
110                                          CreateConflictBehavior behavior ) {
111            String newName = name;
112            boolean conflictingName = workspaces.containsKey(newName);
113            if (conflictingName) {
114                switch (behavior) {
115                    case DO_NOT_CREATE:
116                        return null;
117                    case CREATE_WITH_ADJUSTED_NAME:
118                        int counter = 0;
119                        do {
120                            newName = name + (++counter);
121                        } while (workspaces.containsKey(newName));
122                        break;
123                }
124            }
125            assert workspaces.containsKey(newName) == false;
126            Workspace workspace = new Workspace(newName);
127            workspaces.put(newName, workspace);
128            return workspace;
129        }
130    
131        @GuardedBy( "getLock()" )
132        public Workspace createWorkspace( ExecutionContext context,
133                                          String name,
134                                          CreateConflictBehavior existingWorkspaceBehavior,
135                                          String nameOfWorkspaceToClone ) {
136            Workspace workspace = createWorkspace(context, name, existingWorkspaceBehavior);
137            if (workspace == null) {
138                // Unable to create because of a duplicate name ...
139                return null;
140            }
141            Workspace original = getWorkspace(context, nameOfWorkspaceToClone);
142            if (original != null) {
143                // Copy the properties of the root node ...
144                InMemoryNode root = workspace.getRoot();
145                InMemoryNode origRoot = original.getRoot();
146                root.getProperties().clear();
147                root.getProperties().putAll(origRoot.getProperties());
148    
149                // Loop over each child and call this method to copy the immediate children (and below).
150                // Note that this makes the copy have the same UUID as the original.
151                for (InMemoryNode originalNode : origRoot.getChildren()) {
152                    original.copyNode(context, originalNode, workspace, root, originalNode.getName().getName(), true, null);
153                }
154            }
155            return workspace;
156        }
157    
158        @GuardedBy( "getLock()" )
159        public boolean destroyWorkspace( String name ) {
160            return workspaces.remove(name) != null;
161        }
162    
163        protected class Workspace {
164            private final Map<UUID, InMemoryNode> nodesByUuid = new HashMap<UUID, InMemoryNode>();
165            private final String name;
166    
167            protected Workspace( String name ) {
168                assert name != null;
169                this.name = name;
170                // Create the root node ...
171                InMemoryNode root = new InMemoryNode(rootNodeUuid);
172                nodesByUuid.put(root.getUuid(), root);
173            }
174    
175            /**
176             * @return name
177             */
178            public String getName() {
179                return name;
180            }
181    
182            public InMemoryNode getRoot() {
183                return nodesByUuid.get(rootNodeUuid);
184            }
185    
186            public InMemoryNode getNode( UUID uuid ) {
187                assert uuid != null;
188                return nodesByUuid.get(uuid);
189            }
190    
191            protected Map<UUID, InMemoryNode> getNodesByUuid() {
192                return nodesByUuid;
193            }
194    
195            public InMemoryNode getNode( ExecutionContext context,
196                                         String path ) {
197                assert context != null;
198                assert path != null;
199                return getNode(context.getValueFactories().getPathFactory().create(path));
200            }
201    
202            /**
203             * Find a node with the given path.
204             * 
205             * @param path the path to the node; may not be null
206             * @return the node with the path, or null if the node does not exist
207             */
208            public InMemoryNode getNode( Path path ) {
209                assert path != null;
210                InMemoryNode node = getRoot();
211                for (Path.Segment segment : path) {
212                    InMemoryNode desiredChild = null;
213                    for (InMemoryNode child : node.getChildren()) {
214                        if (child == null) continue;
215                        Path.Segment childName = child.getName();
216                        if (childName == null) continue;
217                        if (childName.equals(segment)) {
218                            desiredChild = child;
219                            break;
220                        }
221                    }
222                    if (desiredChild != null) {
223                        node = desiredChild;
224                    } else {
225                        return null;
226                    }
227                }
228                return node;
229            }
230    
231            /**
232             * Find the lowest existing node along the path.
233             * 
234             * @param path the path to the node; may not be null
235             * @return the lowest existing node along the path, or the root node if no node exists on the path
236             */
237            public Path getLowestExistingPath( Path path ) {
238                assert path != null;
239                InMemoryNode node = getRoot();
240                int segmentNumber = 0;
241                for (Path.Segment segment : path) {
242                    InMemoryNode desiredChild = null;
243                    for (InMemoryNode child : node.getChildren()) {
244                        if (child == null) continue;
245                        Path.Segment childName = child.getName();
246                        if (childName == null) continue;
247                        if (childName.equals(segment)) {
248                            desiredChild = child;
249                            break;
250                        }
251                    }
252                    if (desiredChild != null) {
253                        node = desiredChild;
254                    } else {
255                        return path.subpath(0, segmentNumber);
256                    }
257                    ++segmentNumber;
258                }
259                return RootPath.INSTANCE;
260            }
261    
262            public void removeNode( ExecutionContext context,
263                                    InMemoryNode node ) {
264                assert context != null;
265                assert node != null;
266                if (getRoot().equals(node)) {
267                    nodesByUuid.clear();
268                    // Create the root node ...
269                    InMemoryNode root = new InMemoryNode(rootNodeUuid);
270                    nodesByUuid.put(root.getUuid(), root);
271                    return;
272                }
273                InMemoryNode parent = node.getParent();
274                assert parent != null;
275                parent.getChildren().remove(node);
276                correctSameNameSiblingIndexes(context, parent, node.getName().getName());
277                removeUuidReference(node);
278            }
279    
280            protected void removeUuidReference( InMemoryNode node ) {
281                nodesByUuid.remove(node.getUuid());
282                for (InMemoryNode child : node.getChildren()) {
283                    removeUuidReference(child);
284                }
285            }
286    
287            /**
288             * Create a node at the supplied path. The parent of the new node must already exist.
289             * 
290             * @param context the environment; may not be null
291             * @param pathToNewNode the path to the new node; may not be null
292             * @return the new node (or root if the path specified the root)
293             */
294            public InMemoryNode createNode( ExecutionContext context,
295                                            String pathToNewNode ) {
296                assert context != null;
297                assert pathToNewNode != null;
298                Path path = context.getValueFactories().getPathFactory().create(pathToNewNode);
299                if (path.isRoot()) return getRoot();
300                Path parentPath = path.getParent();
301                InMemoryNode parentNode = getNode(parentPath);
302                Name name = path.getLastSegment().getName();
303                return createNode(context, parentNode, name, null);
304            }
305    
306            /**
307             * Create a new node with the supplied name, as a child of the supplied parent.
308             * 
309             * @param context the execution context
310             * @param parentNode the parent node; may not be null
311             * @param name the name; may not be null
312             * @param uuid the UUID of the node, or null if the UUID is to be generated
313             * @return the new node
314             */
315            public InMemoryNode createNode( ExecutionContext context,
316                                            InMemoryNode parentNode,
317                                            Name name,
318                                            UUID uuid ) {
319                assert context != null;
320                assert name != null;
321                if (parentNode == null) parentNode = getRoot();
322                if (uuid == null) uuid = UUID.randomUUID();
323                InMemoryNode node = new InMemoryNode(uuid);
324                nodesByUuid.put(node.getUuid(), node);
325                node.setParent(parentNode);
326                // Find the last node with this same name ...
327                int nextIndex = 1;
328                if (parentNode.existingNames.contains(name)) {
329                    ListIterator<InMemoryNode> iter = parentNode.getChildren().listIterator(parentNode.getChildren().size());
330                    while (iter.hasPrevious()) {
331                        InMemoryNode prev = iter.previous();
332                        if (prev.getName().getName().equals(name)) {
333                            nextIndex = prev.getName().getIndex() + 1;
334                            break;
335                        }
336                    }
337                }
338                Path.Segment newName = context.getValueFactories().getPathFactory().createSegment(name, nextIndex);
339                node.setName(newName);
340                parentNode.getChildren().add(node);
341                parentNode.existingNames.add(name);
342                return node;
343            }
344    
345            /**
346             * Move the supplied node to the new parent. This method automatically removes the node from its existing parent, and also
347             * correctly adjusts the {@link Path.Segment#getIndex() index} to be correct in the new parent.
348             * 
349             * @param context
350             * @param node the node to be moved; may not be the {@link Workspace#getRoot() root}
351             * @param desiredNewName the new name for the node, if it is to be changed; may be null
352             * @param newWorkspace the workspace containing the new parent node
353             * @param newParent the new parent; may not be the {@link Workspace#getRoot() root}
354             * @param beforeNode the node before which this new node should be placed
355             */
356            public void moveNode( ExecutionContext context,
357                                  InMemoryNode node,
358                                  Name desiredNewName,
359                                  Workspace newWorkspace,
360                                  InMemoryNode newParent,
361                                  InMemoryNode beforeNode) {
362                assert context != null;
363                assert newParent != null;
364                assert node != null;
365    // Why was this restriction here? -- BRC            
366    //            assert newWorkspace.getRoot().equals(newParent) != true;
367                assert this.getRoot().equals(node) != true;
368                InMemoryNode oldParent = node.getParent();
369                Name oldName = node.getName().getName();
370                if (oldParent != null) {
371                    boolean removed = oldParent.getChildren().remove(node);
372                    assert removed == true;
373                    node.setParent(null);
374                    correctSameNameSiblingIndexes(context, oldParent, oldName);
375                }
376                node.setParent(newParent);
377                Name newName = oldName;
378                if (desiredNewName != null) {
379                    newName = desiredNewName;
380                    node.setName(context.getValueFactories().getPathFactory().createSegment(desiredNewName, 1));
381                }
382                
383                if (beforeNode == null) {
384                    newParent.getChildren().add(node);
385                }
386                else {
387                    int index = newParent.getChildren().indexOf(beforeNode);
388                    newParent.getChildren().add(index, node);
389                }
390                correctSameNameSiblingIndexes(context, newParent, newName);
391                
392                // If the node was moved to a new workspace...
393                if (!this.equals(newWorkspace)) {
394                    // We need to remove the node from this workspace's map of nodes ...
395                    this.moveNodeToWorkspace(node, newWorkspace);
396                }
397            }
398    
399            protected void moveNodeToWorkspace( InMemoryNode node,
400                                                Workspace newWorkspace ) {
401                assert this.nodesByUuid.containsKey(node.getUuid());
402                assert !newWorkspace.nodesByUuid.containsKey(node.getUuid());
403    
404                this.nodesByUuid.remove(node.getUuid());
405                newWorkspace.nodesByUuid.put(node.getUuid(), node);
406                for (InMemoryNode child : node.getChildren()) {
407                    moveNodeToWorkspace(child, newWorkspace);
408                }
409            }
410    
411            /**
412             * This should copy the subgraph given by the original node and place the new copy under the supplied new parent. Note
413             * that internal references between nodes within the original subgraph must be reflected as internal nodes within the new
414             * subgraph.
415             * 
416             * @param context the context; may not be null
417             * @param original the node to be copied; may not be null
418             * @param newWorkspace the workspace containing the new parent node; may not be null
419             * @param newParent the parent where the copy is to be placed; may not be null
420             * @param desiredName the desired name for the node; if null, the name will be obtained from the original node
421             * @param recursive true if the copy should be recursive
422             * @param oldToNewUuids the map of UUIDs of nodes in the new subgraph keyed by the UUIDs of nodes in the original; may be
423             *        null if the UUIDs are to be maintained
424             * @return the new node, which is the top of the new subgraph
425             */
426            public InMemoryNode copyNode( ExecutionContext context,
427                                          InMemoryNode original,
428                                          Workspace newWorkspace,
429                                          InMemoryNode newParent,
430                                          Name desiredName,
431                                          boolean recursive,
432                                          Map<UUID, UUID> oldToNewUuids ) {
433                assert context != null;
434                assert original != null;
435                assert newParent != null;
436                assert newWorkspace != null;
437                if (this.equals(newWorkspace) && oldToNewUuids == null) oldToNewUuids = new HashMap<UUID, UUID>();
438                boolean reuseUuids = oldToNewUuids == null;
439    
440                // Get or create the new node ...
441                Name childName = desiredName != null ? desiredName : original.getName().getName();
442                UUID uuidForCopy = reuseUuids ? original.getUuid() : UUID.randomUUID();
443                InMemoryNode copy = newWorkspace.createNode(context, newParent, childName, uuidForCopy);
444                if (oldToNewUuids != null) {
445                    oldToNewUuids.put(original.getUuid(), copy.getUuid());
446                }
447    
448                // Copy the properties ...
449                copy.getProperties().clear();
450                copy.getProperties().putAll(original.getProperties());
451                if (recursive) {
452                    // Loop over each child and call this method ...
453                    for (InMemoryNode child : original.getChildren()) {
454                        copyNode(context, child, newWorkspace, copy, null, true, oldToNewUuids);
455                    }
456                }
457    
458                if (oldToNewUuids != null) {
459                    // Now, adjust any references in the new subgraph to objects in the original subgraph
460                    // (because they were internal references, and need to be internal to the new subgraph)
461                    PropertyFactory propertyFactory = context.getPropertyFactory();
462                    UuidFactory uuidFactory = context.getValueFactories().getUuidFactory();
463                    ValueFactory<Reference> referenceFactory = context.getValueFactories().getReferenceFactory();
464                    for (Map.Entry<UUID, UUID> oldToNew : oldToNewUuids.entrySet()) {
465                        InMemoryNode oldNode = this.getNode(oldToNew.getKey());
466                        InMemoryNode newNode = newWorkspace.getNode(oldToNew.getValue());
467                        assert oldNode != null;
468                        assert newNode != null;
469                        // Iterate over the properties of the new ...
470                        for (Map.Entry<Name, Property> entry : newNode.getProperties().entrySet()) {
471                            Property property = entry.getValue();
472                            // Now see if any of the property values are references ...
473                            List<Object> newValues = new ArrayList<Object>();
474                            boolean foundReference = false;
475                            for (Iterator<?> iter = property.getValues(); iter.hasNext();) {
476                                Object value = iter.next();
477                                PropertyType type = PropertyType.discoverType(value);
478                                if (type == PropertyType.REFERENCE) {
479                                    UUID oldReferencedUuid = uuidFactory.create(value);
480                                    UUID newReferencedUuid = oldToNewUuids.get(oldReferencedUuid);
481                                    if (newReferencedUuid != null) {
482                                        newValues.add(referenceFactory.create(newReferencedUuid));
483                                        foundReference = true;
484                                    }
485                                } else {
486                                    newValues.add(value);
487                                }
488                            }
489                            // If we found at least one reference, we have to build a new Property object ...
490                            if (foundReference) {
491                                Property newProperty = propertyFactory.create(property.getName(), newValues);
492                                entry.setValue(newProperty);
493                            }
494                        }
495                    }
496                }
497    
498                return copy;
499            }
500    
501            protected void correctSameNameSiblingIndexes( ExecutionContext context,
502                                                          InMemoryNode parentNode,
503                                                          Name name ) {
504                if (parentNode == null) return;
505                // Look for the highest existing index ...
506                List<InMemoryNode> childrenWithSameNames = new LinkedList<InMemoryNode>();
507                for (InMemoryNode child : parentNode.getChildren()) {
508                    if (child.getName().getName().equals(name)) childrenWithSameNames.add(child);
509                }
510                if (childrenWithSameNames.size() == 0) return;
511                if (childrenWithSameNames.size() == 1) {
512                    InMemoryNode childWithSameName = childrenWithSameNames.get(0);
513                    Path.Segment newName = context.getValueFactories().getPathFactory().createSegment(name, Path.DEFAULT_INDEX);
514                    childWithSameName.setName(newName);
515                    return;
516                }
517                int index = 1;
518                for (InMemoryNode childWithSameName : childrenWithSameNames) {
519                    Path.Segment segment = childWithSameName.getName();
520                    if (segment.getIndex() != index) {
521                        Path.Segment newName = context.getValueFactories().getPathFactory().createSegment(name, index);
522                        childWithSameName.setName(newName);
523                    }
524                    ++index;
525                }
526            }
527    
528            /**
529             * {@inheritDoc}
530             * 
531             * @see java.lang.Object#equals(java.lang.Object)
532             */
533            @Override
534            public boolean equals( Object obj ) {
535                if (obj == this) return true;
536                if (obj instanceof Workspace) {
537                    Workspace that = (Workspace)obj;
538                    // Assume the workspaces are in the same repository ...
539                    if (!this.name.equals(that.name)) return false;
540                    return true;
541                }
542                return false;
543            }
544    
545            /**
546             * {@inheritDoc}
547             * 
548             * @see java.lang.Object#toString()
549             */
550            @Override
551            public String toString() {
552                return InMemoryRepository.this.getSourceName() + "/" + this.getName();
553            }
554    
555        }
556    
557    }