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 (names.add(property.getName())) idProperties.add(property);
211         }
212         return new LocationWithPathAndProperties(path, idProperties);
213     }
214 
215     /**
216      * Create a location defined by a path and an iterator over identification properties.
217      * 
218      * @param path the path
219      * @param idProperties the iterator over the identification properties
220      * @return a new <code>Location</code> with the given path and identification properties
221      * @throws IllegalArgumentException if any of the arguments are null
222      */
223     public static Location create( Path path,
224                                    Iterable<Property> idProperties ) {
225         CheckArg.isNotNull(path, "path");
226         CheckArg.isNotNull(idProperties, "idProperties");
227         List<Property> idPropertiesList = new ArrayList<Property>();
228         Set<Name> names = new HashSet<Name>();
229         for (Property property : idProperties) {
230             if (names.add(property.getName())) idPropertiesList.add(property);
231         }
232         if (idPropertiesList.isEmpty()) return new LocationWithPath(path);
233         return new LocationWithPathAndProperties(path, idPropertiesList);
234     }
235 
236     /**
237      * Create a location defined by a single identification property.
238      * 
239      * @param idProperty the identification property
240      * @return a new <code>Location</code> with no path and the given identification property.
241      * @throws IllegalArgumentException if <code>idProperty</code> is null
242      */
243     public static Location create( Property idProperty ) {
244         CheckArg.isNotNull(idProperty, "idProperty");
245         if (ModeShapeLexicon.UUID.equals(idProperty.getName()) && idProperty.isSingle()) {
246             Object uuid = idProperty.getFirstValue();
247             assert uuid instanceof UUID;
248             return new LocationWithUuid((UUID)uuid);
249         }
250         return new LocationWithProperty(idProperty);
251     }
252 
253     /**
254      * Create a location defined by multiple identification properties.
255      * 
256      * @param firstIdProperty the first identification property
257      * @param remainingIdProperties the remaining identification property
258      * @return a new <code>Location</code> with no path and the given and identification properties.
259      * @throws IllegalArgumentException if any of the arguments are null
260      */
261     public static Location create( Property firstIdProperty,
262                                    Property... remainingIdProperties ) {
263         CheckArg.isNotNull(firstIdProperty, "firstIdProperty");
264         CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties");
265         if (remainingIdProperties.length == 0) return create(firstIdProperty);
266         List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length);
267         Set<Name> names = new HashSet<Name>();
268         names.add(firstIdProperty.getName());
269         idProperties.add(firstIdProperty);
270         for (Property property : remainingIdProperties) {
271             if (names.add(property.getName())) idProperties.add(property);
272         }
273         return new LocationWithProperties(idProperties);
274     }
275 
276     /**
277      * Create a location defined by a path and an iterator over identification properties.
278      * 
279      * @param idProperties the iterator over the identification properties
280      * @return a new <code>Location</code> with no path and the given identification properties.
281      * @throws IllegalArgumentException if any of the arguments are null
282      */
283     public static Location create( Iterable<Property> idProperties ) {
284         CheckArg.isNotNull(idProperties, "idProperties");
285         List<Property> idPropertiesList = new ArrayList<Property>();
286         Set<Name> names = new HashSet<Name>();
287         for (Property property : idProperties) {
288             if (names.add(property.getName())) idPropertiesList.add(property);
289         }
290         return create(idPropertiesList);
291     }
292 
293     /**
294      * Create a location defined by multiple identification properties. This method does not check whether the identification
295      * properties are duplicated.
296      * 
297      * @param idProperties the identification properties
298      * @return a new <code>Location</code> with no path and the given identification properties.
299      * @throws IllegalArgumentException if <code>idProperties</code> is null or empty
300      */
301     public static Location create( List<Property> idProperties ) {
302         CheckArg.isNotEmpty(idProperties, "idProperties");
303         return new LocationWithProperties(idProperties);
304     }
305 
306     /**
307      * Get the path that (at least in part) defines this location.
308      * 
309      * @return the path, or null if this location is not defined with a path
310      */
311     public abstract Path getPath();
312 
313     /**
314      * Return whether this location is defined (at least in part) by a path.
315      * 
316      * @return true if a {@link #getPath() path} helps define this location
317      */
318     public boolean hasPath() {
319         return getPath() != null;
320     }
321 
322     /**
323      * Get the identification properties that (at least in part) define this location.
324      * 
325      * @return the identification properties, or null if this location is not defined with identification properties
326      */
327     public abstract List<Property> getIdProperties();
328 
329     /**
330      * Return whether this location is defined (at least in part) with identification properties.
331      * 
332      * @return true if a {@link #getIdProperties() identification properties} help define this location
333      */
334     public boolean hasIdProperties() {
335         return getIdProperties() != null && getIdProperties().size() != 0;
336     }
337 
338     /**
339      * Get the identification property with the supplied name, if there is such a property.
340      * 
341      * @param name the name of the identification property
342      * @return the identification property with the supplied name, or null if there is no such property (or if there
343      *         {@link #hasIdProperties() are no identification properties}
344      */
345     public Property getIdProperty( Name name ) {
346         CheckArg.isNotNull(name, "name");
347         if (getIdProperties() != null) {
348             for (Property property : getIdProperties()) {
349                 if (property.getName().equals(name)) return property;
350             }
351         }
352         return null;
353     }
354 
355     /**
356      * Get the first UUID that is in one of the {@link #getIdProperties() identification properties}.
357      * 
358      * @return the UUID for this location, or null if there is no such identification property
359      */
360     public UUID getUuid() {
361         Property property = getIdProperty(ModeShapeLexicon.UUID);
362         if (property != null && !property.isEmpty()) {
363             Object value = property.getFirstValue();
364             if (value instanceof UUID) return (UUID)value;
365         }
366         return null;
367     }
368 
369     /**
370      * Determine whether this location has the same {@link #getPath() path} and {@link #getIdProperties() identification
371      * properties}: if one location has a path, then both must have the same path; likewise, if one location has ID properties,
372      * then both must have the same ID properties.
373      * <p>
374      * This is different than the behavior of {@link #equals(Object)}, which attempts to determine whether two locations are
375      * <i>equivalent</i>. Two location objects are equivalent if they share the same path and/or ID properties: if both locations
376      * have a path, they must have the same path; if both locations have ID properties, these properties must match.
377      * </p>
378      * 
379      * @param that the other location to be compared
380      * @return true if they are the same, or false otherwise (or if the supplied location is null)
381      * @see #equals(Object)
382      */
383     public boolean isSame( Location that ) {
384         if (that == null) return false;
385         if (this.hasPath()) {
386             if (!this.getPath().equals(that.getPath())) return false;
387         } else if (that.hasPath()) {
388             // this has no path, but that does
389             return false;
390         }
391         if (this.hasIdProperties()) {
392             if (that.hasIdProperties()) return this.getIdProperties().equals(that.getIdProperties());
393             return false;
394         }
395         return (!that.hasIdProperties());
396     }
397 
398     /**
399      * {@inheritDoc}
400      * 
401      * @see java.lang.Iterable#iterator()
402      */
403     public Iterator<Property> iterator() {
404         return getIdProperties() != null ? getIdProperties().iterator() : NO_ID_PROPERTIES_ITERATOR;
405     }
406 
407     /**
408      * {@inheritDoc}
409      * 
410      * @see java.lang.Object#hashCode()
411      */
412     @Override
413     public int hashCode() {
414         return HashCode.compute(getPath(), getIdProperties());
415     }
416 
417     /**
418      * {@inheritDoc}
419      * <p>
420      * Two location objects are equal (or equivalent) if they share the same path and/or ID properties: if both locations have a
421      * path, they must have the same path; if both locations have ID properties, these properties must match.
422      * </p>
423      * <p>
424      * To determine whether two location objects represent the same location, use {@link #isSame(Location)}: if one location has a
425      * path, then both must have the same path; likewise, if one location has ID properties, then both must have the same ID
426      * properties.
427      * </p>
428      * 
429      * @see java.lang.Object#equals(java.lang.Object)
430      * @see #isSame(Location)
431      */
432     @Override
433     public boolean equals( Object obj ) {
434         return equals(obj, true);
435     }
436 
437     /**
438      * Compare this location to the supplied location, and determine whether the two locations represent the same logical
439      * location. One location is considered the same as another location when one location is a superset of the other. For
440      * example, consider the following locations:
441      * <ul>
442      * <li>location A is defined with a "<code>/x/y</code>" path</li>
443      * <li>location B is defined with an identification property {id=3}</li>
444      * <li>location C is defined with a "<code>/x/y/z</code>"</li>
445      * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li>
446      * </ul>
447      * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other
448      * combinations would be considered the same.
449      * <p>
450      * Note that passing a null location as a parameter will always return false.
451      * </p>
452      * 
453      * @param obj the other location to compare
454      * @param requireSameNameSiblingIndexes true if the paths must have equivalent {@link Path.Segment#getIndex()
455      *        same-name-sibling indexes}, or false if the same-name-siblings may be different
456      * @return true if the two locations represent the same location, or false otherwise
457      */
458     public boolean equals( Object obj,
459                            boolean requireSameNameSiblingIndexes ) {
460         // if (obj instanceof Location) {
461         // Location that = (Location)obj;
462         // if (this.hasPath()) {
463         // if (!this.getPath().equals(that.getPath())) return false;
464         // } else {
465         // if (that.hasPath()) return false;
466         // }
467         // if (this.hasIdProperties()) {
468         // if (!this.getIdProperties().equals(that.getIdProperties())) return
469         // false;
470         // } else {
471         // if (that.hasIdProperties()) return false;
472         // }
473         // return true;
474         // }
475         // return false;
476         if (obj instanceof Location) {
477             Location that = (Location)obj;
478 
479             // if both have same path they are equal
480             if (requireSameNameSiblingIndexes) {
481                 if (this.hasPath() && that.hasPath()) return (this.getPath().equals(that.getPath()));
482             } else {
483                 Path thisPath = this.getPath();
484                 Path thatPath = that.getPath();
485                 if (thisPath.isRoot()) return thatPath.isRoot();
486                 if (thatPath.isRoot()) return thisPath.isRoot();
487                 // The parents must match ...
488                 if (!thisPath.hasSameAncestor(thatPath)) return false;
489                 // And the names of the last segments must match ...
490                 if (!thisPath.getLastSegment().getName().equals(thatPath.getLastSegment().getName())) return false;
491             }
492 
493             // one or both is/are missing path so check properties instead
494             if (this.hasIdProperties()) return (this.getIdProperties().equals(that.getIdProperties()));
495         }
496 
497         return false;
498     }
499 
500     /**
501      * {@inheritDoc}
502      * 
503      * @see java.lang.Comparable#compareTo(java.lang.Object)
504      */
505     public int compareTo( Location that ) {
506         if (this == that) return 0;
507         if (this.hasPath() && that.hasPath()) {
508             return this.getPath().compareTo(that.getPath());
509         }
510         UUID thisUuid = this.getUuid();
511         UUID thatUuid = that.getUuid();
512         if (thisUuid != null && thatUuid != null) {
513             return thisUuid.compareTo(thatUuid);
514         }
515         return this.hashCode() - that.hashCode();
516     }
517 
518     /**
519      * Get the string form of the location.
520      * 
521      * @return the string
522      * @see #getString(TextEncoder)
523      * @see #getString(NamespaceRegistry)
524      * @see #getString(NamespaceRegistry, TextEncoder)
525      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
526      */
527     public String getString() {
528         return getString(null, null, null);
529     }
530 
531     /**
532      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
533      * and properties.
534      * 
535      * @param encoder the encoder to use, or null if the default encoder should be used
536      * @return the encoded string
537      * @see #getString()
538      * @see #getString(NamespaceRegistry)
539      * @see #getString(NamespaceRegistry, TextEncoder)
540      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
541      */
542     public String getString( TextEncoder encoder ) {
543         return getString(null, encoder, null);
544     }
545 
546     /**
547      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
548      * and properties.
549      * 
550      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
551      *        no namespace registry should be used
552      * @return the encoded string
553      * @see #getString()
554      * @see #getString(TextEncoder)
555      * @see #getString(NamespaceRegistry, TextEncoder)
556      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
557      */
558     public String getString( NamespaceRegistry namespaceRegistry ) {
559         return getString(namespaceRegistry, null, null);
560     }
561 
562     /**
563      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
564      * and properties.
565      * 
566      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
567      *        no namespace registry should be used
568      * @param encoder the encoder to use, or null if the default encoder should be used
569      * @return the encoded string
570      * @see #getString()
571      * @see #getString(TextEncoder)
572      * @see #getString(NamespaceRegistry)
573      * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
574      */
575     public String getString( NamespaceRegistry namespaceRegistry,
576                              TextEncoder encoder ) {
577         return getString(namespaceRegistry, encoder, null);
578     }
579 
580     /**
581      * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
582      * and properties.
583      * 
584      * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
585      *        no namespace registry should be used
586      * @param encoder the encoder to use, or null if the default encoder should be used
587      * @param delimiterEncoder the encoder to use for encoding the delimiters in paths, names, and properties, or null if the
588      *        standard delimiters should be used
589      * @return the encoded string
590      * @see #getString()
591      * @see #getString(TextEncoder)
592      * @see #getString(NamespaceRegistry)
593      * @see #getString(NamespaceRegistry, TextEncoder)
594      */
595     public String getString( NamespaceRegistry namespaceRegistry,
596                              TextEncoder encoder,
597                              TextEncoder delimiterEncoder ) {
598         StringBuilder sb = new StringBuilder();
599         sb.append("{ ");
600         boolean hasPath = this.hasPath();
601         if (hasPath) {
602             sb.append(this.getPath().getString(namespaceRegistry, encoder, delimiterEncoder));
603         }
604         if (this.hasIdProperties()) {
605             if (hasPath) sb.append(" && ");
606             sb.append("[");
607             boolean first = true;
608             for (Property idProperty : this.getIdProperties()) {
609                 if (first) first = false;
610                 else sb.append(", ");
611                 sb.append(idProperty.getString(namespaceRegistry, encoder, delimiterEncoder));
612             }
613             sb.append("]");
614         }
615         sb.append(" }");
616         return sb.toString();
617     }
618 
619     /**
620      * {@inheritDoc}
621      * 
622      * @see java.lang.Object#toString()
623      */
624     @Override
625     public String toString() {
626         StringBuilder sb = new StringBuilder();
627         boolean hasPath = this.hasPath();
628         boolean hasProps = this.hasIdProperties();
629         if (hasPath) {
630             if (hasProps) {
631                 sb.append("<");
632             }
633             sb.append(this.getPath());
634         }
635         if (hasProps) {
636             if (hasPath) sb.append(" && ");
637             sb.append("[");
638             boolean first = true;
639             for (Property idProperty : this.getIdProperties()) {
640                 if (first) first = false;
641                 else sb.append(", ");
642                 sb.append(idProperty);
643             }
644             sb.append("]");
645             if (hasPath) {
646                 sb.append(">");
647             }
648         }
649         return sb.toString();
650     }
651 
652     /**
653      * Create a copy of this location that adds the supplied identification property. The new identification property will replace
654      * any existing identification property with the same name on the original.
655      * 
656      * @param newIdProperty the new identification property, which may be null
657      * @return the new location, or this location if the new identification property is null or empty
658      */
659     public abstract Location with( Property newIdProperty );
660 
661     /**
662      * Create a copy of this location that uses the supplied path.
663      * 
664      * @param newPath the new path for the location
665      * @return the new location, or this location if the path is equal to this location's path
666      */
667     public abstract Location with( Path newPath );
668 
669     /**
670      * Create a copy of this location that adds the supplied UUID as an identification property. The new identification property
671      * will replace any existing identification property with the same name on the original.
672      * 
673      * @param uuid the new UUID, which may be null
674      * @return the new location, or this location if the new identification property is null or empty
675      */
676     public abstract Location with( UUID uuid );
677 
678 }