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 }