001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.graph;
023    
024    import java.util.ArrayList;
025    import java.util.Collections;
026    import java.util.Iterator;
027    import java.util.List;
028    import java.util.NoSuchElementException;
029    import java.util.UUID;
030    import net.jcip.annotations.Immutable;
031    import org.jboss.dna.common.util.CheckArg;
032    import org.jboss.dna.common.util.HashCode;
033    import org.jboss.dna.graph.properties.Name;
034    import org.jboss.dna.graph.properties.Path;
035    import org.jboss.dna.graph.properties.Property;
036    import org.jboss.dna.graph.properties.basic.BasicSingleValueProperty;
037    
038    /**
039     * The location of a node, as specified by either its path, UUID, and/or identification properties.
040     * 
041     * @author Randall Hauch
042     */
043    @Immutable
044    public class Location implements Iterable<Property> {
045    
046        private static final Iterator<Property> NO_ID_PROPERTIES_ITERATOR = new Iterator<Property>() {
047            public boolean hasNext() {
048                return false;
049            }
050    
051            public Property next() {
052                throw new NoSuchElementException();
053            }
054    
055            public void remove() {
056                throw new UnsupportedOperationException();
057            }
058        };
059    
060        private final Path path;
061        private final List<Property> idProperties;
062    
063        /**
064         * Create a location defined by a path.
065         * 
066         * @param path the path
067         * @throws IllegalArgumentException if <code>path</code> is null
068         */
069        public Location( Path path ) {
070            CheckArg.isNotNull(path, "path");
071            this.path = path;
072            this.idProperties = null;
073        }
074    
075        /**
076         * Create a location defined by a UUID.
077         * 
078         * @param uuid the UUID
079         * @throws IllegalArgumentException if <code>uuid</code> is null
080         */
081        public Location( UUID uuid ) {
082            CheckArg.isNotNull(uuid, "uuid");
083            this.path = null;
084            Property idProperty = new BasicSingleValueProperty(DnaLexicon.UUID, uuid);
085            this.idProperties = Collections.singletonList(idProperty);
086        }
087    
088        /**
089         * Create a location defined by a path and an UUID.
090         * 
091         * @param path the path
092         * @param uuid the UUID, or null if there is no UUID
093         * @throws IllegalArgumentException if <code>path</code> is null
094         */
095        public Location( Path path,
096                         UUID uuid ) {
097            CheckArg.isNotNull(uuid, "uuid");
098            this.path = path;
099            if (uuid != null) {
100                Property idProperty = new BasicSingleValueProperty(DnaLexicon.UUID, uuid);
101                this.idProperties = Collections.singletonList(idProperty);
102            } else {
103                this.idProperties = null;
104            }
105        }
106    
107        /**
108         * Create a location defined by a path and a single identification property.
109         * 
110         * @param path the path
111         * @param idProperty the identification property
112         * @throws IllegalArgumentException if <code>path</code> or <code>idProperty</code> is null
113         */
114        public Location( Path path,
115                         Property idProperty ) {
116            CheckArg.isNotNull(path, "path");
117            CheckArg.isNotNull(idProperty, "idProperty");
118            this.path = path;
119            this.idProperties = idProperty != null ? Collections.singletonList(idProperty) : null;
120        }
121    
122        /**
123         * Create a location defined by a path and multiple identification properties.
124         * 
125         * @param path the path
126         * @param firstIdProperty the first identification property
127         * @param remainingIdProperties the remaining identification property
128         * @throws IllegalArgumentException if any of the arguments are null
129         */
130        public Location( Path path,
131                         Property firstIdProperty,
132                         Property... remainingIdProperties ) {
133            CheckArg.isNotNull(path, "path");
134            CheckArg.isNotNull(firstIdProperty, "firstIdProperty");
135            CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties");
136            this.path = path;
137            List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length);
138            idProperties.add(firstIdProperty);
139            for (Property property : remainingIdProperties) {
140                idProperties.add(property);
141            }
142            this.idProperties = Collections.unmodifiableList(idProperties);
143        }
144    
145        /**
146         * Create a location defined by a path and an iterator over identification properties.
147         * 
148         * @param path the path
149         * @param idProperties the iterator over the identification properties
150         * @throws IllegalArgumentException if any of the arguments are null
151         */
152        public Location( Path path,
153                         Iterable<Property> idProperties ) {
154            CheckArg.isNotNull(path, "path");
155            CheckArg.isNotNull(idProperties, "idProperties");
156            this.path = path;
157            List<Property> idPropertiesList = new ArrayList<Property>();
158            for (Property property : idProperties) {
159                idPropertiesList.add(property);
160            }
161            this.idProperties = Collections.unmodifiableList(idPropertiesList);
162        }
163    
164        /**
165         * Create a location defined by a single identification property.
166         * 
167         * @param idProperty the identification property
168         * @throws IllegalArgumentException if <code>idProperty</code> is null
169         */
170        public Location( Property idProperty ) {
171            CheckArg.isNotNull(idProperty, "idProperty");
172            this.path = null;
173            this.idProperties = Collections.singletonList(idProperty);
174        }
175    
176        /**
177         * Create a location defined by multiple identification properties.
178         * 
179         * @param firstIdProperty the first identification property
180         * @param remainingIdProperties the remaining identification property
181         * @throws IllegalArgumentException if any of the arguments are null
182         */
183        public Location( Property firstIdProperty,
184                         Property... remainingIdProperties ) {
185            CheckArg.isNotNull(firstIdProperty, "firstIdProperty");
186            CheckArg.isNotNull(remainingIdProperties, "remainingIdProperties");
187            this.path = null;
188            List<Property> idProperties = new ArrayList<Property>(1 + remainingIdProperties.length);
189            idProperties.add(firstIdProperty);
190            for (Property property : remainingIdProperties) {
191                idProperties.add(property);
192            }
193            this.idProperties = Collections.unmodifiableList(idProperties);
194        }
195    
196        /**
197         * Create a location defined by a path and an iterator over identification properties.
198         * 
199         * @param idProperties the iterator over the identification properties
200         * @throws IllegalArgumentException if any of the arguments are null
201         */
202        public Location( Iterable<Property> idProperties ) {
203            CheckArg.isNotNull(idProperties, "idProperties");
204            this.path = null;
205            List<Property> idPropertiesList = new ArrayList<Property>();
206            for (Property property : idProperties) {
207                idPropertiesList.add(property);
208            }
209            this.idProperties = Collections.unmodifiableList(idPropertiesList);
210        }
211    
212        /**
213         * Create a location defined by multiple identification properties.
214         * 
215         * @param idProperties the identification properties
216         * @throws IllegalArgumentException if <code>idProperties</code> is null or empty
217         */
218        public Location( List<Property> idProperties ) {
219            CheckArg.isNotEmpty(idProperties, "idProperties");
220            this.path = null;
221            this.idProperties = idProperties;
222        }
223    
224        /**
225         * Create a location defined by a path and multiple identification properties.
226         * 
227         * @param path the path
228         * @param idProperties the identification properties
229         * @throws IllegalArgumentException if <code>path</code> is null, or if <code>idProperties</code> is empty
230         */
231        protected Location( Path path,
232                            List<Property> idProperties ) {
233            CheckArg.isNotNull(path, "path");
234            CheckArg.isNotEmpty(idProperties, "idProperties");
235            this.path = path;
236            this.idProperties = idProperties;
237        }
238    
239        /**
240         * Create a location from another but adding the supplied identification property. The new identification property will
241         * replace any existing identification property with the same name on the original.
242         * 
243         * @param original the original location
244         * @param newIdProperty the new identification property
245         * @throws IllegalArgumentException if <code>original</code> is null
246         */
247        protected Location( Location original,
248                            Property newIdProperty ) {
249            CheckArg.isNotNull(original, "original");
250            this.path = original.getPath();
251            if (original.hasIdProperties()) {
252                List<Property> originalIdProperties = original.getIdProperties();
253                if (newIdProperty == null) {
254                    this.idProperties = original.idProperties;
255                } else {
256                    List<Property> idProperties = new ArrayList<Property>(originalIdProperties.size() + 1);
257                    for (Property property : originalIdProperties) {
258                        if (!newIdProperty.getName().equals(property.getName())) idProperties.add(property);
259                    }
260                    idProperties.add(newIdProperty);
261                    this.idProperties = Collections.unmodifiableList(idProperties);
262                }
263            } else {
264                this.idProperties = Collections.singletonList(newIdProperty);
265            }
266        }
267    
268        /**
269         * Create a location from another but adding the supplied identification property. The new identification property will
270         * replace any existing identification property with the same name on the original.
271         * 
272         * @param original the original location
273         * @param newPath the new path for the location
274         * @throws IllegalArgumentException if <code>original</code> is null
275         */
276        protected Location( Location original,
277                            Path newPath ) {
278            CheckArg.isNotNull(original, "original");
279            this.path = newPath != null ? newPath : original.getPath();
280            this.idProperties = original.idProperties;
281        }
282    
283        /**
284         * Get the path that (at least in part) defines this location.
285         * 
286         * @return the path, or null if this location is not defined with a path
287         */
288        public Path getPath() {
289            return path;
290        }
291    
292        /**
293         * Return whether this location is defined (at least in part) by a path.
294         * 
295         * @return true if a {@link #getPath() path} helps define this location
296         */
297        public boolean hasPath() {
298            return path != null;
299        }
300    
301        /**
302         * Get the identification properties that (at least in part) define this location.
303         * 
304         * @return the identification properties, or null if this location is not defined with identification properties
305         */
306        public List<Property> getIdProperties() {
307            return idProperties;
308        }
309    
310        /**
311         * Return whether this location is defined (at least in part) with identification properties.
312         * 
313         * @return true if a {@link #getIdProperties() identification properties} help define this location
314         */
315        public boolean hasIdProperties() {
316            return idProperties != null && idProperties.size() != 0;
317        }
318    
319        /**
320         * Get the identification property with the supplied name, if there is such a property.
321         * 
322         * @param name the name of the identification property
323         * @return the identification property with the supplied name, or null if there is no such property (or if there
324         *         {@link #hasIdProperties() are no identification properties}
325         */
326        public Property getIdProperty( Name name ) {
327            CheckArg.isNotNull(name, "name");
328            if (idProperties != null) {
329                for (Property property : idProperties) {
330                    if (property.getName().equals(name)) return property;
331                }
332            }
333            return null;
334        }
335    
336        /**
337         * Compare this location to the supplied location, and determine whether the two locations represent the same logical
338         * location. One location is considered the same as another location when one location is a superset of the other. For
339         * example, consider the following locations:
340         * <ul>
341         * <li>location A is defined with a "<code>/x/y</code>" path</li>
342         * <li>location B is defined with an identification property {id=3}</li>
343         * <li>location C is defined with a "<code>/x/y/z</code>"</li>
344         * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li>
345         * </ul>
346         * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other
347         * combinations would be considered the same.
348         * <p>
349         * Note that passing a null location as a parameter will always return false.
350         * </p>
351         * 
352         * @param other the other location to compare
353         * @return true if the two locations represent the same location, or false otherwise
354         */
355        public boolean isSame( Location other ) {
356            return isSame(other, true);
357        }
358    
359        /**
360         * Compare this location to the supplied location, and determine whether the two locations represent the same logical
361         * location. One location is considered the same as another location when one location is a superset of the other. For
362         * example, consider the following locations:
363         * <ul>
364         * <li>location A is defined with a "<code>/x/y</code>" path</li>
365         * <li>location B is defined with an identification property {id=3}</li>
366         * <li>location C is defined with a "<code>/x/y/z</code>"</li>
367         * <li>location D is defined with a "<code>/x/y/z</code>" path and an identification property {id=3}</li>
368         * </ul>
369         * Locations C and D would be considered the same, and B and D would also be considered the same. None of the other
370         * combinations would be considered the same.
371         * <p>
372         * Note that passing a null location as a parameter will always return false.
373         * </p>
374         * 
375         * @param other the other location to compare
376         * @param requireSameNameSiblingIndexes true if the paths must have equivalent {@link Path.Segment#getIndex()
377         *        same-name-sibling indexes}, or false if the same-name-siblings may be different
378         * @return true if the two locations represent the same location, or false otherwise
379         */
380        public boolean isSame( Location other,
381                               boolean requireSameNameSiblingIndexes ) {
382            if (other != null) {
383                if (this.hasPath() && other.hasPath()) {
384                    // Paths on both, so the paths MUST match
385                    if (requireSameNameSiblingIndexes) {
386                        if (!this.getPath().equals(other.getPath())) return false;
387                    } else {
388                        Path thisPath = this.getPath();
389                        Path thatPath = other.getPath();
390                        if (thisPath.isRoot() && thatPath.isRoot()) return true;
391                        // The parents must match ...
392                        if (!thisPath.hasSameAncestor(thatPath)) return false;
393                        // And the names of the last segments must match ...
394                        if (!thisPath.getLastSegment().getName().equals(thatPath.getLastSegment().getName())) return false;
395                    }
396    
397                    // And the identification properties must match only if they exist on both
398                    if (this.hasIdProperties() && other.hasIdProperties()) {
399                        return this.getIdProperties().containsAll(other.getIdProperties());
400                    }
401                    return true;
402                }
403                // Path only in one, so the identification properties MUST match
404                if (!other.hasIdProperties()) return false;
405                return this.getIdProperties().containsAll(other.getIdProperties());
406            }
407            return false;
408        }
409    
410        /**
411         * {@inheritDoc}
412         * 
413         * @see java.lang.Iterable#iterator()
414         */
415        public Iterator<Property> iterator() {
416            return idProperties != null ? idProperties.iterator() : NO_ID_PROPERTIES_ITERATOR;
417        }
418    
419        /**
420         * {@inheritDoc}
421         * 
422         * @see java.lang.Object#hashCode()
423         */
424        @Override
425        public int hashCode() {
426            return HashCode.compute(path, idProperties);
427        }
428    
429        /**
430         * {@inheritDoc}
431         * 
432         * @see java.lang.Object#equals(java.lang.Object)
433         */
434        @Override
435        public boolean equals( Object obj ) {
436            if (obj instanceof Location) {
437                Location that = (Location)obj;
438                if (this.hasPath()) {
439                    if (!this.getPath().equals(that.getPath())) return false;
440                } else {
441                    if (that.hasPath()) return false;
442                }
443                if (this.hasIdProperties()) {
444                    if (!this.getIdProperties().equals(that.getIdProperties())) return false;
445                } else {
446                    if (that.hasIdProperties()) return false;
447                }
448                return true;
449            }
450            return false;
451        }
452    
453        /**
454         * {@inheritDoc}
455         * 
456         * @see java.lang.Object#toString()
457         */
458        @Override
459        public String toString() {
460            StringBuilder sb = new StringBuilder();
461            if (this.hasPath()) {
462                if (this.hasIdProperties()) sb.append("[ ");
463                sb.append(this.getPath());
464                if (this.hasIdProperties()) sb.append(" && ");
465            }
466            if (this.hasIdProperties()) {
467                sb.append(this.getIdProperties().toString());
468                if (this.hasPath()) sb.append(" ]");
469            }
470            return sb.toString();
471        }
472    
473        /**
474         * Create a copy of this location that adds the supplied identification property. The new identification property will replace
475         * any existing identification property with the same name on the original.
476         * 
477         * @param newIdProperty the new identification property, which may be null
478         * @return the new location, or this location if the new identification property is null or empty
479         */
480        public Location with( Property newIdProperty ) {
481            if (newIdProperty == null || newIdProperty.isEmpty()) return this;
482            return new Location(this, newIdProperty);
483        }
484    
485        /**
486         * Create a copy of this location that uses the supplied path.
487         * 
488         * @param newPath the new path for the location
489         * @return the new location, or this location if the path is equal to this location's path
490         */
491        public Location with( Path newPath ) {
492            if (newPath == null) return this;
493            if (!this.path.equals(newPath)) return new Location(this, newPath);
494            return this;
495        }
496    
497    }