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
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.scenegraph;
14 import gnu.trove.map.hash.THashMap;
\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
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
28 public abstract class ParentNode<T extends INode> extends Node {
\r
30 private static final long serialVersionUID = 8519410262849626534L;
\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
36 protected static final String[] EMPTY_STRING_ARRAY = {};
\r
38 @SuppressWarnings("rawtypes")
\r
39 private static final Map DISPOSED_CHILDREN = Collections.emptyMap();
\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
46 protected static final ParentNode<?> DISPOSED = new ParentNode<INode>() {
\r
47 private static final long serialVersionUID = 6155494069158034123L;
\r
49 public void asyncRemoveNode(INode node) {
\r
54 protected transient Map<String, T> children = createChildMap();
\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
65 protected transient volatile ParentNode<?> rootNodeCache;
\r
67 protected Map<String, T> createChildMap() {
\r
68 return new THashMap<String, T>(1);
\r
71 public final <TC> TC addNode(Class<TC> a) {
72 return addNode(java.util.UUID.randomUUID().toString(), a);
75 @SuppressWarnings("unchecked")
\r
76 public <TC extends INode> TC addNode(String id, TC child) {
\r
78 child.setParent(this);
\r
79 children.put(id, (T)child);
\r
87 @SuppressWarnings("unchecked")
\r
88 public <TC extends INode> TC attachNode(String id, TC child) {
\r
90 child.setParent(this);
\r
91 children.put(id, (T)child);
\r
99 @SuppressWarnings("unchecked")
\r
100 public <TC extends INode> TC detachNode(String id) {
\r
101 T child = children.remove(id);
\r
104 child.setParent(null);
\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");
115 INode child = null;
\r
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")
\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
138 return attachNode(id, a);
\r
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
147 return addNode(id, a);
150 public final void removeNode(String id) {
\r
151 INode child = null;
\r
152 synchronized (children) {
153 child = children.remove(id);
\r
155 if (child != null) {
\r
156 if (child instanceof ParentNode<?>) {
\r
157 ((ParentNode<?>) child).removeNodes();
\r
160 child.setParent(null);
\r
162 if (propertyChangeListener != null) {
\r
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;
180 children.remove(id);
\r
183 if (child instanceof ParentNode<?>) {
\r
184 ((ParentNode<?>) child).removeNodes();
\r
187 child.setParent(null);
\r
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
\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
206 if (child instanceof ParentNode<?>) {
\r
207 ((ParentNode<?>) child).removeNodes();
\r
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
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
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
\r
237 * state which must not be directly modified by client code!
\r
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();
\r
250 public int getNodeCount() {
251 return children.size();
\r
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")
\r
275 public void cleanup() {
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);
284 children = DISPOSED_CHILDREN;
287 rootNodeCache = DISPOSED;
\r
292 public void delete() {
\r
293 if(parent == null) {
\r
297 synchronized (children) {
\r
298 // 1. Add children under parent
\r
299 parent.appendChildren(children);
\r
301 // 2. Clear children
\r
305 // 3. Remove this node from parent
\r
306 parent.unlinkChild(this);
\r
308 // 4. Cleanup, this node is now disposed
\r
313 * Helper method for delete()
\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
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
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
343 * Same as removeNode, but does not perform cleanup for the child
\r
346 protected void unlinkChild(INode child) {
\r
347 synchronized(children) {
\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
357 if(id == null) return;
\r
358 children.remove(id);
\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
368 public String toString() {
369 return super.toString() + " [#child=" + children.size() + "]";
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
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