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             */
355            public void moveNode( ExecutionContext context,
356                                  InMemoryNode node,
357                                  Name desiredNewName,
358                                  Workspace newWorkspace,
359                                  InMemoryNode newParent ) {
360                assert context != null;
361                assert newParent != null;
362                assert node != null;
363                assert newWorkspace.getRoot().equals(newParent) != true;
364                assert this.getRoot().equals(node) != true;
365                InMemoryNode oldParent = node.getParent();
366                Name oldName = node.getName().getName();
367                if (oldParent != null) {
368                    if (oldParent.equals(newParent)) return;
369                    boolean removed = oldParent.getChildren().remove(node);
370                    assert removed == true;
371                    node.setParent(null);
372                    correctSameNameSiblingIndexes(context, oldParent, oldName);
373                }
374                node.setParent(newParent);
375                Name newName = oldName;
376                if (desiredNewName != null) {
377                    newName = desiredNewName;
378                    node.setName(context.getValueFactories().getPathFactory().createSegment(desiredNewName, 1));
379                }
380                newParent.getChildren().add(node);
381                correctSameNameSiblingIndexes(context, newParent, newName);
382    
383                // If the node was moved to a new workspace...
384                if (!this.equals(newWorkspace)) {
385                    // We need to remove the node from this workspace's map of nodes ...
386                    this.moveNodeToWorkspace(node, newWorkspace);
387                }
388            }
389    
390            protected void moveNodeToWorkspace( InMemoryNode node,
391                                                Workspace newWorkspace ) {
392                assert this.nodesByUuid.containsKey(node.getUuid());
393                assert !newWorkspace.nodesByUuid.containsKey(node.getUuid());
394    
395                this.nodesByUuid.remove(node.getUuid());
396                newWorkspace.nodesByUuid.put(node.getUuid(), node);
397                for (InMemoryNode child : node.getChildren()) {
398                    moveNodeToWorkspace(child, newWorkspace);
399                }
400            }
401    
402            /**
403             * This should copy the subgraph given by the original node and place the new copy under the supplied new parent. Note
404             * that internal references between nodes within the original subgraph must be reflected as internal nodes within the new
405             * subgraph.
406             * 
407             * @param context the context; may not be null
408             * @param original the node to be copied; may not be null
409             * @param newWorkspace the workspace containing the new parent node; may not be null
410             * @param newParent the parent where the copy is to be placed; may not be null
411             * @param desiredName the desired name for the node; if null, the name will be obtained from the original node
412             * @param recursive true if the copy should be recursive
413             * @param oldToNewUuids the map of UUIDs of nodes in the new subgraph keyed by the UUIDs of nodes in the original; may be
414             *        null if the UUIDs are to be maintained
415             * @return the new node, which is the top of the new subgraph
416             */
417            public InMemoryNode copyNode( ExecutionContext context,
418                                          InMemoryNode original,
419                                          Workspace newWorkspace,
420                                          InMemoryNode newParent,
421                                          Name desiredName,
422                                          boolean recursive,
423                                          Map<UUID, UUID> oldToNewUuids ) {
424                assert context != null;
425                assert original != null;
426                assert newParent != null;
427                assert newWorkspace != null;
428                if (this.equals(newWorkspace) && oldToNewUuids == null) oldToNewUuids = new HashMap<UUID, UUID>();
429                boolean reuseUuids = oldToNewUuids == null;
430    
431                // Get or create the new node ...
432                Name childName = desiredName != null ? desiredName : original.getName().getName();
433                UUID uuidForCopy = reuseUuids ? original.getUuid() : UUID.randomUUID();
434                InMemoryNode copy = newWorkspace.createNode(context, newParent, childName, uuidForCopy);
435                if (oldToNewUuids != null) {
436                    oldToNewUuids.put(original.getUuid(), copy.getUuid());
437                }
438    
439                // Copy the properties ...
440                copy.getProperties().clear();
441                copy.getProperties().putAll(original.getProperties());
442                if (recursive) {
443                    // Loop over each child and call this method ...
444                    for (InMemoryNode child : original.getChildren()) {
445                        copyNode(context, child, newWorkspace, copy, null, true, oldToNewUuids);
446                    }
447                }
448    
449                if (oldToNewUuids != null) {
450                    // Now, adjust any references in the new subgraph to objects in the original subgraph
451                    // (because they were internal references, and need to be internal to the new subgraph)
452                    PropertyFactory propertyFactory = context.getPropertyFactory();
453                    UuidFactory uuidFactory = context.getValueFactories().getUuidFactory();
454                    ValueFactory<Reference> referenceFactory = context.getValueFactories().getReferenceFactory();
455                    for (Map.Entry<UUID, UUID> oldToNew : oldToNewUuids.entrySet()) {
456                        InMemoryNode oldNode = this.getNode(oldToNew.getKey());
457                        InMemoryNode newNode = newWorkspace.getNode(oldToNew.getValue());
458                        assert oldNode != null;
459                        assert newNode != null;
460                        // Iterate over the properties of the new ...
461                        for (Map.Entry<Name, Property> entry : newNode.getProperties().entrySet()) {
462                            Property property = entry.getValue();
463                            // Now see if any of the property values are references ...
464                            List<Object> newValues = new ArrayList<Object>();
465                            boolean foundReference = false;
466                            for (Iterator<?> iter = property.getValues(); iter.hasNext();) {
467                                Object value = iter.next();
468                                PropertyType type = PropertyType.discoverType(value);
469                                if (type == PropertyType.REFERENCE) {
470                                    UUID oldReferencedUuid = uuidFactory.create(value);
471                                    UUID newReferencedUuid = oldToNewUuids.get(oldReferencedUuid);
472                                    if (newReferencedUuid != null) {
473                                        newValues.add(referenceFactory.create(newReferencedUuid));
474                                        foundReference = true;
475                                    }
476                                } else {
477                                    newValues.add(value);
478                                }
479                            }
480                            // If we found at least one reference, we have to build a new Property object ...
481                            if (foundReference) {
482                                Property newProperty = propertyFactory.create(property.getName(), newValues);
483                                entry.setValue(newProperty);
484                            }
485                        }
486                    }
487                }
488    
489                return copy;
490            }
491    
492            protected void correctSameNameSiblingIndexes( ExecutionContext context,
493                                                          InMemoryNode parentNode,
494                                                          Name name ) {
495                if (parentNode == null) return;
496                // Look for the highest existing index ...
497                List<InMemoryNode> childrenWithSameNames = new LinkedList<InMemoryNode>();
498                for (InMemoryNode child : parentNode.getChildren()) {
499                    if (child.getName().getName().equals(name)) childrenWithSameNames.add(child);
500                }
501                if (childrenWithSameNames.size() == 0) return;
502                if (childrenWithSameNames.size() == 1) {
503                    InMemoryNode childWithSameName = childrenWithSameNames.get(0);
504                    Path.Segment newName = context.getValueFactories().getPathFactory().createSegment(name, Path.DEFAULT_INDEX);
505                    childWithSameName.setName(newName);
506                    return;
507                }
508                int index = 1;
509                for (InMemoryNode childWithSameName : childrenWithSameNames) {
510                    Path.Segment segment = childWithSameName.getName();
511                    if (segment.getIndex() != index) {
512                        Path.Segment newName = context.getValueFactories().getPathFactory().createSegment(name, index);
513                        childWithSameName.setName(newName);
514                    }
515                    ++index;
516                }
517            }
518    
519            /**
520             * {@inheritDoc}
521             * 
522             * @see java.lang.Object#equals(java.lang.Object)
523             */
524            @Override
525            public boolean equals( Object obj ) {
526                if (obj == this) return true;
527                if (obj instanceof Workspace) {
528                    Workspace that = (Workspace)obj;
529                    // Assume the workspaces are in the same repository ...
530                    if (!this.name.equals(that.name)) return false;
531                    return true;
532                }
533                return false;
534            }
535    
536            /**
537             * {@inheritDoc}
538             * 
539             * @see java.lang.Object#toString()
540             */
541            @Override
542            public String toString() {
543                return InMemoryRepository.this.getSourceName() + "/" + this.getName();
544            }
545    
546        }
547    
548    }