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 }