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}