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