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.graph; 023 024 import java.util.ArrayList; 025 import java.util.Collections; 026 import java.util.Iterator; 027 import java.util.List; 028 import java.util.NoSuchElementException; 029 import java.util.UUID; 030 import net.jcip.annotations.Immutable; 031 import org.jboss.dna.common.util.CheckArg; 032 import org.jboss.dna.common.util.HashCode; 033 import org.jboss.dna.graph.properties.Name; 034 import org.jboss.dna.graph.properties.Path; 035 import org.jboss.dna.graph.properties.Property; 036 import org.jboss.dna.graph.properties.basic.BasicSingleValueProperty; 037 038 /** 039 * The location of a node, as specified by either its path, UUID, and/or identification properties. 040 * 041 * @author Randall Hauch 042 */ 043 @Immutable 044 public class Location implements Iterable<Property> { 045 046 private static final Iterator<Property> NO_ID_PROPERTIES_ITERATOR = new Iterator<Property>() { 047 public boolean hasNext() { 048 return false; 049 } 050 051 public Property next() { 052 throw new NoSuchElementException(); 053 } 054 055 public void remove() { 056 throw new UnsupportedOperationException(); 057 } 058 }; 059 060 private final Path path; 061 private final List<Property> idProperties; 062 063 /** 064 * Create a location defined by a path. 065 * 066 * @param path the path 067 * @throws IllegalArgumentException if <code>path</code> is null 068 */ 069 public Location( Path path ) { 070 CheckArg.isNotNull(path, "path"); 071 this.path = path; 072 this.idProperties = null; 073 } 074 075 /** 076 * Create a location defined by a UUID. 077 * 078 * @param uuid the UUID 079 * @throws IllegalArgumentException if <code>uuid</code> is null 080 */ 081 public Location( UUID uuid ) { 082 CheckArg.isNotNull(uuid, "uuid"); 083 this.path = null; 084 Property idProperty = new BasicSingleValueProperty(DnaLexicon.UUID, uuid); 085 this.idProperties = Collections.singletonList(idProperty); 086 } 087 088 /** 089 * Create a location defined by a path and an UUID. 090 * 091 * @param path the path 092 * @param uuid the UUID, or null if there is no UUID 093 * @throws IllegalArgumentException if <code>path</code> is null 094 */ 095 public Location( Path path, 096 UUID uuid ) { 097 CheckArg.isNotNull(uuid, "uuid"); 098 this.path = path; 099 if (uuid != null) { 100 Property idProperty = new BasicSingleValueProperty(DnaLexicon.UUID, uuid); 101 this.idProperties = Collections.singletonList(idProperty); 102 } else { 103 this.idProperties = null; 104 } 105 } 106 107 /** 108 * Create a location defined by a path and a single identification property. 109 * 110 * @param path the path 111 * @param idProperty the identification property 112 * @throws IllegalArgumentException if <code>path</code> or <code>idProperty</code> is null 113 */ 114 public Location( Path path, 115 Property idProperty ) { 116 CheckArg.isNotNull(path, "path"); 117 CheckArg.isNotNull(idProperty, "idProperty"); 118 this.path = path; 119 this.idProperties = idProperty != null ? Collections.singletonList(idProperty) : null; 120 } 121 122 /** 123 * Create a location defined by a path and multiple identification properties. 124 * 125 * @param path the path 126 * @param firstIdProperty the first identification property 127 * @param remainingIdProperties the remaining identification property 128 * @throws IllegalArgumentException if any of the arguments are null 129 */ 130 public Location( Path path, 131 Property firstIdProperty, 132 Property... remainingIdProperties ) { 133 CheckArg.isNotNull(path, "path"); 134 CheckArg.isNotNull(firstIdProperty, "firstIdProperty"); 135 CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties"); 136 this.path = path; 137 List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length); 138 idProperties.add(firstIdProperty); 139 for (Property property : remainingIdProperties) { 140 idProperties.add(property); 141 } 142 this.idProperties = Collections.unmodifiableList(idProperties); 143 } 144 145 /** 146 * Create a location defined by a path and an iterator over identification properties. 147 * 148 * @param path the path 149 * @param idProperties the iterator over the identification properties 150 * @throws IllegalArgumentException if any of the arguments are null 151 */ 152 public Location( Path path, 153 Iterable<Property> idProperties ) { 154 CheckArg.isNotNull(path, "path"); 155 CheckArg.isNotNull(idProperties, "idProperties"); 156 this.path = path; 157 List<Property> idPropertiesList = new ArrayList<Property>(); 158 for (Property property : idProperties) { 159 idPropertiesList.add(property); 160 } 161 this.idProperties = Collections.unmodifiableList(idPropertiesList); 162 } 163 164 /** 165 * Create a location defined by a single identification property. 166 * 167 * @param idProperty the identification property 168 * @throws IllegalArgumentException if <code>idProperty</code> is null 169 */ 170 public Location( Property idProperty ) { 171 CheckArg.isNotNull(idProperty, "idProperty"); 172 this.path = null; 173 this.idProperties = Collections.singletonList(idProperty); 174 } 175 176 /** 177 * Create a location defined by multiple identification properties. 178 * 179 * @param firstIdProperty the first identification property 180 * @param remainingIdProperties the remaining identification property 181 * @throws IllegalArgumentException if any of the arguments are null 182 */ 183 public Location( Property firstIdProperty, 184 Property... remainingIdProperties ) { 185 CheckArg.isNotNull(firstIdProperty, "firstIdProperty"); 186 CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties"); 187 this.path = null; 188 List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length); 189 idProperties.add(firstIdProperty); 190 for (Property property : remainingIdProperties) { 191 idProperties.add(property); 192 } 193 this.idProperties = Collections.unmodifiableList(idProperties); 194 } 195 196 /** 197 * Create a location defined by a path and an iterator over identification properties. 198 * 199 * @param idProperties the iterator over the identification properties 200 * @throws IllegalArgumentException if any of the arguments are null 201 */ 202 public Location( Iterable<Property> idProperties ) { 203 CheckArg.isNotNull(idProperties, "idProperties"); 204 this.path = null; 205 List<Property> idPropertiesList = new ArrayList<Property>(); 206 for (Property property : idProperties) { 207 idPropertiesList.add(property); 208 } 209 this.idProperties = Collections.unmodifiableList(idPropertiesList); 210 } 211 212 /** 213 * Create a location defined by multiple identification properties. 214 * 215 * @param idProperties the identification properties 216 * @throws IllegalArgumentException if <code>idProperties</code> is null or empty 217 */ 218 public Location( List<Property> idProperties ) { 219 CheckArg.isNotEmpty(idProperties, "idProperties"); 220 this.path = null; 221 this.idProperties = idProperties; 222 } 223 224 /** 225 * Create a location defined by a path and multiple identification properties. 226 * 227 * @param path the path 228 * @param idProperties the identification properties 229 * @throws IllegalArgumentException if <code>path</code> is null, or if <code>idProperties</code> is empty 230 */ 231 protected Location( Path path, 232 List<Property> idProperties ) { 233 CheckArg.isNotNull(path, "path"); 234 CheckArg.isNotEmpty(idProperties, "idProperties"); 235 this.path = path; 236 this.idProperties = idProperties; 237 } 238 239 /** 240 * Create a location from another but adding the supplied identification property. The new identification property will 241 * replace any existing identification property with the same name on the original. 242 * 243 * @param original the original location 244 * @param newIdProperty the new identification property 245 * @throws IllegalArgumentException if <code>original</code> is null 246 */ 247 protected Location( Location original, 248 Property newIdProperty ) { 249 CheckArg.isNotNull(original, "original"); 250 this.path = original.getPath(); 251 if (original.hasIdProperties()) { 252 List<Property> originalIdProperties = original.getIdProperties(); 253 if (newIdProperty == null) { 254 this.idProperties = original.idProperties; 255 } else { 256 List<Property> idProperties = new ArrayList<Property>(originalIdProperties.size() + 1); 257 for (Property property : originalIdProperties) { 258 if (!newIdProperty.getName().equals(property.getName())) idProperties.add(property); 259 } 260 idProperties.add(newIdProperty); 261 this.idProperties = Collections.unmodifiableList(idProperties); 262 } 263 } else { 264 this.idProperties = Collections.singletonList(newIdProperty); 265 } 266 } 267 268 /** 269 * Create a location from another but adding the supplied identification property. The new identification property will 270 * replace any existing identification property with the same name on the original. 271 * 272 * @param original the original location 273 * @param newPath the new path for the location 274 * @throws IllegalArgumentException if <code>original</code> is null 275 */ 276 protected Location( Location original, 277 Path newPath ) { 278 CheckArg.isNotNull(original, "original"); 279 this.path = newPath != null ? newPath : original.getPath(); 280 this.idProperties = original.idProperties; 281 } 282 283 /** 284 * Get the path that (at least in part) defines this location. 285 * 286 * @return the path, or null if this location is not defined with a path 287 */ 288 public Path getPath() { 289 return path; 290 } 291 292 /** 293 * Return whether this location is defined (at least in part) by a path. 294 * 295 * @return true if a {@link #getPath() path} helps define this location 296 */ 297 public boolean hasPath() { 298 return path != null; 299 } 300 301 /** 302 * Get the identification properties that (at least in part) define this location. 303 * 304 * @return the identification properties, or null if this location is not defined with identification properties 305 */ 306 public List<Property> getIdProperties() { 307 return idProperties; 308 } 309 310 /** 311 * Return whether this location is defined (at least in part) with identification properties. 312 * 313 * @return true if a {@link #getIdProperties() identification properties} help define this location 314 */ 315 public boolean hasIdProperties() { 316 return idProperties != null && idProperties.size() != 0; 317 } 318 319 /** 320 * Get the identification property with the supplied name, if there is such a property. 321 * 322 * @param name the name of the identification property 323 * @return the identification property with the supplied name, or null if there is no such property (or if there 324 * {@link #hasIdProperties() are no identification properties} 325 */ 326 public Property getIdProperty( Name name ) { 327 CheckArg.isNotNull(name, "name"); 328 if (idProperties != null) { 329 for (Property property : idProperties) { 330 if (property.getName().equals(name)) return property; 331 } 332 } 333 return null; 334 } 335 336 /** 337 * Compare this location to the supplied location, and determine whether the two locations represent the same logical 338 * location. One location is considered the same as another location when one location is a superset of the other. For 339 * example, consider the following locations: 340 * <ul> 341 * <li>location A is defined with a "<code>/x/y</code>" path</li> 342 * <li>location B is defined with an identification property {id=3}</li> 343 * <li>location C is defined with a "<code>/x/y/z</code>"</li> 344 * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li> 345 * </ul> 346 * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other 347 * combinations would be considered the same. 348 * <p> 349 * Note that passing a null location as a parameter will always return false. 350 * </p> 351 * 352 * @param other the other location to compare 353 * @return true if the two locations represent the same location, or false otherwise 354 */ 355 public boolean isSame( Location other ) { 356 return isSame(other, true); 357 } 358 359 /** 360 * Compare this location to the supplied location, and determine whether the two locations represent the same logical 361 * location. One location is considered the same as another location when one location is a superset of the other. For 362 * example, consider the following locations: 363 * <ul> 364 * <li>location A is defined with a "<code>/x/y</code>" path</li> 365 * <li>location B is defined with an identification property {id=3}</li> 366 * <li>location C is defined with a "<code>/x/y/z</code>"</li> 367 * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li> 368 * </ul> 369 * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other 370 * combinations would be considered the same. 371 * <p> 372 * Note that passing a null location as a parameter will always return false. 373 * </p> 374 * 375 * @param other the other location to compare 376 * @param requireSameNameSiblingIndexes true if the paths must have equivalent {@link Path.Segment#getIndex() 377 * same-name-sibling indexes}, or false if the same-name-siblings may be different 378 * @return true if the two locations represent the same location, or false otherwise 379 */ 380 public boolean isSame( Location other, 381 boolean requireSameNameSiblingIndexes ) { 382 if (other != null) { 383 if (this.hasPath() && other.hasPath()) { 384 // Paths on both, so the paths MUST match 385 if (requireSameNameSiblingIndexes) { 386 if (!this.getPath().equals(other.getPath())) return false; 387 } else { 388 Path thisPath = this.getPath(); 389 Path thatPath = other.getPath(); 390 if (thisPath.isRoot() && thatPath.isRoot()) return true; 391 // The parents must match ... 392 if (!thisPath.hasSameAncestor(thatPath)) return false; 393 // And the names of the last segments must match ... 394 if (!thisPath.getLastSegment().getName().equals(thatPath.getLastSegment().getName())) return false; 395 } 396 397 // And the identification properties must match only if they exist on both 398 if (this.hasIdProperties() && other.hasIdProperties()) { 399 return this.getIdProperties().containsAll(other.getIdProperties()); 400 } 401 return true; 402 } 403 // Path only in one, so the identification properties MUST match 404 if (!other.hasIdProperties()) return false; 405 return this.getIdProperties().containsAll(other.getIdProperties()); 406 } 407 return false; 408 } 409 410 /** 411 * {@inheritDoc} 412 * 413 * @see java.lang.Iterable#iterator() 414 */ 415 public Iterator<Property> iterator() { 416 return idProperties != null ? idProperties.iterator() : NO_ID_PROPERTIES_ITERATOR; 417 } 418 419 /** 420 * {@inheritDoc} 421 * 422 * @see java.lang.Object#hashCode() 423 */ 424 @Override 425 public int hashCode() { 426 return HashCode.compute(path, idProperties); 427 } 428 429 /** 430 * {@inheritDoc} 431 * 432 * @see java.lang.Object#equals(java.lang.Object) 433 */ 434 @Override 435 public boolean equals( Object obj ) { 436 if (obj instanceof Location) { 437 Location that = (Location)obj; 438 if (this.hasPath()) { 439 if (!this.getPath().equals(that.getPath())) return false; 440 } else { 441 if (that.hasPath()) return false; 442 } 443 if (this.hasIdProperties()) { 444 if (!this.getIdProperties().equals(that.getIdProperties())) return false; 445 } else { 446 if (that.hasIdProperties()) return false; 447 } 448 return true; 449 } 450 return false; 451 } 452 453 /** 454 * {@inheritDoc} 455 * 456 * @see java.lang.Object#toString() 457 */ 458 @Override 459 public String toString() { 460 StringBuilder sb = new StringBuilder(); 461 if (this.hasPath()) { 462 if (this.hasIdProperties()) sb.append("[ "); 463 sb.append(this.getPath()); 464 if (this.hasIdProperties()) sb.append(" && "); 465 } 466 if (this.hasIdProperties()) { 467 sb.append(this.getIdProperties().toString()); 468 if (this.hasPath()) sb.append(" ]"); 469 } 470 return sb.toString(); 471 } 472 473 /** 474 * Create a copy of this location that adds the supplied identification property. The new identification property will replace 475 * any existing identification property with the same name on the original. 476 * 477 * @param newIdProperty the new identification property, which may be null 478 * @return the new location, or this location if the new identification property is null or empty 479 */ 480 public Location with( Property newIdProperty ) { 481 if (newIdProperty == null || newIdProperty.isEmpty()) return this; 482 return new Location(this, newIdProperty); 483 } 484 485 /** 486 * Create a copy of this location that uses the supplied path. 487 * 488 * @param newPath the new path for the location 489 * @return the new location, or this location if the path is equal to this location's path 490 */ 491 public Location with( Path newPath ) { 492 if (newPath == null) return this; 493 if (!this.path.equals(newPath)) return new Location(this, newPath); 494 return this; 495 } 496 497 }