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 gnu.trove.map.hash.THashMap;
16 import java.beans.PropertyChangeEvent;
17 import java.beans.PropertyChangeListener;
18 import java.util.Collection;
19 import java.util.Collections;
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.
28 public abstract class ParentNode<T extends INode> extends Node {
30 private static final long serialVersionUID = 8519410262849626534L;
32 public static final String EXISTING = "#EXISTING#";
33 public static final String UNLINK = "#UNLINK#";
34 public static final String NULL = "#NULL#";
36 protected static final String[] EMPTY_STRING_ARRAY = {};
38 @SuppressWarnings("rawtypes")
39 private static final Map DISPOSED_CHILDREN = Collections.emptyMap();
42 * A value used for {@link #rootNodeCache} to indicate the node has been
43 * disposed and the root node cache is to be considered indefinitely
46 protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {
47 private static final long serialVersionUID = 6155494069158034123L;
49 public void asyncRemoveNode(INode node) {
54 protected transient Map<String, T> children = createChildMap();
57 * A cached value for the root node of this parent node. This makes it
58 * possible to optimize {@link #getRootNode()} so that not all invocations
59 * go through the whole node hierarchy to find the root. This helps in
60 * making {@link ILookupService} located in the root node perform better and
61 * therefore be more useful.
65 protected transient volatile ParentNode<?> rootNodeCache;
67 protected Map<String, T> createChildMap() {
68 return new THashMap<String, T>(1);
71 public final <TC> TC addNode(Class<TC> a) {
72 return addNode(java.util.UUID.randomUUID().toString(), a);
75 @SuppressWarnings("unchecked")
76 public <TC extends INode> TC addNode(String id, TC child) {
78 child.setParent(this);
79 children.put(id, (T)child);
87 @SuppressWarnings("unchecked")
88 public <TC extends INode> TC attachNode(String id, TC child) {
90 child.setParent(this);
91 children.put(id, (T)child);
99 @SuppressWarnings("unchecked")
100 public <TC extends INode> TC detachNode(String id) {
101 T child = children.remove(id);
104 child.setParent(null);
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");
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);
124 child.setParent(this);
125 children.put(id, (T)child);
132 @SuppressWarnings("unchecked")
133 public final <TC extends INode> TC getOrAttachNode(String id, TC a) {
134 synchronized (children) {
135 if (children.containsKey(id))
136 return (TC) children.get(id);
138 return attachNode(id, a);
141 @SuppressWarnings("unchecked")
142 public final <TC> TC getOrCreateNode(String id, Class<TC> a) {
143 synchronized (children) {
144 if (children.containsKey(id))
145 return (TC) children.get(id);
147 return addNode(id, a);
150 public final void removeNode(String id) {
152 synchronized (children) {
153 child = children.remove(id);
156 if (child instanceof ParentNode<?>) {
157 ((ParentNode<?>) child).removeNodes();
160 child.setParent(null);
162 if (propertyChangeListener != null) {
163 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
168 public final void removeNode(INode child) {
169 synchronized (children) {
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)) {
179 if(id == null) return;
183 if (child instanceof ParentNode<?>) {
184 ((ParentNode<?>) child).removeNodes();
187 child.setParent(null);
189 if (propertyChangeListener != null) {
190 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
195 * This method removes and disposes all children of the node (and their
198 public final void removeNodes() {
199 synchronized (children) {
200 boolean changed = false;
201 String[] keys = children.keySet().toArray(EMPTY_STRING_ARRAY);
202 for (String key : keys) {
203 INode child = children.remove(key);
206 if (child instanceof ParentNode<?>) {
207 ((ParentNode<?>) child).removeNodes();
210 child.setParent(null);
211 if (propertyChangeListener != null) {
212 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
222 * Invoked every time the set of child changes for a {@link ParentNode}.
223 * Extending implementations may override to perform their own actions, such
224 * as invalidating possible caches.
226 protected void childrenChanged() {
229 public abstract void asyncRemoveNode(INode node);
231 public T getNode(String id) {
232 return children.get(id);
236 * @return the IDs of the child nodes as a collection. Returns internal
237 * state which must not be directly modified by client code!
239 public Collection<String> getNodeIds() {
240 return children.keySet();
244 * @return the collection of this node's children in an unspecified order
246 public Collection<T> getNodes() {
247 return children.values();
250 public int getNodeCount() {
251 return children.size();
255 * Recursively set the PropertyChangeListener
257 * @param propertyChangeListener
259 public void setPropertyChangeListener(PropertyChangeListener propertyChangeListener) {
260 this.propertyChangeListener = propertyChangeListener;
261 synchronized(children) {
262 for(T t : children.values()) {
264 if(child instanceof ParentNode<?>) {
265 ((ParentNode<?>)child).setPropertyChangeListener(propertyChangeListener);
267 ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME
273 @SuppressWarnings("unchecked")
275 public void cleanup() {
277 if (children != DISPOSED_CHILDREN) {
278 synchronized(children) {
279 for(T child : children.values()) {
280 ((INode)child).cleanup();
281 ((INode)child).setParent(null);
284 children = DISPOSED_CHILDREN;
287 rootNodeCache = DISPOSED;
292 public void delete() {
297 synchronized (children) {
298 // 1. Add children under parent
299 parent.appendChildren(children);
305 // 3. Remove this node from parent
306 parent.unlinkChild(this);
308 // 4. Cleanup, this node is now disposed
313 * Helper method for delete()
316 @SuppressWarnings("unchecked")
317 protected void appendChildren(Map<String, ?> children) {
318 synchronized(this.children) {
319 for(String id : children.keySet()) {
320 INode child = (INode)children.get(id);
321 this.children.put(id, (T)child); // Hopefully cast works
322 child.setParent(this);
324 // Send notify only if we are on server side (or standalone)
325 if (propertyChangeListener != null && location.equals(Location.LOCAL)) {
326 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", null, EXISTING)); // "children" is a special field name
332 @SuppressWarnings("unchecked")
333 protected void appendChild(String id, INode child) {
334 children.put(id, (T)child);
335 child.setParent(this);
336 // Send notify only if we are on server side (or standalone)
337 if (propertyChangeListener != null && location.equals(Location.LOCAL)) {
338 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+(child).getId()+"]", null, EXISTING)); // "children" is a special field name
343 * Same as removeNode, but does not perform cleanup for the child
346 protected void unlinkChild(INode child) {
347 synchronized(children) {
349 // FIXME: damn slow, needs more data structure to be supported well
350 // One option would be to store the id<->node mappings in a BidiMap.
351 for(String tmp : children.keySet()) {
352 if(children.get(tmp).equals(child)) {
357 if(id == null) return;
362 if(propertyChangeListener != null && location.equals(Location.LOCAL)) {
363 propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), UNLINK)); // "children" is a special field name
368 public String toString() {
369 return super.toString() + " [#child=" + children.size() + "]";
373 public ParentNode<?> getRootNode() {
374 // Note: using double-checked locking idiom with volatile keyword.
375 // Works with Java 1.5+
376 ParentNode<?> result = rootNodeCache;
377 if (result == DISPOSED)
380 if (result == null) {
381 synchronized (this) {
382 result = rootNodeCache;
383 if (result == null) {
384 if (parent != null) {
385 result = parent.getRootNode();
386 rootNodeCache = result;