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> {
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 LocationWithPathAndProperties(null, 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         * Get the string form of the location.
436         * 
437         * @return the string
438         * @see #getString(TextEncoder)
439         * @see #getString(NamespaceRegistry)
440         * @see #getString(NamespaceRegistry, TextEncoder)
441         * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
442         */
443        public String getString() {
444            return getString(null, null, null);
445        }
446    
447        /**
448         * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
449         * and properties.
450         * 
451         * @param encoder the encoder to use, or null if the default encoder should be used
452         * @return the encoded string
453         * @see #getString()
454         * @see #getString(NamespaceRegistry)
455         * @see #getString(NamespaceRegistry, TextEncoder)
456         * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
457         */
458        public String getString( TextEncoder encoder ) {
459            return getString(null, encoder, null);
460        }
461    
462        /**
463         * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
464         * and properties.
465         * 
466         * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
467         *        no namespace registry should be used
468         * @return the encoded string
469         * @see #getString()
470         * @see #getString(TextEncoder)
471         * @see #getString(NamespaceRegistry, TextEncoder)
472         * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
473         */
474        public String getString( NamespaceRegistry namespaceRegistry ) {
475            CheckArg.isNotNull(namespaceRegistry, "namespaceRegistry");
476            return getString(namespaceRegistry, null, null);
477        }
478    
479        /**
480         * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
481         * and properties.
482         * 
483         * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
484         *        no namespace registry should be used
485         * @param encoder the encoder to use, or null if the default encoder should be used
486         * @return the encoded string
487         * @see #getString()
488         * @see #getString(TextEncoder)
489         * @see #getString(NamespaceRegistry)
490         * @see #getString(NamespaceRegistry, TextEncoder, TextEncoder)
491         */
492        public String getString( NamespaceRegistry namespaceRegistry,
493                                 TextEncoder encoder ) {
494            CheckArg.isNotNull(namespaceRegistry, "namespaceRegistry");
495            return getString(namespaceRegistry, encoder, null);
496        }
497    
498        /**
499         * Get the encoded string form of the location, using the supplied encoder to encode characters in each of the location's path
500         * and properties.
501         * 
502         * @param namespaceRegistry the namespace registry to use for getting the string form of the path and properties, or null if
503         *        no namespace registry should be used
504         * @param encoder the encoder to use, or null if the default encoder should be used
505         * @param delimiterEncoder the encoder to use for encoding the delimiters in paths, names, and properties, or null if the
506         *        standard delimiters should be used
507         * @return the encoded string
508         * @see #getString()
509         * @see #getString(TextEncoder)
510         * @see #getString(NamespaceRegistry)
511         * @see #getString(NamespaceRegistry, TextEncoder)
512         */
513        public String getString( NamespaceRegistry namespaceRegistry,
514                                 TextEncoder encoder,
515                                 TextEncoder delimiterEncoder ) {
516            StringBuilder sb = new StringBuilder();
517            sb.append("{ ");
518            boolean hasPath = this.hasPath();
519            if (hasPath) {
520                sb.append(this.getPath().getString(namespaceRegistry, encoder, delimiterEncoder));
521            }
522            if (this.hasIdProperties()) {
523                if (hasPath) sb.append(" && ");
524                sb.append("[");
525                boolean first = true;
526                for (Property idProperty : this.getIdProperties()) {
527                    if (first) first = false;
528                    else sb.append(", ");
529                    sb.append(idProperty.getString(namespaceRegistry, encoder, delimiterEncoder));
530                }
531                sb.append("]");
532            }
533            sb.append(" }");
534            return sb.toString();
535        }
536    
537        /**
538         * {@inheritDoc}
539         * 
540         * @see java.lang.Object#toString()
541         */
542        @Override
543        public String toString() {
544            StringBuilder sb = new StringBuilder();
545            sb.append("{ ");
546            boolean hasPath = this.hasPath();
547            if (hasPath) {
548                sb.append(this.getPath());
549            }
550            if (this.hasIdProperties()) {
551                if (hasPath) sb.append(" && ");
552                sb.append("[");
553                boolean first = true;
554                for (Property idProperty : this.getIdProperties()) {
555                    if (first) first = false;
556                    else sb.append(", ");
557                    sb.append(idProperty);
558                }
559                sb.append("]");
560            }
561            sb.append(" }");
562            return sb.toString();
563        }
564    
565        /**
566         * Create a copy of this location that adds the supplied identification property. The new identification property will replace
567         * any existing identification property with the same name on the original.
568         * 
569         * @param newIdProperty the new identification property, which may be null
570         * @return the new location, or this location if the new identification property is null or empty
571         */
572        public abstract Location with( Property newIdProperty );
573    
574        /**
575         * Create a copy of this location that uses the supplied path.
576         * 
577         * @param newPath the new path for the location
578         * @return the new location, or this location if the path is equal to this location's path
579         */
580        public abstract Location with( Path newPath );
581    
582        /**
583         * Create a copy of this location that adds the supplied UUID as an identification property. The new identification property
584         * will replace any existing identification property with the same name on the original.
585         * 
586         * @param uuid the new UUID, which may be null
587         * @return the new location, or this location if the new identification property is null or empty
588         */
589        public abstract Location with( UUID uuid );
590    
591    }