]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java
Merge commit 'd186091'
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / ParentNode.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
3  * in Industry THTH ry.\r
4  * All rights reserved. This program and the accompanying materials\r
5  * are made available under the terms of the Eclipse Public License v1.0\r
6  * which accompanies this distribution, and is available at\r
7  * http://www.eclipse.org/legal/epl-v10.html\r
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.scenegraph;
13
14 import gnu.trove.map.hash.THashMap;\r
15 \r
16 import java.beans.PropertyChangeEvent;\r
17 import java.beans.PropertyChangeListener;\r
18 import java.util.Collection;\r
19 import java.util.Collections;\r
20 import java.util.Map;\r
21
22 /**\r
23  * Base class of all scene graph nodes which can have a set of sub-nodes\r
24  * (children). This class only provides support for unordered children.\r
25  * \r
26  * @param <T>\r
27  */\r
28 public abstract class ParentNode<T extends INode> extends Node {\r
29 \r
30     private static final long                  serialVersionUID     = 8519410262849626534L;\r
31 \r
32     public static final String                 EXISTING             = "#EXISTING#";\r
33     public static final String                 UNLINK               = "#UNLINK#";\r
34     public static final String                 NULL                 = "#NULL#";\r
35 \r
36     protected static final String[]            EMPTY_STRING_ARRAY   = {};\r
37 \r
38     @SuppressWarnings("rawtypes")\r
39     private static final Map                   DISPOSED_CHILDREN    = Collections.emptyMap();\r
40 \r
41     /**\r
42      * A value used for {@link #rootNodeCache} to indicate the node has been\r
43      * disposed and the root node cache is to be considered indefinitely\r
44      * invalid.\r
45      */\r
46     protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {\r
47         private static final long serialVersionUID = 6155494069158034123L;\r
48         @Override\r
49         public void asyncRemoveNode(INode node) {\r
50             throw new Error();\r
51         }\r
52     };\r
53 \r
54     protected transient Map<String, T>         children             = createChildMap();\r
55
56     /**\r
57      * A cached value for the root node of this parent node. This makes it\r
58      * possible to optimize {@link #getRootNode()} so that not all invocations\r
59      * go through the whole node hierarchy to find the root. This helps in\r
60      * making {@link ILookupService} located in the root node perform better and\r
61      * therefore be more useful.\r
62      * \r
63      * @see #DISPOSED\r
64      */\r
65     protected transient volatile ParentNode<?> rootNodeCache;\r
66 \r
67     protected Map<String, T> createChildMap() {\r
68         return new THashMap<String, T>(1);\r
69     }\r
70     \r
71     public final <TC> TC addNode(Class<TC> a) {
72         return addNode(java.util.UUID.randomUUID().toString(), a);
73     }
74 \r
75     @SuppressWarnings("unchecked")\r
76     public <TC extends INode> TC addNode(String id, TC child) {\r
77 \r
78         child.setParent(this);\r
79         children.put(id, (T)child);\r
80 \r
81         child.init();\r
82         childrenChanged();\r
83         return (TC)child;\r
84         \r
85     }\r
86 \r
87     @SuppressWarnings("unchecked")\r
88     public <TC extends INode> TC attachNode(String id, TC child) {\r
89 \r
90         child.setParent(this);\r
91         children.put(id, (T)child);\r
92 \r
93         child.attach();\r
94         childrenChanged();\r
95         return (TC)child;\r
96         \r
97     }\r
98 \r
99     @SuppressWarnings("unchecked")\r
100     public <TC extends INode> TC detachNode(String id) {\r
101         T child = children.remove(id);\r
102         if (child == null)\r
103             return null;\r
104         child.setParent(null);\r
105         childrenChanged();\r
106         return (TC) child;\r
107     }\r
108 \r
109     @SuppressWarnings("unchecked")
110     public <TC> TC addNode(String id, Class<TC> a) {
111         // a must be extended from Node
112         if(!Node.class.isAssignableFrom(a)) {
113             throw new IllegalArgumentException(a + " is not extended from org.simantics.scenegraph.Node");
114         }
115         INode child = null;\r
116         try {
117             child = (Node) a.newInstance();
118         } catch (InstantiationException e) {
119             throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e);
120         } catch (IllegalAccessException e) {
121             throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e);
122         }
123
124         child.setParent(this);
125         children.put(id, (T)child);
126
127         child.init();
128         childrenChanged();
129         return (TC)child;
130     }
131 \r
132     @SuppressWarnings("unchecked")\r
133     public final <TC extends INode> TC getOrAttachNode(String id, TC a) {\r
134         synchronized (children) {\r
135             if (children.containsKey(id))\r
136                 return (TC) children.get(id);\r
137         }\r
138         return attachNode(id, a);\r
139     }\r
140     
141     @SuppressWarnings("unchecked")
142     public final <TC> TC getOrCreateNode(String id, Class<TC> a) {
143         synchronized (children) {\r
144             if (children.containsKey(id))\r
145                 return (TC) children.get(id);\r
146         }\r
147         return addNode(id, a);
148     }
149
150     public final void removeNode(String id) {\r
151         INode child = null;\r
152         synchronized (children) {
153             child = children.remove(id);\r
154         }
155         if (child != null) {\r
156             if (child instanceof ParentNode<?>) {\r
157                 ((ParentNode<?>) child).removeNodes();\r
158             }\r
159             child.cleanup();
160             child.setParent(null);\r
161             childrenChanged();
162             if (propertyChangeListener != null) {\r
163                 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
164             }
165         }
166     }
167
168     public final void removeNode(INode child) {
169         synchronized (children) {
170             String id = null;
171             // FIXME: damn slow, needs more data structure to be supported well
172             // One option would be to store the id<->node mappings in a BidiMap.
173             for (String tmp : children.keySet()) {
174                 if (children.get(tmp).equals(child)) {
175                     id = tmp;
176                     break;
177                 }
178             }
179             if(id == null) return;
180             children.remove(id);\r
181             childrenChanged();
182         }\r
183         if (child instanceof ParentNode<?>) {\r
184             ((ParentNode<?>) child).removeNodes();\r
185         }
186         child.cleanup();
187         child.setParent(null);\r
188
189         if (propertyChangeListener != null) {
190             propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
191         }
192     }\r
193 \r
194     /**\r
195      * This method removes and disposes all children of the node (and their\r
196      * children).\r
197      */\r
198     public final void removeNodes() {
199         synchronized (children) {\r
200             boolean changed = false;
201             String[] keys = children.keySet().toArray(EMPTY_STRING_ARRAY);
202             for (String key : keys) {\r
203                 INode child = children.remove(key);\r
204                 if (child != null) {\r
205                     changed = true;\r
206                     if (child instanceof ParentNode<?>) {\r
207                         ((ParentNode<?>) child).removeNodes();\r
208                     }
209                     child.cleanup();
210                     child.setParent(null);\r
211                     if (propertyChangeListener != null) {\r
212                         propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
213                     }
214                 }
215             }
216             if (changed)
217                 childrenChanged();
218         }
219     }
220
221     /**\r
222      * Invoked every time the set of child changes for a {@link ParentNode}.\r
223      * Extending implementations may override to perform their own actions, such\r
224      * as invalidating possible caches.\r
225      */\r
226     protected void childrenChanged() {
227     }
228
229     public abstract void asyncRemoveNode(INode node);
230
231     public T getNode(String id) {
232         return children.get(id);
233     }
234
235     /**\r
236      * @return the IDs of the child nodes as a collection. Returns internal\r
237      *         state which must not be directly modified by client code!\r
238      */\r
239     public Collection<String> getNodeIds() {
240         return children.keySet();
241     }
242
243     /**
244      * @return the collection of this node's children in an unspecified order
245      */
246     public Collection<T> getNodes() {
247         return children.values();\r
248     }
249
250     public int getNodeCount() {
251         return children.size();\r
252     }
253
254     /**
255      * Recursively set the PropertyChangeListener
256      * 
257      * @param propertyChangeListener
258      */
259     public void setPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
260         this.propertyChangeListener = propertyChangeListener;
261         synchronized(children) {
262             for(T t : children.values()) {
263                 INode child = t;
264                 if(child instanceof ParentNode<?>) {
265                     ((ParentNode<?>)child).setPropertyChangeListener(propertyChangeListener);
266                 } else {
267                     ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME
268                 }
269             }
270         }
271     }
272
273     @SuppressWarnings("unchecked")\r
274     @Override
275     public void cleanup() {
276         retractMapping();\r
277         if (children != DISPOSED_CHILDREN) { \r
278             synchronized(children) {
279                 for(T child : children.values()) {
280                     ((INode)child).cleanup();\r
281                     ((INode)child).setParent(null);
282                 }
283                 children.clear();\r
284                 children = DISPOSED_CHILDREN;
285                 childrenChanged();\r
286             }\r
287             rootNodeCache = DISPOSED;\r
288         }
289     }\r
290 \r
291     @Override
292     public void delete() {\r
293         if(parent == null) {\r
294             return;\r
295         }\r
296 \r
297         synchronized (children) {\r
298             // 1. Add children under parent\r
299             parent.appendChildren(children);\r
300 \r
301             // 2. Clear children\r
302             children.clear();\r
303         }\r
304 \r
305         // 3. Remove this node from parent\r
306         parent.unlinkChild(this);\r
307 \r
308         // 4. Cleanup, this node is now disposed\r
309         cleanup();\r
310     }\r
311 \r
312     /**\r
313      * Helper method for delete()\r
314      * @param children\r
315      */\r
316     @SuppressWarnings("unchecked")\r
317     protected void appendChildren(Map<String, ?> children) {\r
318         synchronized(this.children) {\r
319             for(String id : children.keySet()) {\r
320                 INode child = (INode)children.get(id);\r
321                 this.children.put(id, (T)child); // Hopefully cast works\r
322                 child.setParent(this);\r
323 \r
324                 // Send notify only if we are on server side (or standalone)\r
325                 if (propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
326                     propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", null, EXISTING)); // "children" is a special field name\r
327                 }\r
328             }\r
329         }\r
330     }\r
331 \r
332     @SuppressWarnings("unchecked")\r
333     protected void appendChild(String id, INode child) {\r
334         children.put(id, (T)child);\r
335         child.setParent(this);\r
336         // Send notify only if we are on server side (or standalone)\r
337         if (propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
338             propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+(child).getId()+"]", null, EXISTING)); // "children" is a special field name\r
339         }\r
340     }\r
341 \r
342     /**\r
343      * Same as removeNode, but does not perform cleanup for the child\r
344      * @param child\r
345      */\r
346     protected void unlinkChild(INode child) {\r
347         synchronized(children) {\r
348             String id = null;\r
349             // FIXME: damn slow, needs more data structure to be supported well\r
350             // One option would be to store the id<->node mappings in a BidiMap.\r
351             for(String tmp : children.keySet()) {\r
352                 if(children.get(tmp).equals(child)) {\r
353                     id = tmp;\r
354                     break;\r
355                 }\r
356             }\r
357             if(id == null) return;\r
358             children.remove(id);\r
359             childrenChanged();\r
360         }\r
361 \r
362         if(propertyChangeListener != null && location.equals(Location.LOCAL)) {\r
363             propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), UNLINK)); // "children" is a special field name\r
364         }\r
365     }\r
366
367     @Override
368     public String toString() {
369         return super.toString() + " [#child=" + children.size() + "]";
370     }
371
372     @Override\r
373     public ParentNode<?> getRootNode() {\r
374         // Note: using double-checked locking idiom with volatile keyword.\r
375         // Works with Java 1.5+\r
376         ParentNode<?> result = rootNodeCache;\r
377         if (result == DISPOSED)\r
378             return null;\r
379 \r
380         if (result == null) {\r
381             synchronized (this) {\r
382                 result = rootNodeCache;\r
383                 if (result == null) {\r
384                     if (parent != null) {\r
385                         result = parent.getRootNode();\r
386                         rootNodeCache = result;\r
387                     }\r
388                 }\r
389             }\r
390         }\r
391         return result;\r
392     }\r
393 \r
394 }