001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.sequencer.ddl.node;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import org.modeshape.common.annotation.NotThreadSafe;
028import org.modeshape.common.util.CheckArg;
029import org.modeshape.jcr.api.JcrConstants;
030
031/**
032 * Utility object class designed to facilitate constructing an AST or Abstract Syntax Tree representing nodes and properties that
033 * are compatible with ModeShape graph component structure.
034 */
035@NotThreadSafe
036public final class AstNode implements Iterable<AstNode>, Cloneable {
037
038    private AstNode parent;
039
040    private final String name;
041    private final Map<String, Object> properties = new HashMap<String, Object>();
042    private final LinkedList<AstNode> children = new LinkedList<AstNode>();
043    private final List<AstNode> childrenView = Collections.unmodifiableList(children);
044
045    /**
046     * Construct a node with the supplied name but without a parent.
047     * 
048     * @param name the name of the node; may not be null
049     */
050    AstNode( String name ) {
051        this(null, name);
052    }
053
054    /**
055     * Construct a node with the supplied name and parent.
056     * 
057     * @param parent the parent node; may be null if the new node is to be a root
058     * @param name the name of the node; may not be null
059     */
060    public AstNode( AstNode parent,
061                    String name ) {
062        CheckArg.isNotNull(name, "name");
063        this.name = name;
064        if (parent != null) {
065            this.parent = parent;
066            this.parent.children.add(this);
067        }
068    }
069
070    /**
071     * @param mixin the mixin being added (cannot be <code>null</code> or empty)
072     * @return <code>true</code> if mixin was added
073     */
074    public boolean addMixin( final String mixin ) {
075        CheckArg.isNotEmpty(mixin, "mixin");
076        final List<String> mixins = getMixins();
077
078        if (!mixins.contains(mixin)) {
079            if (mixins.add(mixin)) {
080                setProperty(JcrConstants.JCR_MIXIN_TYPES, mixins);
081            }
082        }
083
084        return false;
085    }
086
087    /**
088     * @param mixin the mixin to look for (cannot be <code>null</code> or empty)
089     * @return <code>true</code> if the node has the specified mixin
090     */
091    public boolean hasMixin( final String mixin ) {
092        CheckArg.isNotEmpty(mixin, "mixin");
093        return getMixins().contains(mixin);
094    }
095
096    /**
097     * Get the name of the node.
098     * 
099     * @return the node's name; never null
100     */
101    public String getName() {
102        return name;
103    }
104
105    public String getPrimaryType() {
106        return (String)properties.get(JcrConstants.JCR_PRIMARY_TYPE);
107    }
108
109    /**
110     * Get the current same-name-sibling index.
111     * 
112     * @return the SNS index, or 1 if this is the first sibling with the same name
113     */
114    public int getSameNameSiblingIndex() {
115        int snsIndex = 1;
116        if (this.parent == null) {
117            return snsIndex;
118        }
119        // Go through all the children ...
120        for (AstNode sibling : this.parent.getChildren()) {
121            if (sibling == this) {
122                break;
123            }
124            if (sibling.getName().equals(this.name)) {
125                ++snsIndex;
126            }
127        }
128        return snsIndex;
129    }
130
131    /**
132     * Get the current path of this node
133     * 
134     * @return the path of this node; never null
135     */
136    public String getAbsolutePath() {
137        StringBuilder pathBuilder = new StringBuilder("/").append(this.getName());
138        AstNode parent = this.getParent();
139        while (parent != null) {
140            pathBuilder.insert(0, "/" + parent.getName());
141            parent = parent.getParent();
142        }
143        return pathBuilder.toString();
144    }
145
146    /**
147     * Get the property with the supplied name.
148     * 
149     * @param name the property name; never null
150     * @return the property, or null if no such property exists on the node
151     */
152    public Object getProperty( String name ) {
153        return properties.get(name);
154    }
155
156    /**
157     * Set the property with the given name to the supplied value. Any existing property with the same name will be replaced.
158     * 
159     * @param name the name of the property; may not be null
160     * @param value the value of the property; may not be null
161     * @return this node, for method chaining purposes
162     */
163    public AstNode setProperty( String name,
164                                Object value ) {
165        CheckArg.isNotNull(name, "name");
166        CheckArg.isNotNull(value, "value");
167        properties.put(name, value);
168        return this;
169    }
170
171    /**
172     * Set the property with the given name to the supplied values. If there is at least one value, the new property will replace
173     * any existing property with the same name. This method does nothing if zero values are supplied.
174     * 
175     * @param name the name of the property; may not be null
176     * @param values the values of the property
177     * @return this node, for method chaining purposes
178     */
179    public AstNode setProperty( String name,
180                                Object... values ) {
181        CheckArg.isNotNull(name, "name");
182        CheckArg.isNotNull(values, "value");
183        if (values.length != 0) {
184            properties.put(name, Arrays.asList(values));
185        }
186        return this;
187    }
188
189    /**
190     * Remove and return the property with the supplied name.
191     * 
192     * @param name the property name; may not be null
193     * @return the list of values of the property that was removed, or null if there was no such property
194     */
195    public Object removeProperty( String name ) {
196        return properties.remove(name);
197    }
198
199    /**
200     * Return the list of property names for this node.
201     * 
202     * @return the list of strings.
203     */
204    public List<String> getPropertyNames() {
205        return new ArrayList<String>(properties.keySet());
206    }
207
208    @SuppressWarnings( "unchecked" )
209    public List<String> getMixins() {
210        Object mixinValues = getProperty(JcrConstants.JCR_MIXIN_TYPES);
211        List<String> result = new ArrayList<String>();
212        if (mixinValues instanceof Collection) {
213            result.addAll((Collection<? extends String>)mixinValues);
214        } else if (mixinValues != null) {
215            result.add(mixinValues.toString());
216        }
217        return result;
218    }
219
220    /**
221     * Get the parent of this node.
222     * 
223     * @return the parent node, or null if this node has no parent
224     */
225    public AstNode getParent() {
226        return parent;
227    }
228
229    /**
230     * Set the parent for this node. If this node already has a parent, this method will remove this node from the current parent.
231     * If the supplied parent is not null, then this node will be added to the supplied parent's children.
232     * 
233     * @param parent the new parent, or null if this node is to have no parent
234     */
235    public void setParent( AstNode parent ) {
236        removeFromParent();
237        if (parent != null) {
238            this.parent = parent;
239            this.parent.children.add(this);
240        }
241    }
242
243    /**
244     * Insert the supplied node into the plan node tree immediately above this node. If this node has a parent when this method is
245     * called, the new parent essentially takes the place of this node within the list of children of the old parent. This method
246     * does nothing if the supplied new parent is null.
247     * <p>
248     * For example, consider a plan node tree before this method is called:
249     * 
250     * <pre>
251     *        A
252     *      / | \
253     *     /  |  \
254     *    B   C   D
255     * </pre>
256     * 
257     * Then after this method is called with <code>c.insertAsParent(e)</code>, the resulting plan node tree will be:
258     * 
259     * <pre>
260     *        A
261     *      / | \
262     *     /  |  \
263     *    B   E   D
264     *        |
265     *        |
266     *        C
267     * </pre>
268     * 
269     * </p>
270     * <p>
271     * Also note that the node on which this method is called ('C' in the example above) will always be added as the
272     * {@link #addLastChild(AstNode) last child} to the new parent. This allows the new parent to already have children before
273     * this method is called.
274     * </p>
275     * 
276     * @param newParent the new parent; method does nothing if this is null
277     */
278    public void insertAsParent( AstNode newParent ) {
279        if (newParent == null) {
280            return;
281        }
282        newParent.removeFromParent();
283        if (this.parent != null) {
284            this.parent.replaceChild(this, newParent);
285        }
286        newParent.addLastChild(this);
287    }
288
289    /**
290     * Remove this node from its parent, and return the node that used to be the parent of this node. Note that this method
291     * removes the entire subgraph under this node.
292     * 
293     * @return the node that was the parent of this node, or null if this node had no parent
294     * @see #extractChild(AstNode)
295     * @see #extractFromParent()
296     */
297    public AstNode removeFromParent() {
298        AstNode result = this.parent;
299        if (this.parent != null) {
300            // Remove this node from its current parent ...
301            this.parent.children.remove(this);
302            this.parent = null;
303        }
304        return result;
305    }
306
307    /**
308     * Replace the supplied child with another node. If the replacement is already a child of this node, this method effectively
309     * swaps the position of the child and replacement nodes.
310     * 
311     * @param child the node that is already a child and that is to be replaced; may not be null and must be a child
312     * @param replacement the node that is to replace the 'child' node; may not be null
313     * @return true if the child was successfully replaced
314     */
315    public boolean replaceChild( AstNode child,
316                                 AstNode replacement ) {
317        assert child != null;
318        assert replacement != null;
319        if (child.parent == this) {
320            int i = this.children.indexOf(child);
321            if (replacement.parent == this) {
322                // Swapping the positions ...
323                int j = this.children.indexOf(replacement);
324                this.children.set(i, replacement);
325                this.children.set(j, child);
326                return true;
327            }
328            // The replacement is not yet a child ...
329            this.children.set(i, replacement);
330            replacement.removeFromParent();
331            replacement.parent = this;
332            child.parent = null;
333            return true;
334        }
335        return false;
336    }
337
338    /**
339     * Get the number of child nodes.
340     * 
341     * @return the number of children; never negative
342     */
343    public int getChildCount() {
344        return this.children.size();
345    }
346
347    /**
348     * Get the first child.
349     * 
350     * @return the first child, or null if there are no children
351     */
352    public AstNode getFirstChild() {
353        return this.children.isEmpty() ? null : this.children.getFirst();
354    }
355
356    /**
357     * Get the last child.
358     * 
359     * @return the last child, or null if there are no children
360     */
361    public AstNode getLastChild() {
362        return this.children.isEmpty() ? null : this.children.getLast();
363    }
364
365    /**
366     * @param name the name of the child being requested (cannot be <code>null</code> or empty)
367     * @return a collection of children with the specified name (never <code>null</code> but can be empty)
368     */
369    public List<AstNode> childrenWithName( final String name ) {
370        CheckArg.isNotEmpty(name, "name");
371
372        if (this.children.isEmpty()) {
373            return Collections.emptyList();
374        }
375
376        final List<AstNode> matches = new ArrayList<AstNode>();
377
378        for (final AstNode kid : this.children) {
379            if (name.equals(kid.getName())) {
380                matches.add(kid);
381            }
382        }
383
384        return matches;
385    }
386
387    /**
388     * Get the child at the supplied index.
389     * 
390     * @param index the index
391     * @return the child, or null if there are no children
392     * @throws IndexOutOfBoundsException if the index is not valid given the number of children
393     */
394    public AstNode getChild( int index ) {
395        return this.children.isEmpty() ? null : this.children.get(index);
396    }
397
398    /**
399     * Add the supplied node to the front of the list of children.
400     * 
401     * @param child the node that should be added as the first child; may not be null
402     */
403    public void addFirstChild( AstNode child ) {
404        assert child != null;
405        this.children.addFirst(child);
406        child.removeFromParent();
407        child.parent = this;
408    }
409
410    /**
411     * Add the supplied node to the end of the list of children.
412     * 
413     * @param child the node that should be added as the last child; may not be null
414     */
415    public void addLastChild( AstNode child ) {
416        assert child != null;
417        this.children.addLast(child);
418        child.removeFromParent();
419        child.parent = this;
420    }
421
422    /**
423     * Add the supplied nodes at the end of the list of children.
424     * 
425     * @param otherChildren the children to add; may not be null
426     */
427    public void addChildren( Iterable<AstNode> otherChildren ) {
428        assert otherChildren != null;
429        for (AstNode planNode : otherChildren) {
430            this.addLastChild(planNode);
431        }
432    }
433
434    /**
435     * Add the supplied nodes at the end of the list of children.
436     * 
437     * @param first the first child to add
438     * @param second the second child to add
439     */
440    public void addChildren( AstNode first,
441                             AstNode second ) {
442        if (first != null) {
443            this.addLastChild(first);
444        }
445        if (second != null) {
446            this.addLastChild(second);
447        }
448    }
449
450    /**
451     * Add the supplied nodes at the end of the list of children.
452     * 
453     * @param first the first child to add
454     * @param second the second child to add
455     * @param third the third child to add
456     */
457    public void addChildren( AstNode first,
458                             AstNode second,
459                             AstNode third ) {
460        if (first != null) {
461            this.addLastChild(first);
462        }
463        if (second != null) {
464            this.addLastChild(second);
465        }
466        if (third != null) {
467            this.addLastChild(third);
468        }
469    }
470
471    /**
472     * Remove the node from this node.
473     * 
474     * @param child the child node; may not be null
475     * @return true if the child was removed from this node, or false if the supplied node was not a child of this node
476     */
477    public boolean removeChild( AstNode child ) {
478        boolean result = this.children.remove(child);
479        if (result) {
480            child.parent = null;
481        }
482        return result;
483    }
484
485    /**
486     * Remove the child node from this node, and replace that child with its first child (if there is one).
487     * 
488     * @param child the child to be extracted; may not be null and must have at most 1 child
489     * @see #extractFromParent()
490     */
491    public void extractChild( AstNode child ) {
492        if (child.getChildCount() == 0) {
493            removeChild(child);
494        } else {
495            AstNode grandChild = child.getFirstChild();
496            replaceChild(child, grandChild);
497        }
498    }
499
500    /**
501     * Extract this node from its parent, but replace this node with its child (if there is one).
502     * 
503     * @see #extractChild(AstNode)
504     */
505    public void extractFromParent() {
506        this.parent.extractChild(this);
507    }
508
509    /**
510     * Get the unmodifiable list of child nodes. This list will immediately reflect any changes made to the children (via other
511     * methods), but this list cannot be used to add or remove children.
512     * 
513     * @return the list of children, which immediately reflects changes but which cannot be modified directly; never null
514     */
515    public List<AstNode> getChildren() {
516        return childrenView;
517    }
518
519    /**
520     * @param mixin the mixin to match children with (cannot be <code>null</code> or empty)
521     * @return the children having the specified mixin (never <code>null</code>)
522     */
523    public List<AstNode> getChildren( final String mixin ) {
524        final List<AstNode> result = new ArrayList<AstNode>();
525
526        for (final AstNode kid : getChildren()) {
527            if (kid.getMixins().contains(mixin)) {
528                result.add(kid);
529            }
530        }
531
532        return result;
533    }
534
535    /**
536     * {@inheritDoc}
537     * <p>
538     * This iterator is immutable.
539     * </p>
540     * 
541     * @see java.lang.Iterable#iterator()
542     */
543    @Override
544    public Iterator<AstNode> iterator() {
545        return childrenView.iterator();
546    }
547
548    /**
549     * Remove all children from this node. All nodes immediately become orphaned. The resulting list will be mutable.
550     * 
551     * @return a copy of all the of the children that were removed (and which have no parent); never null
552     */
553    public List<AstNode> removeAllChildren() {
554        if (this.children.isEmpty()) {
555            return new ArrayList<AstNode>(0);
556        }
557        List<AstNode> copyOfChildren = new ArrayList<AstNode>(this.children);
558        for (Iterator<AstNode> childIter = this.children.iterator(); childIter.hasNext();) {
559            AstNode child = childIter.next();
560            childIter.remove();
561            child.parent = null;
562        }
563        return copyOfChildren;
564    }
565
566    /**
567     * Determine whether the supplied plan is equivalent to this plan.
568     * 
569     * @param other the other plan to compare with this instance
570     * @return true if the two plans are equivalent, or false otherwise
571     */
572    public boolean isSameAs( AstNode other ) {
573        if (other == null) {
574            return false;
575        }
576        if (!this.name.equals(other.name)) {
577            return false;
578        }
579        if (!this.properties.equals(other.properties)) {
580            return false;
581        }
582        if (this.getChildCount() != other.getChildCount()) {
583            return false;
584        }
585        Iterator<AstNode> thisChildren = this.getChildren().iterator();
586        Iterator<AstNode> thatChildren = other.getChildren().iterator();
587        while (thisChildren.hasNext() && thatChildren.hasNext()) {
588            if (!thisChildren.next().isSameAs(thatChildren.next())) {
589                return false;
590            }
591        }
592        return true;
593    }
594
595    /**
596     * {@inheritDoc}
597     * <p>
598     * This class returns a new clone of the plan tree rooted at this node. However, the top node of the resulting plan tree (that
599     * is, the node returned from this method) has no parent.
600     * </p>
601     * 
602     * @see java.lang.Object#clone()
603     */
604    @Override
605    public AstNode clone() {
606        return cloneWithoutNewParent();
607    }
608
609    protected AstNode cloneWithoutNewParent() {
610        AstNode result = new AstNode(this.name);
611        result.properties.putAll(this.properties);
612        // Clone the children ...
613        for (AstNode child : children) {
614            AstNode childClone = child.cloneWithoutNewParent();
615            // The child has no parent, so add the child to the new result ...
616            result.addLastChild(childClone);
617        }
618        return result;
619    }
620
621    @Override
622    public String toString() {
623        StringBuilder stringBuilder = new StringBuilder();
624        stringBuilder.append(getAbsolutePath());
625        stringBuilder.append("[");
626        for (Iterator<String> propertyIt = properties.keySet().iterator(); propertyIt.hasNext();) {
627            String propertyName = propertyIt.next();
628            stringBuilder.append(propertyName).append(":").append(properties.get(propertyName));
629            if (propertyIt.hasNext()) {
630                {
631                    stringBuilder.append(", ");
632                }
633            }
634        }
635        stringBuilder.append("]");
636        return stringBuilder.toString();
637    }
638}