View Javadoc

1   /*
2    * Copyright 2006-2016 The JGUIraffe Team.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License")
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.jguiraffe.gui.builder.components.model;
17  
18  import java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.apache.commons.configuration.tree.ConfigurationNode;
24  import org.apache.commons.configuration.tree.DefaultConfigurationKey;
25  
26  /**
27   * <p>
28   * A class that represents a path in a tree component.
29   * </p>
30   * <p>
31   * Objects of this class are used for uniquely identifying specific nodes in a
32   * tree. They contain the path from a target node to the root node. This is used
33   * for instance to describe the selected node(s) in a tree.
34   * </p>
35   * <p>
36   * With the methods provided by this class the path can be queried as a list of
37   * <code>ConfigurationNode</code> objects. (Theoretically the end node of the
38   * path would already be a unique description of a path because there is only a
39   * single way to the root node; by navigating through the parent nodes an
40   * application can construct this path. However, if this information is provided
41   * explicitly as a list, it is much easier for an application to deal with
42   * paths.)
43   * </p>
44   * <p>
45   * It is also possible to query the single names and indices of the nodes
46   * comprising the path. This is especially useful for constructing a
47   * configuration key representing the path. This key can then be used for
48   * querying or manipulating the <code>Configuration</code> object that serves as
49   * data store for the tree's model.
50   * </p>
51   * <p>
52   * When an instance of this class is created it is fully initialized with the
53   * nodes that belong to the represented path. These references cannot be changed
54   * later any more. However, the nodes themselves are not copied because this
55   * could be expensive. Therefore the class is not immutable. It can be used by
56   * multiple threads only under the precondition that the configuration nodes
57   * involved do not change. The main use case however is that a path is obtained
58   * and processed by a single event handler in the event dispatch thread.
59   * </p>
60   *
61   * @author Oliver Heger
62   * @version $Id: TreeNodePath.java 205 2012-01-29 18:29:57Z oheger $
63   */
64  public class TreeNodePath
65  {
66      /** Stores a list with the nodes in the path. */
67      private final List<ConfigurationNode> nodes;
68  
69      /** An array with the names of the nodes in the path. */
70      private final String[] nodeNames;
71  
72      /** An array with the indices of the nodes in the path. */
73      private final int[] indices;
74  
75      /**
76       * Creates a new instance of <code>TreeNodePath</code> and initializes it
77       * with the target node. From this node the path to the root will be
78       * constructed.
79       *
80       * @param target the target node (must not be <b>null</b>)
81       * @throws IllegalArgumentException if the target node is <b>null</b>
82       */
83      public TreeNodePath(ConfigurationNode target)
84      {
85          if (target == null)
86          {
87              throw new IllegalArgumentException("Target node must not be null!");
88          }
89  
90          nodes = initPath(target);
91          nodeNames = initNodeNames(nodes);
92          indices = initNodeIndices(nodes);
93      }
94  
95      /**
96       * Creates a new instance of <code>TreeNodePath</code> and initializes it
97       * with the nodes comprising the path.
98       *
99       * @param pathNodes the collection with the nodes of the path (must not be
100      *        <b>null</b>)
101      * @throws IllegalArgumentException if the collection is <b>null</b>
102      */
103     public TreeNodePath(Collection<ConfigurationNode> pathNodes)
104     {
105         if (pathNodes == null)
106         {
107             throw new IllegalArgumentException(
108                     "Collection with the nodes must not be null!");
109         }
110 
111         nodes = new ArrayList<ConfigurationNode>(pathNodes);
112         nodeNames = initNodeNames(nodes);
113         indices = initNodeIndices(nodes);
114     }
115 
116     /**
117      * Returns a list with the nodes comprising the path. The root node of the
118      * tree is at index 0. The target node of this path is at the highest index.
119      * Note that the list returned by this method is immutable.
120      *
121      * @return a list with the nodes of this path
122      */
123     public List<ConfigurationNode> getNodes()
124     {
125         return Collections.unmodifiableList(nodes);
126     }
127 
128     /**
129      * Returns the length of this path. This is the number of nodes contained in
130      * this path from the target node to the root node.
131      *
132      * @return the length of this path
133      */
134     public int size()
135     {
136         return nodes.size();
137     }
138 
139     /**
140      * <p>
141      * Returns the name of the path node with the given index. The purpose of
142      * this method is constructing unique configuration keys by iterating over
143      * all node names and indices. Because the root node is not part of a
144      * configuration key it is not taken into account by this method. Thus the
145      * index 0 will not return the name of the root node, but the name of the
146      * next node in the path below the root node. The last node of the path has
147      * then the index <code>size() - 2</code>. The following code fragment shows
148      * how this method and <code>getNodeIndex()</code> can be used for
149      * constructing a string representation of this path:
150      * </p>
151      * <p>
152      *
153      * <pre>
154      * TreeNodePath path = ...;
155      * StringBuilder buf = new StringBuilder();
156      * for (int i = 0; i &lt; path.size() - 1; i++) {
157      *   if (i &gt; 0) {
158      *     buf.append('/');  // separator for node names
159      *   }
160      *   buf.append(path.getNodeName(i);
161      *   buf.append('[').append(path.getNodeIndex(i)).append(']');
162      * }
163      * </pre>
164      *
165      * </p>
166      *
167      * @param index the index of the desired node
168      * @return the name of this node
169      * @throws ArrayIndexOutOfBoundsException if the index is invalid
170      */
171     public String getNodeName(int index)
172     {
173         return nodeNames[index];
174     }
175 
176     /**
177      * Returns the index of the path node at the specified position in the path.
178      * Analogously to <code>getNodeName()</code> this method is intended for
179      * constructing unique keys for paths that can be used for accessing the
180      * underlying <code>Configuration</code> object. Because a configuration
181      * node can have multiple child nodes with the same name indices are
182      * required for making keys unique. The index returned by this method is non
183      * 0 only if there are multiple child nodes with the same name. In this case
184      * it determines, which of these child nodes is the one that belongs to this
185      * path.
186      *
187      * @param index the index of the desired node (in the path)
188      * @return a unique index for this node for constructing a unique key
189      * @throws ArrayIndexOutOfBoundsException if the index is invalid
190      * @see #getNodeName(int)
191      */
192     public int getNodeIndex(int index)
193     {
194         return indices[index];
195     }
196 
197     /**
198      * Returns the target node of this path. This is the last component in the
199      * path.
200      *
201      * @return the target node of this path
202      */
203     public ConfigurationNode getTargetNode()
204     {
205         return nodes.get(nodes.size() - 1);
206     }
207 
208     /**
209      * Appends this path to the given <code>ConfigurationKey</code>. This
210      * implementation will create a unique key that corresponds to the path
211      * represented. The node names and the indices along the path are appended
212      * to the given configuration key.
213      *
214      * @param key the key (must not be <b>null</b>)
215      * @throws IllegalArgumentException if the key is <b>null</b>
216      */
217     public void pathToKey(DefaultConfigurationKey key)
218     {
219         if (key == null)
220         {
221             throw new IllegalArgumentException("Key must not be null!");
222         }
223 
224         for (int i = 0; i < size() - 1; i++)
225         {
226             key.append(getNodeName(i));
227             key.appendIndex(getNodeIndex(i));
228         }
229     }
230 
231     /**
232      * Returns the parent path of this {@code TreeNodePath}. This means that the
233      * target node of the {@code TreeNodePath} returned by this method is the
234      * parent node of this {@code TreeNodePath}'s target node. This {@code
235      * TreeNodePath} must have at least a size of 2, otherwise an exception is
236      * thrown.
237      *
238      * @return the parent path of this {@code TreeNodePath}
239      * @throws IllegalStateException if this path already represents the root
240      *         node
241      */
242     public TreeNodePath parentPath()
243     {
244         if (size() <= 1)
245         {
246             throw new IllegalStateException(
247                     "Cannot obtain parent path for root node!");
248         }
249 
250         return new TreeNodePath(nodes.get(size() - 2));
251     }
252 
253     /**
254      * Returns a {@code TreeNodePath} object that was created by appending the
255      * specified {@code ConfigurationNode} to this path. The new node becomes
256      * the target node of the new {@code TreeNodePath} object. This method is
257      * useful when navigating through a tree structure. The passed in node must
258      * be a child node of the current target node.
259      *
260      * @param node the node to be appended to the path
261      * @return the new {@code TreeNodePath} extended by the node
262      * @throws IllegalArgumentException if the passed in node is <b>null</b> or
263      *         not a child node of the target node
264      */
265     public TreeNodePath append(ConfigurationNode node)
266     {
267         if (node == null)
268         {
269             throw new IllegalArgumentException("Node must not be null!");
270         }
271         if (node.getParentNode() != getTargetNode())
272         {
273             throw new IllegalArgumentException(
274                     "Node is not a child of the target node!");
275         }
276 
277         List<ConfigurationNode> newNodes =
278                 new ArrayList<ConfigurationNode>(size() + 1);
279         newNodes.addAll(nodes);
280         newNodes.add(node);
281         return new TreeNodePath(newNodes);
282     }
283 
284     /**
285      * Returns a {@code TreeNodePath} object that was created by appending the
286      * specified child node of the current target node to this path. This method
287      * determines the child node with the given name and index. It then creates
288      * a new {@code TreeNodePath} object with this node as target node.
289      *
290      * @param childName the name of the child node to be appended
291      * @param index the index of the node (in case there are multiple children
292      *        with the same name)
293      * @return the new {@code TreeNodePath} extended by the child node
294      * @throws IllegalArgumentException if no child node with this name can be
295      *         found
296      * @throws IndexOutOfBoundsException if the index is invalid
297      */
298     public TreeNodePath append(String childName, int index)
299     {
300         if (childName == null)
301         {
302             throw new IllegalArgumentException(
303                     "Name of child node must not be null!");
304         }
305         if (getTargetNode().getChildrenCount(childName) < 1)
306         {
307             throw new IllegalArgumentException("Cannot find child with name "
308                     + childName);
309         }
310 
311         return append((ConfigurationNode) getTargetNode()
312                 .getChildren(childName).get(index));
313     }
314 
315     /**
316      * Returns a {@code TreeNodePath} object that was created by appending the
317      * first child node of the current target node with the given name to this path.
318      * This is a short cut of {@code append(childName, 0)}.
319      * @param childName the name of the child node to be appended
320      * @return the new {@code TreeNodePath} extended by the child node
321      * @throws IllegalArgumentException if no child node with this name can be found
322      */
323     public TreeNodePath append(String childName)
324     {
325         return append(childName, 0);
326     }
327 
328     /**
329      * Tests whether two objects are equal. Two path objects are considered
330      * equal if and only if they refer to the same target node.
331      *
332      * @param obj the object to test
333      * @return a flag whether these objects are equal
334      */
335     @Override
336     public boolean equals(Object obj)
337     {
338         if (obj == this)
339         {
340             return true;
341         }
342         if (!(obj instanceof TreeNodePath))
343         {
344             return false;
345         }
346 
347         TreeNodePath c = (TreeNodePath) obj;
348         return getTargetNode().equals(c.getTargetNode());
349     }
350 
351     /**
352      * Returns a hash code for this object. The hash code is obtained from the
353      * target node.
354      *
355      * @return a hash code
356      */
357     @Override
358     public int hashCode()
359     {
360         return getTargetNode().hashCode();
361     }
362 
363     /**
364      * Returns a string representation for this object. This string will contain
365      * a representation of the path using '/' as path separator and square
366      * brackets for indices.
367      *
368      * @return a string for this object
369      */
370     @Override
371     public String toString()
372     {
373         StringBuilder buf = new StringBuilder();
374         buf.append(getClass().getName()).append('@');
375         buf.append(System.identityHashCode(this));
376         buf.append("[ ");
377 
378         for (int i = 0; i < size() - 1; i++)
379         {
380             if (i > 0)
381             {
382                 buf.append('/');
383             }
384             buf.append(getNodeName(i));
385             buf.append('[').append(getNodeIndex(i)).append(']');
386         }
387 
388         buf.append(" ]");
389         return buf.toString();
390     }
391 
392     /**
393      * Obtains the complete path from the specified target node. This method
394      * navigates through the parent nodes until it finds the root node.
395      *
396      * @param node the target node
397      * @return a list with the nodes of this path
398      */
399     private List<ConfigurationNode> initPath(ConfigurationNode node)
400     {
401         List<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
402         ConfigurationNode nd = node;
403 
404         while (nd != null)
405         {
406             nodes.add(nd);
407             nd = nd.getParentNode();
408         }
409 
410         Collections.reverse(nodes);
411         return nodes;
412     }
413 
414     /**
415      * Extracts the array with the node names from the given node list.
416      *
417      * @param nodes the list with the nodes
418      * @return an array with the node names
419      */
420     private String[] initNodeNames(List<ConfigurationNode> nodes)
421     {
422         String[] names = new String[nodes.size() - 1];
423         for (int i = 0; i < names.length; i++)
424         {
425             names[i] = nodes.get(i + 1).getName();
426         }
427         return names;
428     }
429 
430     /**
431      * Determines the indices for the nodes that belong to the path.
432      *
433      * @param nodes the list with the nodes
434      * @return an array with the indices of the nodes
435      */
436     private int[] initNodeIndices(List<ConfigurationNode> nodes)
437     {
438         int[] indices = new int[nodes.size() - 1];
439         for (int i = 0; i < indices.length; i++)
440         {
441             indices[i] = indexForChild(nodes.get(i + 1));
442         }
443         return indices;
444     }
445 
446     /**
447      * Determines the index of the specified child node.
448      *
449      * @param nd the node
450      * @return the index of this node relative to its parent
451      */
452     private static int indexForChild(ConfigurationNode nd)
453     {
454         ConfigurationNode parent = nd.getParentNode();
455         assert parent != null : "No parent node!";
456 
457         List<?> children = parent.getChildren(nd.getName());
458         int index = 0;
459         for (Object c : children)
460         {
461             if (c == nd)
462             {
463                 break;
464             }
465             index++;
466         }
467 
468         return index;
469     }
470 }