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 }