1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
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
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.scenegraph;
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;
21 import gnu.trove.map.TLongObjectMap;
22 import gnu.trove.map.hash.TLongObjectHashMap;
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.
30 public abstract class ParentNode<T extends INode> extends Node {
32 private static final long serialVersionUID = 8519410262849626534L;
34 public static final String EXISTING = "#EXISTING#";
35 public static final String UNLINK = "#UNLINK#";
36 public static final String NULL = "#NULL#";
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
43 protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {
44 private static final long serialVersionUID = 6155494069158034123L;
46 public void asyncRemoveNode(INode node) {
51 private static class ImmutableIdMap extends TLongObjectHashMap<String> {
52 private static final String MSG = "immutable singleton instance";
55 public String put(long key, String value) {
56 throw new UnsupportedOperationException(MSG);
59 public void putAll(Map<? extends Long, ? extends String> map) {
60 throw new UnsupportedOperationException(MSG);
63 public void putAll(TLongObjectMap<? extends String> map) {
64 throw new UnsupportedOperationException(MSG);
67 public String putIfAbsent(long key, String value) {
68 throw new UnsupportedOperationException(MSG);
73 * This is the value given to {@link #children} when this node is disposed and
76 private static final Map<String, INode> DISPOSED_CHILDREN = Collections.emptyMap();
79 * This is the value given to {@link #childrenIdMap} when this node is disposed
82 private static final TLongObjectMap<String> DISPOSED_CHILDREN_ID_MAP = new ImmutableIdMap();
84 protected transient Map<String, INode> children = createChildMap();
85 private transient TLongObjectMap<String> childrenIdMap = new TLongObjectHashMap<>();
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.
96 protected transient volatile ParentNode<?> rootNodeCache;
98 protected Map<String, INode> createChildMap() {
99 return createChildMap(1);
102 protected Map<String, INode> createChildMap(int initialCapacity) {
103 // With JDK 1.8 HashMap is faster than Trove
104 return new HashMap<>(initialCapacity);
107 public final <TC> TC addNode(Class<TC> a) {
108 return addNode(java.util.UUID.randomUUID().toString(), a);
111 public <TC extends INode> TC addNode(String id, TC child) {
112 return addNodeInternal(id, child, true, true);
115 private <TC extends INode> TC addNodeInternal(String id, TC child, boolean init, boolean addToChildren) {
116 child.setParent(this);
118 children.put(id, child);
120 childrenIdMap.put(child.getId(), id);
128 public <TC extends INode> TC attachNode(String id, TC child) {
129 return addNodeInternal(id, child, false, true);
132 @SuppressWarnings("unchecked")
133 public <TC extends INode> TC detachNode(String id) {
134 INode child = children.remove(id);
137 childrenIdMap.remove(child.getId());
138 child.setParent(null);
143 public <TC> TC addNode(String id, Class<TC> a) {
144 return addNodeInternal0(id, a, true);
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");
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);
161 return (TC) addNodeInternal(id, child, true, addToChildren);
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);
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);
178 public final void removeNode(String id) {
179 INode child = children.remove(id);
181 removeNodeInternal(child, true);
184 public final void removeNode(INode child) {
185 String key = childrenIdMap.get(child.getId());
190 * This method removes and disposes all children of the node (and their
193 public final void removeNodes() {
194 boolean changed = children.size() > 0;
195 children.forEach((id, child) -> removeNodeInternal(child, false));
197 childrenIdMap.clear();
203 private void removeNodeInternal(INode child, boolean triggerChildrenChanged) {
205 if (child instanceof ParentNode<?>) {
206 ((ParentNode<?>) child).removeNodes();
209 child.setParent(null);
210 if (triggerChildrenChanged)
212 triggerPropertyChangeEvent(child);
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.
221 protected void childrenChanged() {
224 public abstract void asyncRemoveNode(INode node);
226 @SuppressWarnings("unchecked")
227 public T getNode(String id) {
228 return (T) children.get(id);
232 * @return the IDs of the child nodes as a collection. Returns internal
233 * state which must not be directly modified by client code!
235 public Collection<String> getNodeIds() {
236 return children.keySet();
240 * @return the collection of this node's children in an unspecified order
242 @SuppressWarnings("unchecked")
243 public Collection<T> getNodes() {
244 return (Collection<T>) children.values();
247 public int getNodeCount() {
248 return children.size();
252 * Recursively set the PropertyChangeListener
254 * @param propertyChangeListener
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);
262 ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME
268 public void cleanup() {
270 if (children != DISPOSED_CHILDREN) {
271 children.forEach((id, child) -> {
273 child.setParent(null);
276 childrenIdMap.clear();
277 children = DISPOSED_CHILDREN;
278 childrenIdMap = DISPOSED_CHILDREN_ID_MAP;
280 rootNodeCache = DISPOSED;
285 public void delete() {
290 // 1. Add children under parent
291 parent.appendChildren(children);
293 // 2. Clear child maps to prevent cleanup from deleting them in step 4.
295 childrenIdMap.clear();
297 // 3. Remove this node from parent
298 parent.unlinkChild(this);
300 // 4. Cleanup, this node is now disposed
305 * Helper method for delete()
308 protected void appendChildren(Map<String, INode> children) {
309 children.forEach((key, value) -> {
310 appendChildInternal(key, value);
314 protected void appendChild(String id, INode child) {
315 appendChildInternal(id, child);
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);
326 * Same as removeNode, but does not perform cleanup for the child
329 protected void unlinkChild(INode child) {
330 String id = childrenIdMap.remove(child.getId());
334 triggerPropertyChangeEvent(child);
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
346 public String toString() {
347 return super.toString() + " [#child=" + children.size() + "]";
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)
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;