001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.connector.jbosscache;
023    
024    import java.util.ArrayList;
025    import java.util.Arrays;
026    import java.util.Collections;
027    import java.util.LinkedList;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.Set;
031    import java.util.UUID;
032    import java.util.concurrent.TimeUnit;
033    import java.util.concurrent.atomic.AtomicInteger;
034    import javax.transaction.xa.XAResource;
035    import org.jboss.cache.Cache;
036    import org.jboss.cache.Fqn;
037    import org.jboss.cache.Node;
038    import org.jboss.dna.graph.DnaLexicon;
039    import org.jboss.dna.graph.ExecutionContext;
040    import org.jboss.dna.graph.Location;
041    import org.jboss.dna.graph.cache.CachePolicy;
042    import org.jboss.dna.graph.connectors.RepositoryConnection;
043    import org.jboss.dna.graph.connectors.RepositorySourceException;
044    import org.jboss.dna.graph.connectors.RepositorySourceListener;
045    import org.jboss.dna.graph.properties.Name;
046    import org.jboss.dna.graph.properties.Path;
047    import org.jboss.dna.graph.properties.PathFactory;
048    import org.jboss.dna.graph.properties.PathNotFoundException;
049    import org.jboss.dna.graph.properties.Property;
050    import org.jboss.dna.graph.properties.PropertyFactory;
051    import org.jboss.dna.graph.properties.ValueFactory;
052    import org.jboss.dna.graph.properties.Path.Segment;
053    import org.jboss.dna.graph.requests.CopyBranchRequest;
054    import org.jboss.dna.graph.requests.CreateNodeRequest;
055    import org.jboss.dna.graph.requests.DeleteBranchRequest;
056    import org.jboss.dna.graph.requests.MoveBranchRequest;
057    import org.jboss.dna.graph.requests.ReadAllChildrenRequest;
058    import org.jboss.dna.graph.requests.ReadAllPropertiesRequest;
059    import org.jboss.dna.graph.requests.Request;
060    import org.jboss.dna.graph.requests.UpdatePropertiesRequest;
061    import org.jboss.dna.graph.requests.processor.RequestProcessor;
062    
063    /**
064     * The repository connection to a JBoss Cache instance.
065     * 
066     * @author Randall Hauch
067     */
068    public class JBossCacheConnection implements RepositoryConnection {
069    
070        protected static final RepositorySourceListener NO_OP_LISTENER = new RepositorySourceListener() {
071    
072            /**
073             * {@inheritDoc}
074             */
075            public void notify( String sourceName,
076                                Object... events ) {
077                // do nothing
078            }
079        };
080    
081        private final JBossCacheSource source;
082        private final Cache<Name, Object> cache;
083        private RepositorySourceListener listener = NO_OP_LISTENER;
084    
085        JBossCacheConnection( JBossCacheSource source,
086                              Cache<Name, Object> cache ) {
087            assert source != null;
088            assert cache != null;
089            this.source = source;
090            this.cache = cache;
091        }
092    
093        /**
094         * @return cache
095         */
096        /*package*/Cache<Name, Object> getCache() {
097            return cache;
098        }
099    
100        /**
101         * {@inheritDoc}
102         */
103        public String getSourceName() {
104            return source.getName();
105        }
106    
107        /**
108         * {@inheritDoc}
109         */
110        public CachePolicy getDefaultCachePolicy() {
111            return source.getDefaultCachePolicy();
112        }
113    
114        /**
115         * {@inheritDoc}
116         */
117        public XAResource getXAResource() {
118            return null;
119        }
120    
121        /**
122         * {@inheritDoc}
123         */
124        public boolean ping( long time,
125                             TimeUnit unit ) {
126            this.cache.getRoot();
127            return true;
128        }
129    
130        /**
131         * {@inheritDoc}
132         */
133        public void setListener( RepositorySourceListener listener ) {
134            this.listener = listener != null ? listener : NO_OP_LISTENER;
135        }
136    
137        /**
138         * {@inheritDoc}
139         */
140        public void close() {
141            // do nothing
142        }
143    
144        /**
145         * {@inheritDoc}
146         * 
147         * @see org.jboss.dna.graph.connectors.RepositoryConnection#execute(org.jboss.dna.graph.ExecutionContext,
148         *      org.jboss.dna.graph.requests.Request)
149         */
150        public void execute( final ExecutionContext context,
151                             final Request request ) throws RepositorySourceException {
152            final PathFactory pathFactory = context.getValueFactories().getPathFactory();
153            final PropertyFactory propertyFactory = context.getPropertyFactory();
154            final ValueFactory<UUID> uuidFactory = context.getValueFactories().getUuidFactory();
155            RequestProcessor processor = new RequestProcessor(getSourceName(), context) {
156                @Override
157                public void process( ReadAllChildrenRequest request ) {
158                    Path nodePath = request.of().getPath();
159                    Node<Name, Object> node = getNode(context, nodePath);
160                    // Get the names of the children, using the child list ...
161                    Path.Segment[] childList = (Path.Segment[])node.get(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST);
162                    for (Path.Segment child : childList) {
163                        // We have the child segment, but we need the UUID property ...
164                        Node<Name, Object> childNode = node.getChild(child);
165                        Object uuid = childNode.get(DnaLexicon.UUID);
166                        if (uuid == null) {
167                            uuid = generateUuid();
168                            childNode.put(DnaLexicon.UUID, uuid);
169                        } else {
170                            uuid = uuidFactory.create(uuid);
171                        }
172                        Property uuidProperty = propertyFactory.create(DnaLexicon.UUID, uuid);
173                        request.addChild(pathFactory.create(nodePath, child), uuidProperty);
174                    }
175                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
176                    request.setActualLocationOfNode(new Location(nodePath, uuid));
177                }
178    
179                @Override
180                public void process( ReadAllPropertiesRequest request ) {
181                    Path nodePath = request.at().getPath();
182                    Node<Name, Object> node = getNode(context, nodePath);
183                    Map<Name, Object> dataMap = node.getData();
184                    for (Map.Entry<Name, Object> data : dataMap.entrySet()) {
185                        Name propertyName = data.getKey();
186                        // Don't allow the child list property to be accessed
187                        if (propertyName.equals(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST)) continue;
188                        Object values = data.getValue();
189                        Property property = propertyFactory.create(propertyName, values);
190                        request.addProperty(property);
191                    }
192                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
193                    request.setActualLocationOfNode(new Location(nodePath, uuid));
194                }
195    
196                @Override
197                public void process( CreateNodeRequest request ) {
198                    Path path = request.at().getPath();
199                    Path parent = path.getParent();
200                    // Look up the parent node, which must exist ...
201                    Node<Name, Object> parentNode = getNode(context, parent);
202    
203                    // Update the children to account for same-name siblings.
204                    // This not only updates the FQN of the child nodes, but it also sets the property that stores the
205                    // the array of Path.Segment for the children (since the cache doesn't maintain order).
206                    Path.Segment newSegment = updateChildList(parentNode,
207                                                              path.getLastSegment().getName(),
208                                                              getExecutionContext(),
209                                                              true);
210                    Node<Name, Object> node = parentNode.addChild(Fqn.fromElements(newSegment));
211                    assert checkChildren(parentNode);
212    
213                    // Add the UUID property (if required), which may be overwritten by a supplied property ...
214                    node.put(DnaLexicon.UUID, generateUuid());
215                    // Now add the properties to the supplied node ...
216                    for (Property property : request.properties()) {
217                        if (property.size() == 0) continue;
218                        Name propName = property.getName();
219                        Object value = null;
220                        if (property.size() == 1) {
221                            value = property.iterator().next();
222                        } else {
223                            value = property.getValuesAsArray();
224                        }
225                        node.put(propName, value);
226                    }
227                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
228                    Path nodePath = pathFactory.create(parent, newSegment);
229                    request.setActualLocationOfNode(new Location(nodePath, uuid));
230                }
231    
232                @Override
233                public void process( UpdatePropertiesRequest request ) {
234                    Path nodePath = request.on().getPath();
235                    Node<Name, Object> node = getNode(context, nodePath);
236                    // Now set (or remove) the properties to the supplied node ...
237                    for (Property property : request.properties()) {
238                        Name propName = property.getName();
239                        // Don't allow the child list property to be removed or changed
240                        if (propName.equals(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST)) continue;
241                        if (property.size() == 0) {
242                            node.remove(propName);
243                            continue;
244                        }
245                        Object value = null;
246                        if (property.size() == 1) {
247                            value = property.iterator().next();
248                        } else {
249                            value = property.getValuesAsArray();
250                        }
251                        node.put(propName, value);
252                    }
253                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
254                    request.setActualLocationOfNode(new Location(nodePath, uuid));
255                }
256    
257                @Override
258                public void process( CopyBranchRequest request ) {
259                    Path nodePath = request.from().getPath();
260                    Node<Name, Object> node = getNode(context, nodePath);
261                    // Look up the new parent, which must exist ...
262                    Path newParentPath = request.into().getPath();
263                    Node<Name, Object> newParent = getNode(context, newParentPath);
264                    Path.Segment newSegment = copyNode(node, newParent, true, null, null, getExecutionContext());
265    
266                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
267                    Path newPath = pathFactory.create(newParentPath, newSegment);
268                    request.setActualLocations(new Location(nodePath, uuid), new Location(newPath, uuid));
269                }
270    
271                @Override
272                public void process( DeleteBranchRequest request ) {
273                    Path nodePath = request.at().getPath();
274                    Node<Name, Object> node = getNode(context, nodePath);
275                    node.getParent().removeChild(node.getFqn().getLastElement());
276                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
277                    request.setActualLocationOfNode(new Location(nodePath, uuid));
278                }
279    
280                @Override
281                public void process( MoveBranchRequest request ) {
282                    Path nodePath = request.from().getPath();
283                    Node<Name, Object> node = getNode(context, nodePath);
284                    boolean recursive = true;
285                    // Look up the new parent, which must exist ...
286                    Path newParentPath = request.into().getPath();
287                    Node<Name, Object> newParent = getNode(context, newParentPath);
288                    Path.Segment newSegment = copyNode(node, newParent, recursive, DnaLexicon.UUID, null, getExecutionContext());
289    
290                    // Now delete the old node ...
291                    Node<Name, Object> oldParent = node.getParent();
292                    boolean removed = oldParent.removeChild(node.getFqn().getLastElement());
293                    assert removed;
294    
295                    UUID uuid = uuidFactory.create(node.get(DnaLexicon.UUID));
296                    Path newPath = pathFactory.create(newParentPath, newSegment);
297                    request.setActualLocations(new Location(nodePath, uuid), new Location(newPath, uuid));
298                }
299            };
300            processor.process(request);
301        }
302    
303        /**
304         * @return listener
305         */
306        protected RepositorySourceListener getListener() {
307            return this.listener;
308        }
309    
310        protected Fqn<?> getFullyQualifiedName( Path path ) {
311            assert path != null;
312            return Fqn.fromList(path.getSegmentsList());
313        }
314    
315        /**
316         * Get a relative fully-qualified name that consists only of the supplied segment.
317         * 
318         * @param pathSegment the segment from which the fully qualified name is to be created
319         * @return the relative fully-qualified name
320         */
321        protected Fqn<?> getFullyQualifiedName( Path.Segment pathSegment ) {
322            assert pathSegment != null;
323            return Fqn.fromElements(pathSegment);
324        }
325    
326        @SuppressWarnings( "unchecked" )
327        protected Path getPath( PathFactory factory,
328                                Fqn<?> fqn ) {
329            List<Path.Segment> segments = (List<Path.Segment>)fqn.peekElements();
330            return factory.create(factory.createRootPath(), segments);
331        }
332    
333        protected Node<Name, Object> getNode( ExecutionContext context,
334                                              Path path ) {
335            // Look up the node with the supplied path ...
336            Fqn<?> fqn = getFullyQualifiedName(path);
337            Node<Name, Object> node = cache.getNode(fqn);
338            if (node == null) {
339                String nodePath = path.getString(context.getNamespaceRegistry());
340                Path lowestExisting = null;
341                while (fqn != null) {
342                    fqn = fqn.getParent();
343                    node = cache.getNode(fqn);
344                    if (node != null) {
345                        lowestExisting = getPath(context.getValueFactories().getPathFactory(), fqn);
346                        fqn = null;
347                    }
348                }
349                throw new PathNotFoundException(new Location(path), lowestExisting,
350                                                JBossCacheConnectorI18n.nodeDoesNotExist.text(nodePath));
351            }
352            return node;
353    
354        }
355    
356        protected UUID generateUuid() {
357            return UUID.randomUUID();
358        }
359    
360        protected Path.Segment copyNode( Node<Name, Object> original,
361                                         Node<Name, Object> newParent,
362                                         boolean recursive,
363                                         Name uuidProperty,
364                                         AtomicInteger count,
365                                         ExecutionContext context ) {
366            assert original != null;
367            assert newParent != null;
368            // Get or create the new node ...
369            Segment name = (Segment)original.getFqn().getLastElement();
370    
371            // Update the children to account for same-name siblings.
372            // This not only updates the FQN of the child nodes, but it also sets the property that stores the
373            // the array of Path.Segment for the children (since the cache doesn't maintain order).
374            Path.Segment newSegment = updateChildList(newParent, name.getName(), context, true);
375            Node<Name, Object> copy = newParent.addChild(getFullyQualifiedName(newSegment));
376            assert checkChildren(newParent);
377            // Copy the properties ...
378            copy.clearData();
379            copy.putAll(original.getData());
380            if (uuidProperty != null) {
381                // Generate a new UUID for the new node, overwriting any existing value from the original ...
382                copy.put(uuidProperty, generateUuid());
383            }
384            if (count != null) count.incrementAndGet();
385            if (recursive) {
386                // Loop over each child and call this method ...
387                for (Node<Name, Object> child : original.getChildren()) {
388                    copyNode(child, copy, true, uuidProperty, count, context);
389                }
390            }
391            return newSegment;
392        }
393    
394        /**
395         * Update (or create) the array of {@link Path.Segment path segments} for the children of the supplied node. This array
396         * maintains the ordered list of children (since the {@link Cache} does not maintain the order). Invoking this method will
397         * change any existing children that a {@link Path.Segment#getName() name part} that matches the supplied
398         * <code>changedName</code> to have the appropriate {@link Path.Segment#getIndex() same-name sibling index}.
399         * 
400         * @param parent the parent node; may not be null
401         * @param changedName the name that should be compared to the existing node siblings to determine whether the same-name
402         *        sibling indexes should be updated; may not be null
403         * @param context the execution context; may not be null
404         * @param addChildWithName true if a new child with the supplied name is to be added to the children (but which does not yet
405         *        exist in the node's children)
406         * @return the path segment for the new child, or null if <code>addChildWithName</code> was false
407         */
408        protected Path.Segment updateChildList( Node<Name, Object> parent,
409                                                Name changedName,
410                                                ExecutionContext context,
411                                                boolean addChildWithName ) {
412            assert parent != null;
413            assert changedName != null;
414            assert context != null;
415            Set<Node<Name, Object>> children = parent.getChildren();
416            if (children.isEmpty() && !addChildWithName) return null;
417    
418            // Go through the children, looking for any children with the same name as the 'changedName'
419            List<ChildInfo> childrenWithChangedName = new LinkedList<ChildInfo>();
420            Path.Segment[] childNames = (Path.Segment[])parent.get(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST);
421            int index = 0;
422            if (childNames != null) {
423                for (Path.Segment childName : childNames) {
424                    if (childName.getName().equals(changedName)) {
425                        ChildInfo info = new ChildInfo(childName, index);
426                        childrenWithChangedName.add(info);
427                    }
428                    index++;
429                }
430            }
431            if (addChildWithName) {
432                // Make room for the new child at the end of the array ...
433                if (childNames == null) {
434                    childNames = new Path.Segment[1];
435                } else {
436                    int numExisting = childNames.length;
437                    Path.Segment[] newChildNames = new Path.Segment[numExisting + 1];
438                    System.arraycopy(childNames, 0, newChildNames, 0, numExisting);
439                    childNames = newChildNames;
440                }
441    
442                // And add a child info for the new node ...
443                ChildInfo info = new ChildInfo(null, index);
444                childrenWithChangedName.add(info);
445                Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(changedName);
446                childNames[index++] = newSegment;
447            }
448            assert childNames != null;
449    
450            // Now process the children with the same name, which may include a child info for the new node ...
451            assert childrenWithChangedName.isEmpty() == false;
452            if (childrenWithChangedName.size() == 1) {
453                // The child should have no indexes ...
454                ChildInfo child = childrenWithChangedName.get(0);
455                if (child.segment != null && child.segment.hasIndex()) {
456                    // The existing child needs to have a new index ..
457                    Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(changedName);
458                    // Replace the child with the correct FQN ...
459                    changeNodeName(parent, child.segment, newSegment, context);
460                    // Change the segment in the child list ...
461                    childNames[child.childIndex] = newSegment;
462                }
463            } else {
464                // There is more than one child with the same name ...
465                int i = 0;
466                for (ChildInfo child : childrenWithChangedName) {
467                    if (child.segment != null) {
468                        // Determine the new name and index ...
469                        Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(changedName, i + 1);
470                        // Replace the child with the correct FQN ...
471                        changeNodeName(parent, child.segment, newSegment, context);
472                        // Change the segment in the child list ...
473                        childNames[child.childIndex] = newSegment;
474                    } else {
475                        // Determine the new name and index ...
476                        Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(changedName, i + 1);
477                        childNames[child.childIndex] = newSegment;
478                    }
479                    ++i;
480                }
481            }
482    
483            // Record the list of children as a property on the parent ...
484            // (Do this last, as it doesn't need to be done if there's an exception in the above logic)
485            context.getLogger(getClass()).trace("Updating child list of {0} to: {1}", parent.getFqn(), Arrays.asList(childNames));
486            parent.put(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST, childNames); // replaces any existing value
487    
488            if (addChildWithName) {
489                // Return the segment for the new node ...
490                return childNames[childNames.length - 1];
491            }
492            return null;
493        }
494    
495        protected boolean checkChildren( Node<Name, Object> parent ) {
496            Path.Segment[] childNamesProperty = (Path.Segment[])parent.get(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST);
497            Set<Object> childNames = parent.getChildrenNames();
498            boolean result = true;
499            if (childNamesProperty.length != childNames.size()) result = false;
500            for (int i = 0; i != childNamesProperty.length; ++i) {
501                if (!childNames.contains(childNamesProperty[i])) result = false;
502            }
503            if (!result) {
504                List<Path.Segment> names = new ArrayList<Path.Segment>();
505                for (Object name : childNames) {
506                    names.add((Path.Segment)name);
507                }
508                Collections.sort(names);
509                // Logger.getLogger(getClass()).trace("Child list on {0} is: {1}",
510                // parent.getFqn(),
511                // StringUtil.readableString(childNamesProperty));
512                // Logger.getLogger(getClass()).trace("Children of {0} is: {1}", parent.getFqn(), StringUtil.readableString(names));
513            }
514            return result;
515        }
516    
517        /**
518         * Utility class used by the {@link JBossCacheConnection#updateChildList(Node, Name, ExecutionContext, boolean)} method.
519         * 
520         * @author Randall Hauch
521         */
522        private static class ChildInfo {
523            protected final Path.Segment segment;
524            protected final int childIndex;
525    
526            protected ChildInfo( Path.Segment childSegment,
527                                 int childIndex ) {
528                this.segment = childSegment;
529                this.childIndex = childIndex;
530            }
531    
532        }
533    
534        /**
535         * Changes the name of the node in the cache (but does not update the list of child segments stored on the parent).
536         * 
537         * @param parent
538         * @param existing
539         * @param newSegment
540         * @param context
541         */
542        protected void changeNodeName( Node<Name, Object> parent,
543                                       Path.Segment existing,
544                                       Path.Segment newSegment,
545                                       ExecutionContext context ) {
546            assert parent != null;
547            assert existing != null;
548            assert newSegment != null;
549            assert context != null;
550    
551            if (existing.equals(newSegment)) return;
552            context.getLogger(getClass()).trace("Renaming {0} to {1} under {2}", existing, newSegment, parent.getFqn());
553            Node<Name, Object> existingChild = parent.getChild(existing);
554            assert existingChild != null;
555    
556            // JBoss Cache can move a node from one node to another node, but the move doesn't change the name;
557            // since you provide the FQN of the parent location, the name of the node cannot be changed.
558            // Therefore, to compensate, we need to create a new child, copy all of the data, move all of the child
559            // nodes of the old node, then remove the old node.
560    
561            // Create the new node ...
562            Node<Name, Object> newChild = parent.addChild(Fqn.fromElements(newSegment));
563            Fqn<?> newChildFqn = newChild.getFqn();
564    
565            // Copy the data ...
566            newChild.putAll(existingChild.getData());
567    
568            // Move the children ...
569            for (Node<Name, Object> grandChild : existingChild.getChildren()) {
570                cache.move(grandChild.getFqn(), newChildFqn);
571            }
572    
573            // Remove the existing ...
574            parent.removeChild(existing);
575        }
576    }