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.HashSet;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Set;
22  
23  import org.apache.commons.configuration.HierarchicalConfiguration;
24  import org.apache.commons.configuration.event.ConfigurationEvent;
25  import org.apache.commons.configuration.event.ConfigurationListener;
26  import org.apache.commons.configuration.tree.ConfigurationNode;
27  import org.apache.commons.lang.StringUtils;
28  
29  /**
30   * <p>
31   * A helper class for concrete tree view implementations that supports the
32   * processing of change events fired by a configuration serving as tree model.
33   * </p>
34   * <p>
35   * In <em>JGUIraffe</em>, for tree views instances of
36   * {@code HierarchicalConfiguration} are used as data model. It is desired that
37   * the tree view updates itself automatically if the underlying configuration
38   * object is changed. This is possible because a change listener can be
39   * registered at the configuration. This class implements such a change
40   * listener. It reacts on configuration change events, detects the configuration
41   * parent node affected by this change and passes it to an associated
42   * {@link TreeModelChangeListener}. This {@code TreeModelChangeListener}
43   * implementation can then update the underlying UI component.
44   * </p>
45   * <p>
46   * However, it is not a trivial task to find out the exact configuration node
47   * affected by a change. Configuration change events typically only contain the
48   * key of the node affected by a change, and this key is not necessarily unique.
49   * This implementation deals with this fact by trying to find a parent node
50   * common to all nodes referenced by a key. So the goal is to invalidate only a
51   * minimum part of the tree that has to be regenerated to be in sync with the
52   * model configuration. In the worst case, when no specific parent node can be
53   * determined, the root node used; this means that the whole tree component has
54   * to be reconstructed.
55   * </p>
56   * <p>
57   * One limitation of this implementation is that it is not fully thread-safe. It
58   * expects that only a single change event from a configuration is received at a
59   * given point in time. Typically, this should not be a problem because
60   * configuration implementations do not support concurrent updates.
61   * </p>
62   * <p>
63   * This class is mainly intended to be used internally by implementations for
64   * specific UI toolkits. Therefore, it does not do any sophisticated parameter
65   * checks.
66   * </p>
67   *
68   * @author Oliver Heger
69   * @version $Id: $
70   * @since 1.3
71   */
72  public class TreeConfigurationChangeHandler implements ConfigurationListener
73  {
74      /** The associated configuration. */
75      private final HierarchicalConfiguration configuration;
76  
77      /** The change listener to be notified. */
78      private final TreeModelChangeListener modelChangeListener;
79  
80      /** The node affected by the most recent change event. */
81      private ConfigurationNode changedNode;
82  
83      /**
84       * Creates a new instance of {@code TreeConfigurationChangeHandler} and
85       * initializes it with the given {@code HierarchicalConfiguration} and the
86       * change listener. Note that this constructor does not register the object
87       * as listener at the configuration; this has to be done manually.
88       *
89       * @param config the associated {@code HierarchicalConfiguration}
90       * @param listener the change listener
91       */
92      public TreeConfigurationChangeHandler(HierarchicalConfiguration config,
93              TreeModelChangeListener listener)
94      {
95          configuration = config;
96          modelChangeListener = listener;
97      }
98  
99      /**
100      * Returns the {@code HierarchicalConfiguration} associated with this
101      * object.
102      *
103      * @return the {@code HierarchicalConfiguration}
104      */
105     public HierarchicalConfiguration getConfiguration()
106     {
107         return configuration;
108     }
109 
110     /**
111      * Returns the {@code TreeModelChangeListener} associated with this object.
112      *
113      * @return the {@code TreeModelChangeListener}
114      */
115     public TreeModelChangeListener getModelChangeListener()
116     {
117         return modelChangeListener;
118     }
119 
120     /**
121      * Changes the name of a {@code ConfigurationNode}. This is a utility method
122      * which is probably needed by each concrete tree model implementation.
123      * Normally, the name of a configuration node cannot be changed if it is
124      * part of a node hierarchy. Therefore, this method uses some tricks to
125      * achieve this goal. The return value indicates if a change was done. If
126      * the passed in new name equals the current name, nothing is changed, and
127      * result is <b>false</b>.
128      *
129      * @param node the {@code ConfigurationNode} to be changed
130      * @param newName the new name of this node
131      * @return <b>true</b> if a name change was necessary, <b>false</b>
132      *         otherwise
133      */
134     public boolean changeNodeName(ConfigurationNode node, String newName)
135     {
136         if (!StringUtils.equals(node.getName(), newName))
137         {
138             ConfigurationNode parent = node.getParentNode();
139             node.setParentNode(null);
140             node.setName(newName);
141             node.setParentNode(parent);
142             return true;
143         }
144         return false;
145     }
146 
147     /**
148      * Reacts on change events of the associated {@code Configuration}. This
149      * implementation determines the root node of the sub tree affected by the
150      * change. This node is then passed to the associated
151      * {@code TreeModelChangeListener}.
152      *
153      * @param event the change event fired by the configuration
154      */
155     public void configurationChanged(ConfigurationEvent event)
156     {
157         if (event.isBeforeUpdate())
158         {
159             // These types have to be checked before an update because
160             // afterwards the key may be invalid.
161             switch (event.getType())
162             {
163             case HierarchicalConfiguration.EVENT_CLEAR_PROPERTY:
164             case HierarchicalConfiguration.EVENT_CLEAR_TREE:
165             case HierarchicalConfiguration.EVENT_SET_PROPERTY:
166                 changedNode = findAffectedNode(event.getPropertyName(), true);
167                 break;
168 
169             case HierarchicalConfiguration.EVENT_ADD_NODES:
170                 changedNode = findAffectedNode(event.getPropertyName(), false);
171                 break;
172 
173             default:
174                 changedNode = getConfiguration().getRootNode();
175             }
176         }
177 
178         else
179         {
180             // Some event types need to be checked after the update; only
181             // then the key is valid.
182             if (event.getType() == HierarchicalConfiguration.EVENT_ADD_PROPERTY)
183             {
184                 changedNode = findAffectedNode(event.getPropertyName(), true);
185             }
186 
187             notifyListener();
188         }
189     }
190 
191     /**
192      * Tries to determine the least common parent node of the passed in nodes.
193      * In worst case, this is the configuration's root node.
194      *
195      * @param nd1 node 1
196      * @param nd2 node 2
197      * @return the common parent node
198      */
199     ConfigurationNode findCommonParent(ConfigurationNode nd1,
200             ConfigurationNode nd2)
201     {
202         Set<ConfigurationNode> path = new HashSet<ConfigurationNode>();
203         for (ConfigurationNode current = nd2; current != null; current =
204                 current.getParentNode())
205         {
206             if (current == nd1)
207             {
208                 return current;
209             }
210             path.add(current);
211         }
212 
213         for (ConfigurationNode current = nd1.getParentNode(); current != null; current =
214                 current.getParentNode())
215         {
216             if (path.contains(current))
217             {
218                 return current;
219             }
220         }
221 
222         return getConfiguration().getRootNode();
223     }
224 
225     /**
226      * Obtains the deepest node in the hierarchical structure affected by a
227      * change event described by the given key. This method tries to resolve the
228      * key. If there is only one hit, the affected node can be uniquely
229      * identified. Otherwise, the least common parent node of all retrieved
230      * nodes is searched for. In worst case, this is the configuration's root
231      * node.
232      *
233      * @param key the key associated with a change event
234      * @param parent a flag whether the parent node of the key is desired
235      * @return the deepest node affected by the change
236      */
237     private ConfigurationNode findAffectedNode(String key, boolean parent)
238     {
239         List<ConfigurationNode> nodes = nodesForKey(key);
240         if (nodes.isEmpty())
241         {
242             return getConfiguration().getRootNode();
243         }
244 
245         Iterator<ConfigurationNode> it = nodes.iterator();
246         ConfigurationNode current = resolveParent(it.next(), parent);
247         while (it.hasNext() && current != getConfiguration().getRootNode())
248         {
249             current =
250                     findCommonParent(current, resolveParent(it.next(), parent));
251         }
252         return current;
253     }
254 
255     /**
256      * Obtains a list with the nodes referred to by the specified key. This
257      * method queries the expression engine of the wrapped configuration to
258      * resolve the key.
259      *
260      * @param key the configuration key
261      * @return a list with the configuration nodes this key points to
262      */
263     private List<ConfigurationNode> nodesForKey(String key)
264     {
265         return getConfiguration().getExpressionEngine().query(
266                 getConfiguration().getRootNode(), key);
267     }
268 
269     /**
270      * Notifies the associated listener about a change event. The deepest node
271      * in the hierarchical structure affected by the change is passed.
272      */
273     private void notifyListener()
274     {
275         getModelChangeListener().treeModelChanged(changedNode);
276     }
277 
278     /**
279      * Resolves the specified node regarding the parent flag. If the parent flag
280      * is set, the node's parent is returned (if any); otherwise, the node
281      * itself is returned.
282      *
283      * @param nd the node in question
284      * @param parent the parent flag
285      * @return the resolved node
286      */
287     private static ConfigurationNode resolveParent(ConfigurationNode nd,
288             boolean parent)
289     {
290         if (parent)
291         {
292             ConfigurationNode parentNode = nd.getParentNode();
293             return (parentNode != null) ? parentNode : nd;
294         }
295         return nd;
296     }
297 }