View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.graph;
25  
26  import java.io.Serializable;
27  import java.util.ArrayList;
28  import java.util.Comparator;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.NoSuchElementException;
33  import java.util.Set;
34  import java.util.UUID;
35  import net.jcip.annotations.Immutable;
36  import org.modeshape.common.text.TextEncoder;
37  import org.modeshape.common.util.CheckArg;
38  import org.modeshape.common.util.HashCode;
39  import org.modeshape.graph.property.Name;
40  import org.modeshape.graph.property.NamespaceRegistry;
41  import org.modeshape.graph.property.Path;
42  import org.modeshape.graph.property.Property;
43  import org.modeshape.graph.property.Path.Segment;
44  
45  /**
46   * The location of a node, as specified by either its path, UUID, and/or identification properties. Hash codes are not implemented
47   * in this base class to allow immutable subclasses to calculate and cache the hash code during object construction.
48   */
49  @Immutable
50  public abstract class Location implements Iterable<Property>, Comparable<Location>, Serializable {
51  
52      private static final long serialVersionUID = 1L;
53  
54      private static final Comparator<Location> COMPARATOR = new Comparator<Location>() {
55          /**
56           * {@inheritDoc}
57           * 
58           * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
59           */
60          public int compare( Location o1,
61                              Location o2 ) {
62              if (o1 == o2) return 0;
63              if (o1 == null) return -1;
64              if (o2 == null) return 1;
65              return o1.compareTo(o2);
66          }
67      };
68  
69      /**
70       * Get a {@link Comparator} that can be used to compare two Location objects. Note that Location implements {@link Comparable}
71       * .
72       * 
73       * @return the comparator; never null
74       */
75      public static final Comparator<Location> comparator() {
76          return COMPARATOR;
77      }
78  
79      /**
80       * Simple shared iterator instance that is used when there are no properties.
81       */
82      protected static final Iterator<Property> NO_ID_PROPERTIES_ITERATOR = new Iterator<Property>() {
83          public boolean hasNext() {
84              return false;
85          }
86  
87          public Property next() {
88              throw new NoSuchElementException();
89          }
90  
91          public void remove() {
92              throw new UnsupportedOperationException();
93          }
94      };
95  
96      /**
97       * Create a location defined by a path.
98       * 
99       * @param path the path
100      * @return a new <code>Location</code> with the given path and no identification properties
101      * @throws IllegalArgumentException if <code>path</code> is null
102      */
103     public static Location create( Path path ) {
104         CheckArg.isNotNull(path, "path");
105         UUID id = identifierFor(path);
106         if (id != null) return new LocationWithUuid(id);
107         return new LocationWithPath(path);
108     }
109 
110     protected static UUID identifierFor( Path identifierPath ) {
111         if (!identifierPath.isIdentifier()) return null;
112         // Get the identifier segment ...
113         Segment segment = identifierPath.getLastSegment();
114         assert segment.isIdentifier();
115         String id = segment.getName().getLocalName();
116         try {
117             // The local part of the segment's name should be the identifier, though it may not be a UUID ...
118             return UUID.fromString(id);
119         } catch (IllegalArgumentException err) {
120             String pathStr = "[" + id + "]";
121             throw new IllegalArgumentException(GraphI18n.identifierPathContainedUnsupportedIdentifierFormat.text(pathStr));
122         }
123     }
124 
125     /**
126      * Create a location defined by a UUID.
127      * 
128      * @param uuid the UUID
129      * @return a new <code>Location</code> with no path and a single identification property with the name
130      *         {@link ModeShapeLexicon#UUID} and the given <code>uuid</code> for a value.
131      * @throws IllegalArgumentException if <code>uuid</code> is null
132      */
133     public static Location create( UUID uuid ) {
134         CheckArg.isNotNull(uuid, "uuid");
135         return new LocationWithUuid(uuid);
136     }
137 
138     /**
139      * Create a location defined by a path and an UUID.
140      * 
141      * @param path the path
142      * @param uuid the UUID, or null if there is no UUID
143      * @return a new <code>Location</code> with the given path (if any) and a single identification property with the name
144      *         {@link ModeShapeLexicon#UUID} and the given <code>uuid</code> (if it is present) for a value.
145      * @throws IllegalArgumentException if <code>path</code> is null
146      */
147     public static Location create( Path path,
148                                    UUID uuid ) {
149         if (path == null) return create(uuid);
150         if (uuid == null) return create(path);
151         UUID id = identifierFor(path);
152         if (id != null) {
153             if (!id.equals(uuid)) {
154                 String pathStr = "[" + id + "]";
155                 throw new IllegalArgumentException(GraphI18n.identifierPathDoesNotMatchSuppliedUuid.text(pathStr, uuid));
156             }
157             return new LocationWithUuid(id);
158         }
159         return new LocationWithPathAndUuid(path, uuid);
160     }
161 
162     /**
163      * Create a location defined by a path and a single identification property.
164      * 
165      * @param path the path
166      * @param idProperty the identification property
167      * @return a new <code>Location</code> with the given path and identification property (if it is present).
168      * @throws IllegalArgumentException if <code>path</code> or <code>idProperty</code> is null
169      */
170     public static Location create( Path path,
171                                    Property idProperty ) {
172         CheckArg.isNotNull(path, "path");
173         CheckArg.isNotNull(idProperty, "idProperty");
174         if (ModeShapeLexicon.UUID.equals(idProperty.getName()) && idProperty.isSingle()) {
175             Object uuid = idProperty.getFirstValue();
176             assert uuid instanceof UUID;
177             UUID id = identifierFor(path);
178             if (id != null) {
179                 if (!id.equals(uuid)) {
180                     String pathStr = "[" + id + "]";
181                     throw new IllegalArgumentException(GraphI18n.identifierPathDoesNotMatchSuppliedUuid.text(pathStr, uuid));
182                 }
183                 return new LocationWithUuid(id);
184             }
185             return new LocationWithPathAndUuid(path, (UUID)uuid);
186         }
187         return new LocationWithPathAndProperty(path, idProperty);
188     }
189 
190     /**
191      * Create a location defined by a path and multiple identification properties.
192      * 
193      * @param path the path
194      * @param firstIdProperty the first identification property
195      * @param remainingIdProperties the remaining identification property
196      * @return a new <code>Location</code> with the given path and identification properties.
197      * @throws IllegalArgumentException if any of the arguments are null
198      */
199     public static Location create( Path path,
200                                    Property firstIdProperty,
201                                    Property... remainingIdProperties ) {
202         CheckArg.isNotNull(path, "path");
203         CheckArg.isNotNull(firstIdProperty, "firstIdProperty");
204         CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties");
205         List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length);
206         Set<Name> names = new HashSet<Name>();
207         names.add(firstIdProperty.getName());
208         idProperties.add(firstIdProperty);
209         for (Property property : remainingIdProperties) {
210             if (property == null) continue;
211             if (names.add(property.getName())) idProperties.add(property);
212         }
213         if (idProperties.isEmpty()) return new LocationWithPath(path);
214         if (idProperties.size() == 1) {
215             Property property = idProperties.get(0);
216             if (property.isSingle()
217                 && (property.getName().equals(JcrLexicon.UUID) || property.getName().equals(ModeShapeLexicon.UUID))) {
218                 Object value = property.getFirstValue();
219                 if (value instanceof UUID) {
220                     return new LocationWithPathAndUuid(path, (UUID)value);
221                 }
222             }
223             return new LocationWithPathAndProperty(path, idProperties.get(0));
224         }
225         return new LocationWithPathAndProperties(path, idProperties);
226     }
227 
228     /**
229      * Create a location defined by a path and an iterator over identification properties.
230      * 
231      * @param path the path
232      * @param idProperties the iterator over the identification properties
233      * @return a new <code>Location</code> with the given path and identification properties
234      * @throws IllegalArgumentException if any of the arguments are null
235      */
236     public static Location create( Path path,
237                                    Iterable<Property> idProperties ) {
238         CheckArg.isNotNull(path, "path");
239         CheckArg.isNotNull(idProperties, "idProperties");
240         List<Property> idPropertiesList = new ArrayList<Property>();
241         Set<Name> names = new HashSet<Name>();
242         for (Property property : idProperties) {
243             if (property == null) continue;
244             if (names.add(property.getName())) idPropertiesList.add(property);
245         }
246         if (idPropertiesList.isEmpty()) return new LocationWithPath(path);
247         if (idPropertiesList.size() == 1) {
248             Property property = idPropertiesList.get(0);
249             if (property.isSingle()
250                 && (property.getName().equals(JcrLexicon.UUID) || property.getName().equals(ModeShapeLexicon.UUID))) {
251                 Object value = property.getFirstValue();
252                 if (value instanceof UUID) {
253                     return new LocationWithPathAndUuid(path, (UUID)value);
254                 }
255             }
256             return new LocationWithPathAndProperty(path, idPropertiesList.get(0));
257         }
258         return new LocationWithPathAndProperties(path, idPropertiesList);
259     }
260 
261     /**
262      * Create a location defined by a single identification property.
263      * 
264      * @param idProperty the identification property
265      * @return a new <code>Location</code> with no path and the given identification property.
266      * @throws IllegalArgumentException if <code>idProperty</code> is null
267      */
268     public static Location create( Property idProperty ) {
269         CheckArg.isNotNull(idProperty, "idProperty");
270         if (ModeShapeLexicon.UUID.equals(idProperty.getName()) && idProperty.isSingle()) {
271             Object uuid = idProperty.getFirstValue();
272             assert uuid instanceof UUID;
273             return new LocationWithUuid((UUID)uuid);
274         }
275         return new LocationWithProperty(idProperty);
276     }
277 
278     /**
279      * Create a location defined by multiple identification properties.
280      * 
281      * @param firstIdProperty the first identification property
282      * @param remainingIdProperties the remaining identification property
283      * @return a new <code>Location</code> with no path and the given and identification properties.
284      * @throws IllegalArgumentException if any of the arguments are null
285      */
286     public static Location create( Property firstIdProperty,
287                                    Property... remainingIdProperties ) {
288         CheckArg.isNotNull(firstIdProperty, "firstIdProperty");
289         CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties");
290         if (remainingIdProperties.length == 0) return create(firstIdProperty);
291         List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length);
292         Set<Name> names = new HashSet<Name>();
293         names.add(firstIdProperty.getName());
294         idProperties.add(firstIdProperty);
295         for (Property property : remainingIdProperties) {
296             if (names.add(property.getName())) idProperties.add(property);
297         }
298         return new LocationWithProperties(idProperties);
299     }
300 
301     /**
302      * Create a location defined by a path and an iterator over identification properties.
303      * 
304      * @param idProperties the iterator over the identification properties
305      * @return a new <code>Location</code> with no path and the given identification properties.
306      * @throws IllegalArgumentException if any of the arguments are null
307      */
308     public static Location create( Iterable<Property> idProperties ) {
309         CheckArg.isNotNull(idProperties, "idProperties");
310         List<Property> idPropertiesList = new ArrayList<Property>();
311         Set<Name> names = new HashSet<Name>();
312         for (Property property : idProperties) {
313             if (names.add(property.getName())) idPropertiesList.add(property);
314         }
315         return create(idPropertiesList);
316     }
317 
318     /**
319      * Create a location defined by multiple identification properties. This method does not check whether the identification
320      * properties are duplicated.
321      * 
322      * @param idProperties the identification properties
323      * @return a new <code>Location</code> with no path and the given identification properties.
324      * @throws IllegalArgumentException if <code>idProperties</code> is null or empty
325      */
326     public static Location create( List<Property> idProperties ) {
327         CheckArg.isNotEmpty(idProperties, "idProperties");
328         return new LocationWithProperties(idProperties);
329     }
330 
331     /**
332      * Get the path that (at least in part) defines this location.
333      * 
334      * @return the path, or null if this location is not defined with a path
335      */
336     public abstract Path getPath();
337 
338     /**
339      * Return whether this location is defined (at least in part) by a path.
340      * 
341      * @return true if a {@link #getPath() path} helps define this location
342      */
343     public boolean hasPath() {
344         return getPath() != null;
345     }
346 
347     /**
348      * Get the identification properties that (at least in part) define this location.
349      * 
350      * @return the identification properties, or null if this location is not defined with identification properties
351      */
352     public abstract List<Property> getIdProperties();
353 
354     /**
355      * Return whether this location is defined (at least in part) with identification properties.
356      * 
357      * @return true if a {@link #getIdProperties() identification properties} help define this location
358      */
359     public boolean hasIdProperties() {
360         return getIdProperties() != null && getIdProperties().size() != 0;
361     }
362 
363     /**
364      * Get the identification property with the supplied name, if there is such a property.
365      * 
366      * @param name the name of the identification property
367      * @return the identification property with the supplied name, or null if there is no such property (or if there
368      *         {@link #hasIdProperties() are no identification properties}
369      */
370     public Property getIdProperty( Name name ) {
371         CheckArg.isNotNull(name, "name");
372         if (getIdProperties() != null) {
373             for (Property property : getIdProperties()) {
374                 if (property.getName().equals(name)) return property;
375             }
376         }
377         return null;
378     }
379 
380     /**
381      * Get the first UUID that is in one of the {@link #getIdProperties() identification properties}.
382      * 
383      * @return the UUID for this location, or null if there is no such identification property
384      */
385     public UUID getUuid() {
386         Property property = getIdProperty(ModeShapeLexicon.UUID);
387         if (property != null && !property.isEmpty()) {
388             Object value = property.getFirstValue();
389             if (value instanceof UUID) return (UUID)value;
390         }
391         return null;
392     }
393 
394     /**
395      * Determine whether this location has the same {@link #getPath() path} and {@link #getIdProperties() identification
396      * properties}: if one location has a path, then both must have the same path; likewise, if one location has ID properties,
397      * then both must have the same ID properties.
398      * <p>
399      * This is different than the behavior of {@link #equals(Object)}, which attempts to determine whether two locations are
400      * <i>equivalent</i>. Two location objects are equivalent if they share the same path and/or ID properties: if both locations
401      * have a path, they must have the same path; if both locations have ID properties, these properties must match.
402      * </p>
403      * 
404      * @param that the other location to be compared
405      * @return true if they are the same, or false otherwise (or if the supplied location is null)
406      * @see #equals(Object)
407      */
408     public boolean isSame( Location that ) {
409         if (that == null) return false;
410         if (this.hasPath()) {
411             if (!this.getPath().equals(that.getPath())) return false;
412         } else if (that.hasPath()) {
413             // this has no path, but that does
414             return false;
415         }
416         if (this.hasIdProperties()) {
417             if (that.hasIdProperties()) return this.getIdProperties().equals(that.getIdProperties());
418             return false;
419         }
420         return (!that.hasIdProperties());
421     }
422 
423     /**
424      * {@inheritDoc}
425      * 
426      * @see java.lang.Iterable#iterator()
427      */
428     public Iterator<Property> iterator() {
429         return getIdProperties() != null ? getIdProperties().iterator() : NO_ID_PROPERTIES_ITERATOR;
430     }
431 
432     /**
433      * {@inheritDoc}
434      * 
435      * @see java.lang.Object#hashCode()
436      */
437     @Override
438     public int hashCode() {
439         return HashCode.compute(getPath(), getIdProperties());
440     }
441 
442     /**
443      * {@inheritDoc}
444      * <p>
445      * Two location objects are equal (or equivalent) if they share the same path and/or ID properties: if both locations have a
446      * path, they must have the same path; if both locations have ID properties, these properties must match.
447      * </p>
448      * <p>
449      * To determine whether two location objects represent the same location, use {@link #isSame(Location)}: if one location has a
450      * path, then both must have the same path; likewise, if one location has ID properties, then both must have the same ID
451      * properties.
452      * </p>
453      * 
454      * @see java.lang.Object#equals(java.lang.Object)
455      * @see #isSame(Location)
456      */
457     @Override
458     public boolean equals( Object obj ) {
459         return equals(obj, true);
460     }
461 
462     /**
463      * Compare this location to the supplied location, and determine whether the two locations represent the same logical
464      * location. One location is considered the same as another location when one location is a superset of the other. For
465      * example, consider the following locations:
466      * <ul>
467      * <li>location A is defined with a "<code>/x/y</code>" path</li>
468      * <li>location B is defined with an identification property {id=3}</li>
469      * <li>location C is defined with a "<code>/x/y/z</code>"</li>
470      * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li>
471      * </ul>
472      * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other
473      * combinations would be considered the same.
474      * <p>
475      * Note that passing a null location as a parameter will always return false.
476      * </p>
477      * 
478      * @param obj the other location to compare
479      * @param requireSameNameSiblingIndexes true if the paths must have equivalent {@link Path.Segment#getIndex()
480      *        same-name-sibling indexes}, or false if the same-name-siblings may be different
481      * @return true if the two locations represent the same location, or false otherwise
482      */
483     public boolean equals( Object obj,
484                            boolean requireSameNameSiblingIndexes ) {
485         if (obj instanceof Location) {
486             Location that = (Location)obj;
487 
488             // if both have same path they are equal
489             if (requireSameNameSiblingIndexes) {
490                 if (this.hasPath() && that.hasPath()) return (this.getPath().equals(that.getPath()));
491             } else {
492                 Path thisPath = this.getPath();
493                 Path thatPath = that.getPath();
494                 if (thisPath.isRoot()) return thatPath.isRoot();
495                 if (thatPath.isRoot()) return thisPath.isRoot();
496                 // The parents must match ...
497                 if (!thisPath.hasSameAncestor(thatPath)) return false;
498                 // And the names of the last segments must match ...
499                 if (!thisPath.getLastSegment().getName().equals(thatPath.getLastSegment().getName())) return false;
500             }
501 
502             // one or both is/are missing path so check properties instead
503             if (this.hasIdProperties()) return (this.getIdProperties().equals(that.getIdProperties()));
504         }
505 
506         return false;
507     }
508 
509     /**
510      * {@inheritDoc}
511      * 
512      * @see java.lang.Comparable#compareTo(java.lang.Object)
513      */
514     public int compareTo( Location that ) {
515         if (this == that) return 0;
516         if (this.hasPath() && that.hasPath()) {
517             return this.getPath().compareTo(that.getPath());
518         }
519         UUID thisUuid = this.getUuid();
520         UUID thatUuid = that.getUuid();
521         if (thisUuid != null && thatUuid != null) {
522             return thisUuid.compareTo(thatUuid);
523         }
524         return this.hashCode() - that.hashCode();
525     }
526 
527     /**
528      * Get the string form of the location.
529      * 
530      * @return the string
531      * @see #getString(TextEncoder)
532      * @see #getString(NamespaceRegistry)
533      * @see #getString(NamespaceRegistry, TextEncoder)
534      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
535      */
536     public String getString() {
537         return getString(null, null, null);
538     }
539 
540     /**
541      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
542      * and properties.
543      * 
544      * @param encoder the encoder to use, or null if the default encoder should be used
545      * @return the encoded string
546      * @see #getString()
547      * @see #getString(NamespaceRegistry)
548      * @see #getString(NamespaceRegistry, TextEncoder)
549      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
550      */
551     public String getString( TextEncoder encoder ) {
552         return getString(null, encoder, null);
553     }
554 
555     /**
556      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
557      * and properties.
558      * 
559      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
560      *        no namespace registry should be used
561      * @return the encoded string
562      * @see #getString()
563      * @see #getString(TextEncoder)
564      * @see #getString(NamespaceRegistry, TextEncoder)
565      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
566      */
567     public String getString( NamespaceRegistry namespaceRegistry ) {
568         return getString(namespaceRegistry, null, null);
569     }
570 
571     /**
572      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
573      * and properties.
574      * 
575      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
576      *        no namespace registry should be used
577      * @param encoder the encoder to use, or null if the default encoder should be used
578      * @return the encoded string
579      * @see #getString()
580      * @see #getString(TextEncoder)
581      * @see #getString(NamespaceRegistry)
582      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
583      */
584     public String getString( NamespaceRegistry namespaceRegistry,
585                              TextEncoder encoder ) {
586         return getString(namespaceRegistry, encoder, null);
587     }
588 
589     /**
590      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
591      * and properties.
592      * 
593      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
594      *        no namespace registry should be used
595      * @param encoder the encoder to use, or null if the default encoder should be used
596      * @param delimiterEncoder the encoder to use for encoding the delimiters in paths, names, and properties, or null if the
597      *        standard delimiters should be used
598      * @return the encoded string
599      * @see #getString()
600      * @see #getString(TextEncoder)
601      * @see #getString(NamespaceRegistry)
602      * @see #getString(NamespaceRegistry, TextEncoder)
603      */
604     public String getString( NamespaceRegistry namespaceRegistry,
605                              TextEncoder encoder,
606                              TextEncoder delimiterEncoder ) {
607         StringBuilder sb = new StringBuilder();
608         sb.append("{ ");
609         boolean hasPath = this.hasPath();
610         if (hasPath) {
611             sb.append(this.getPath().getString(namespaceRegistry, encoder, delimiterEncoder));
612         }
613         if (this.hasIdProperties()) {
614             if (hasPath) sb.append(" && ");
615             sb.append("[");
616             boolean first = true;
617             for (Property idProperty : this.getIdProperties()) {
618                 if (first) first = false;
619                 else sb.append(", ");
620                 sb.append(idProperty.getString(namespaceRegistry, encoder, delimiterEncoder));
621             }
622             sb.append("]");
623         }
624         sb.append(" }");
625         return sb.toString();
626     }
627 
628     /**
629      * {@inheritDoc}
630      * 
631      * @see java.lang.Object#toString()
632      */
633     @Override
634     public String toString() {
635         StringBuilder sb = new StringBuilder();
636         boolean hasPath = this.hasPath();
637         boolean hasProps = this.hasIdProperties();
638         if (hasPath) {
639             if (hasProps) {
640                 sb.append("<");
641             }
642             sb.append(this.getPath());
643         }
644         if (hasProps) {
645             if (hasPath) sb.append(" && ");
646             sb.append("[");
647             boolean first = true;
648             for (Property idProperty : this.getIdProperties()) {
649                 if (first) first = false;
650                 else sb.append(", ");
651                 sb.append(idProperty);
652             }
653             sb.append("]");
654             if (hasPath) {
655                 sb.append(">");
656             }
657         }
658         return sb.toString();
659     }
660 
661     /**
662      * Create a copy of this location that adds the supplied identification property. The new identification property will replace
663      * any existing identification property with the same name on the original.
664      * 
665      * @param newIdProperty the new identification property, which may be null
666      * @return the new location, or this location if the new identification property is null or empty
667      */
668     public abstract Location with( Property newIdProperty );
669 
670     /**
671      * Create a copy of this location that uses the supplied path.
672      * 
673      * @param newPath the new path for the location
674      * @return the new location, or this location if the path is equal to this location's path
675      */
676     public abstract Location with( Path newPath );
677 
678     /**
679      * Create a copy of this location that adds the supplied UUID as an identification property. The new identification property
680      * will replace any existing identification property with the same name on the original.
681      * 
682      * @param uuid the new UUID, which may be null
683      * @return the new location, or this location if the new identification property is null or empty
684      */
685     public abstract Location with( UUID uuid );
686 
687 }