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 < path.size() - 1; i++) {
157 * if (i > 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 }