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; 025 026 import java.util.ArrayList; 027 import java.util.HashSet; 028 import java.util.Iterator; 029 import java.util.List; 030 import java.util.NoSuchElementException; 031 import java.util.Set; 032 import java.util.UUID; 033 import net.jcip.annotations.Immutable; 034 import org.jboss.dna.common.text.TextEncoder; 035 import org.jboss.dna.common.util.CheckArg; 036 import org.jboss.dna.common.util.HashCode; 037 import org.jboss.dna.graph.property.Name; 038 import org.jboss.dna.graph.property.NamespaceRegistry; 039 import org.jboss.dna.graph.property.Path; 040 import org.jboss.dna.graph.property.Property; 041 042 /** 043 * The location of a node, as specified by either its path, UUID, and/or identification properties. 044 * 045 * @author Randall Hauch 046 */ 047 @Immutable 048 public abstract class Location implements Iterable<Property>, Comparable<Location> { 049 050 /** 051 * Simple shared iterator instance that is used when there are no properties. 052 */ 053 protected static final Iterator<Property> NO_ID_PROPERTIES_ITERATOR = new Iterator<Property>() { 054 public boolean hasNext() { 055 return false; 056 } 057 058 public Property next() { 059 throw new NoSuchElementException(); 060 } 061 062 public void remove() { 063 throw new UnsupportedOperationException(); 064 } 065 }; 066 067 /** 068 * Create a location defined by a path. 069 * 070 * @param path the path 071 * @return a new <code>Location</code> with the given path and no identification properties 072 * @throws IllegalArgumentException if <code>path</code> is null 073 */ 074 public static Location create( Path path ) { 075 CheckArg.isNotNull(path, "path"); 076 077 return new LocationWithPath(path); 078 } 079 080 /** 081 * Create a location defined by a UUID. 082 * 083 * @param uuid the UUID 084 * @return a new <code>Location</code> with no path and a single identification property with the name {@link DnaLexicon#UUID} 085 * and the given <code>uuid</code> for a value. 086 * @throws IllegalArgumentException if <code>uuid</code> is null 087 */ 088 public static Location create( UUID uuid ) { 089 CheckArg.isNotNull(uuid, "uuid"); 090 return new LocationWithUuid(uuid); 091 } 092 093 /** 094 * Create a location defined by a path and an UUID. 095 * 096 * @param path the path 097 * @param uuid the UUID, or null if there is no UUID 098 * @return a new <code>Location</code> with the given path (if any) and a single identification property with the name 099 * {@link DnaLexicon#UUID} and the given <code>uuid</code> (if it is present) for a value. 100 * @throws IllegalArgumentException if <code>path</code> is null 101 */ 102 public static Location create( Path path, 103 UUID uuid ) { 104 if (path == null) { 105 CheckArg.isNotNull(uuid, "uuid"); 106 return new LocationWithUuid(uuid); 107 } 108 if (uuid == null) return new LocationWithPath(path); 109 return new LocationWithPathAndUuid(path, uuid); 110 } 111 112 /** 113 * Create a location defined by a path and a single identification property. 114 * 115 * @param path the path 116 * @param idProperty the identification property 117 * @return a new <code>Location</code> with the given path and identification property (if it is present). 118 * @throws IllegalArgumentException if <code>path</code> or <code>idProperty</code> is null 119 */ 120 public static Location create( Path path, 121 Property idProperty ) { 122 CheckArg.isNotNull(path, "path"); 123 CheckArg.isNotNull(idProperty, "idProperty"); 124 return new LocationWithPathAndProperty(path, idProperty); 125 } 126 127 /** 128 * Create a location defined by a path and multiple identification properties. 129 * 130 * @param path the path 131 * @param firstIdProperty the first identification property 132 * @param remainingIdProperties the remaining identification property 133 * @return a new <code>Location</code> with the given path and identification properties. 134 * @throws IllegalArgumentException if any of the arguments are null 135 */ 136 public static Location create( Path path, 137 Property firstIdProperty, 138 Property... remainingIdProperties ) { 139 CheckArg.isNotNull(path, "path"); 140 CheckArg.isNotNull(firstIdProperty, "firstIdProperty"); 141 CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties"); 142 List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length); 143 Set<Name> names = new HashSet<Name>(); 144 names.add(firstIdProperty.getName()); 145 idProperties.add(firstIdProperty); 146 for (Property property : remainingIdProperties) { 147 if (names.add(property.getName())) idProperties.add(property); 148 } 149 return new LocationWithPathAndProperties(path, idProperties); 150 } 151 152 /** 153 * Create a location defined by a path and an iterator over identification properties. 154 * 155 * @param path the path 156 * @param idProperties the iterator over the identification properties 157 * @return a new <code>Location</code> with the given path and identification properties 158 * @throws IllegalArgumentException if any of the arguments are null 159 */ 160 public static Location create( Path path, 161 Iterable<Property> idProperties ) { 162 CheckArg.isNotNull(path, "path"); 163 CheckArg.isNotNull(idProperties, "idProperties"); 164 List<Property> idPropertiesList = new ArrayList<Property>(); 165 Set<Name> names = new HashSet<Name>(); 166 for (Property property : idProperties) { 167 if (names.add(property.getName())) idPropertiesList.add(property); 168 } 169 switch (idPropertiesList.size()) { 170 case 0: 171 return new LocationWithPath(path); 172 case 1: 173 return new LocationWithPathAndProperty(path, idPropertiesList.get(0)); 174 default: 175 return new LocationWithPathAndProperties(path, idPropertiesList); 176 } 177 } 178 179 /** 180 * Create a location defined by a single identification property. 181 * 182 * @param idProperty the identification property 183 * @return a new <code>Location</code> with no path and the given identification property. 184 * @throws IllegalArgumentException if <code>idProperty</code> is null 185 */ 186 public static Location create( Property idProperty ) { 187 CheckArg.isNotNull(idProperty, "idProperty"); 188 return new LocationWithProperty(idProperty); 189 } 190 191 /** 192 * Create a location defined by multiple identification properties. 193 * 194 * @param firstIdProperty the first identification property 195 * @param remainingIdProperties the remaining identification property 196 * @return a new <code>Location</code> with no path and the given and identification properties. 197 * @throws IllegalArgumentException if any of the arguments are null 198 */ 199 public static Location create( Property firstIdProperty, 200 Property... remainingIdProperties ) { 201 CheckArg.isNotNull(firstIdProperty, "firstIdProperty"); 202 CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties"); 203 if (remainingIdProperties.length == 0) return new LocationWithProperty(firstIdProperty); 204 List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length); 205 Set<Name> names = new HashSet<Name>(); 206 names.add(firstIdProperty.getName()); 207 idProperties.add(firstIdProperty); 208 for (Property property : remainingIdProperties) { 209 if (names.add(property.getName())) idProperties.add(property); 210 } 211 return new LocationWithProperties(idProperties); 212 } 213 214 /** 215 * Create a location defined by a path and an iterator over identification properties. 216 * 217 * @param idProperties the iterator over the identification properties 218 * @return a new <code>Location</code> with no path and the given identification properties. 219 * @throws IllegalArgumentException if any of the arguments are null 220 */ 221 public static Location create( Iterable<Property> idProperties ) { 222 CheckArg.isNotNull(idProperties, "idProperties"); 223 List<Property> idPropertiesList = new ArrayList<Property>(); 224 Set<Name> names = new HashSet<Name>(); 225 for (Property property : idProperties) { 226 if (names.add(property.getName())) idPropertiesList.add(property); 227 } 228 switch (idPropertiesList.size()) { 229 case 0: 230 CheckArg.isNotEmpty(idPropertiesList, "idProperties"); 231 assert false; 232 return null; // never get here 233 case 1: 234 return new LocationWithProperty(idPropertiesList.get(0)); 235 default: 236 return new LocationWithProperties(idPropertiesList); 237 } 238 } 239 240 /** 241 * Create a location defined by multiple identification properties. This method does not check whether the identification 242 * properties are duplicated. 243 * 244 * @param idProperties the identification properties 245 * @return a new <code>Location</code> with no path and the given identification properties. 246 * @throws IllegalArgumentException if <code>idProperties</code> is null or empty 247 */ 248 public static Location create( List<Property> idProperties ) { 249 CheckArg.isNotEmpty(idProperties, "idProperties"); 250 return new LocationWithPathAndProperties(null, idProperties); 251 } 252 253 /** 254 * Get the path that (at least in part) defines this location. 255 * 256 * @return the path, or null if this location is not defined with a path 257 */ 258 public abstract Path getPath(); 259 260 /** 261 * Return whether this location is defined (at least in part) by a path. 262 * 263 * @return true if a {@link #getPath() path} helps define this location 264 */ 265 public boolean hasPath() { 266 return getPath() != null; 267 } 268 269 /** 270 * Get the identification properties that (at least in part) define this location. 271 * 272 * @return the identification properties, or null if this location is not defined with identification properties 273 */ 274 public abstract List<Property> getIdProperties(); 275 276 /** 277 * Return whether this location is defined (at least in part) with identification properties. 278 * 279 * @return true if a {@link #getIdProperties() identification properties} help define this location 280 */ 281 public boolean hasIdProperties() { 282 return getIdProperties() != null && getIdProperties().size() != 0; 283 } 284 285 /** 286 * Get the identification property with the supplied name, if there is such a property. 287 * 288 * @param name the name of the identification property 289 * @return the identification property with the supplied name, or null if there is no such property (or if there 290 * {@link #hasIdProperties() are no identification properties} 291 */ 292 public Property getIdProperty( Name name ) { 293 CheckArg.isNotNull(name, "name"); 294 if (getIdProperties() != null) { 295 for (Property property : getIdProperties()) { 296 if (property.getName().equals(name)) return property; 297 } 298 } 299 return null; 300 } 301 302 /** 303 * Get the first UUID that is in one of the {@link #getIdProperties() identification properties}. 304 * 305 * @return the UUID for this location, or null if there is no such identification property 306 */ 307 public UUID getUuid() { 308 Property property = getIdProperty(DnaLexicon.UUID); 309 if (property != null && !property.isEmpty()) { 310 Object value = property.getFirstValue(); 311 if (value instanceof UUID) return (UUID)value; 312 } 313 return null; 314 } 315 316 /** 317 * Compare this location to the supplied location, and determine whether the two locations represent the same logical 318 * location. One location is considered the same as another location when one location is a superset of the other. For 319 * example, consider the following locations: 320 * <ul> 321 * <li>location A is defined with a "<code>/x/y</code>" path</li> 322 * <li>location B is defined with an identification property {id=3}</li> 323 * <li>location C is defined with a "<code>/x/y/z</code>"</li> 324 * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li> 325 * </ul> 326 * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other 327 * combinations would be considered the same. 328 * <p> 329 * Note that passing a null location as a parameter will always return false. 330 * </p> 331 * 332 * @param other the other location to compare 333 * @return true if the two locations represent the same location, or false otherwise 334 */ 335 public boolean isSame( Location other ) { 336 return isSame(other, true); 337 } 338 339 /** 340 * Compare this location to the supplied location, and determine whether the two locations represent the same logical 341 * location. One location is considered the same as another location when one location is a superset of the other. For 342 * example, consider the following locations: 343 * <ul> 344 * <li>location A is defined with a "<code>/x/y</code>" path</li> 345 * <li>location B is defined with an identification property {id=3}</li> 346 * <li>location C is defined with a "<code>/x/y/z</code>"</li> 347 * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li> 348 * </ul> 349 * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other 350 * combinations would be considered the same. 351 * <p> 352 * Note that passing a null location as a parameter will always return false. 353 * </p> 354 * 355 * @param other the other location to compare 356 * @param requireSameNameSiblingIndexes true if the paths must have equivalent {@link Path.Segment#getIndex() 357 * same-name-sibling indexes}, or false if the same-name-siblings may be different 358 * @return true if the two locations represent the same location, or false otherwise 359 */ 360 public boolean isSame( Location other, 361 boolean requireSameNameSiblingIndexes ) { 362 if (other != null) { 363 if (this.hasPath() && other.hasPath()) { 364 // Paths on both, so the paths MUST match 365 if (requireSameNameSiblingIndexes) { 366 if (!this.getPath().equals(other.getPath())) return false; 367 } else { 368 Path thisPath = this.getPath(); 369 Path thatPath = other.getPath(); 370 if (thisPath.isRoot()) return thatPath.isRoot(); 371 if (thatPath.isRoot()) return thisPath.isRoot(); 372 // The parents must match ... 373 if (!thisPath.hasSameAncestor(thatPath)) return false; 374 // And the names of the last segments must match ... 375 if (!thisPath.getLastSegment().getName().equals(thatPath.getLastSegment().getName())) return false; 376 } 377 378 // And the identification properties must match only if they exist on both 379 if (this.hasIdProperties() && other.hasIdProperties()) { 380 return this.getIdProperties().containsAll(other.getIdProperties()); 381 } 382 return true; 383 } 384 // Path only in one, so the identification properties MUST match 385 if (!other.hasIdProperties()) return false; 386 return this.getIdProperties().containsAll(other.getIdProperties()); 387 } 388 return false; 389 } 390 391 /** 392 * {@inheritDoc} 393 * 394 * @see java.lang.Iterable#iterator() 395 */ 396 public Iterator<Property> iterator() { 397 return getIdProperties() != null ? getIdProperties().iterator() : NO_ID_PROPERTIES_ITERATOR; 398 } 399 400 /** 401 * {@inheritDoc} 402 * 403 * @see java.lang.Object#hashCode() 404 */ 405 @Override 406 public int hashCode() { 407 return HashCode.compute(getPath(), getIdProperties()); 408 } 409 410 /** 411 * {@inheritDoc} 412 * 413 * @see java.lang.Object#equals(java.lang.Object) 414 */ 415 @Override 416 public boolean equals( Object obj ) { 417 if (obj instanceof Location) { 418 Location that = (Location)obj; 419 if (this.hasPath()) { 420 if (!this.getPath().equals(that.getPath())) return false; 421 } else { 422 if (that.hasPath()) return false; 423 } 424 if (this.hasIdProperties()) { 425 if (!this.getIdProperties().equals(that.getIdProperties())) return false; 426 } else { 427 if (that.hasIdProperties()) return false; 428 } 429 return true; 430 } 431 return false; 432 } 433 434 /** 435 * {@inheritDoc} 436 * 437 * @see java.lang.Comparable#compareTo(java.lang.Object) 438 */ 439 public int compareTo( Location that ) { 440 if (this == that) return 0; 441 if (this.hasPath() && that.hasPath()) { 442 return this.getPath().compareTo(that.getPath()); 443 } 444 UUID thisUuid = this.getUuid(); 445 UUID thatUuid = that.getUuid(); 446 if (thisUuid != null && thatUuid != null) { 447 return thisUuid.compareTo(thatUuid); 448 } 449 return this.hashCode() - that.hashCode(); 450 } 451 452 /** 453 * Get the string form of the location. 454 * 455 * @return the string 456 * @see #getString(TextEncoder) 457 * @see #getString(NamespaceRegistry) 458 * @see #getString(NamespaceRegistry, TextEncoder) 459 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 460 */ 461 public String getString() { 462 return getString(null, null, null); 463 } 464 465 /** 466 * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path 467 * and properties. 468 * 469 * @param encoder the encoder to use, or null if the default encoder should be used 470 * @return the encoded string 471 * @see #getString() 472 * @see #getString(NamespaceRegistry) 473 * @see #getString(NamespaceRegistry, TextEncoder) 474 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 475 */ 476 public String getString( TextEncoder encoder ) { 477 return getString(null, encoder, null); 478 } 479 480 /** 481 * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path 482 * and properties. 483 * 484 * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if 485 * no namespace registry should be used 486 * @return the encoded string 487 * @see #getString() 488 * @see #getString(TextEncoder) 489 * @see #getString(NamespaceRegistry, TextEncoder) 490 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 491 */ 492 public String getString( NamespaceRegistry namespaceRegistry ) { 493 return getString(namespaceRegistry, null, null); 494 } 495 496 /** 497 * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path 498 * and properties. 499 * 500 * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if 501 * no namespace registry should be used 502 * @param encoder the encoder to use, or null if the default encoder should be used 503 * @return the encoded string 504 * @see #getString() 505 * @see #getString(TextEncoder) 506 * @see #getString(NamespaceRegistry) 507 * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder) 508 */ 509 public String getString( NamespaceRegistry namespaceRegistry, 510 TextEncoder encoder ) { 511 return getString(namespaceRegistry, encoder, null); 512 } 513 514 /** 515 * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path 516 * and properties. 517 * 518 * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if 519 * no namespace registry should be used 520 * @param encoder the encoder to use, or null if the default encoder should be used 521 * @param delimiterEncoder the encoder to use for encoding the delimiters in paths, names, and properties, or null if the 522 * standard delimiters should be used 523 * @return the encoded string 524 * @see #getString() 525 * @see #getString(TextEncoder) 526 * @see #getString(NamespaceRegistry) 527 * @see #getString(NamespaceRegistry, TextEncoder) 528 */ 529 public String getString( NamespaceRegistry namespaceRegistry, 530 TextEncoder encoder, 531 TextEncoder delimiterEncoder ) { 532 StringBuilder sb = new StringBuilder(); 533 sb.append("{ "); 534 boolean hasPath = this.hasPath(); 535 if (hasPath) { 536 sb.append(this.getPath().getString(namespaceRegistry, encoder, delimiterEncoder)); 537 } 538 if (this.hasIdProperties()) { 539 if (hasPath) sb.append(" && "); 540 sb.append("["); 541 boolean first = true; 542 for (Property idProperty : this.getIdProperties()) { 543 if (first) first = false; 544 else sb.append(", "); 545 sb.append(idProperty.getString(namespaceRegistry, encoder, delimiterEncoder)); 546 } 547 sb.append("]"); 548 } 549 sb.append(" }"); 550 return sb.toString(); 551 } 552 553 /** 554 * {@inheritDoc} 555 * 556 * @see java.lang.Object#toString() 557 */ 558 @Override 559 public String toString() { 560 StringBuilder sb = new StringBuilder(); 561 boolean hasPath = this.hasPath(); 562 boolean hasProps = this.hasIdProperties(); 563 if (hasPath) { 564 if (hasProps) { 565 sb.append("<"); 566 } 567 sb.append(this.getPath()); 568 } 569 if (hasProps) { 570 if (hasPath) sb.append(" && "); 571 sb.append("["); 572 boolean first = true; 573 for (Property idProperty : this.getIdProperties()) { 574 if (first) first = false; 575 else sb.append(", "); 576 sb.append(idProperty); 577 } 578 sb.append("]"); 579 if (hasPath) { 580 sb.append(">"); 581 } 582 } 583 return sb.toString(); 584 } 585 586 /** 587 * Create a copy of this location that adds the supplied identification property. The new identification property will replace 588 * any existing identification property with the same name on the original. 589 * 590 * @param newIdProperty the new identification property, which may be null 591 * @return the new location, or this location if the new identification property is null or empty 592 */ 593 public abstract Location with( Property newIdProperty ); 594 595 /** 596 * Create a copy of this location that uses the supplied path. 597 * 598 * @param newPath the new path for the location 599 * @return the new location, or this location if the path is equal to this location's path 600 */ 601 public abstract Location with( Path newPath ); 602 603 /** 604 * Create a copy of this location that adds the supplied UUID as an identification property. The new identification property 605 * will replace any existing identification property with the same name on the original. 606 * 607 * @param uuid the new UUID, which may be null 608 * @return the new location, or this location if the new identification property is null or empty 609 */ 610 public abstract Location with( UUID uuid ); 611 612 }