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