/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.scenegraph; import gnu.trove.map.hash.THashMap; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Collections; import java.util.Map; /** * Base class of all scene graph nodes which can have a set of sub-nodes * (children). This class only provides support for unordered children. * * @param */ public abstract class ParentNode extends Node { private static final long serialVersionUID = 8519410262849626534L; public static final String EXISTING = "#EXISTING#"; public static final String UNLINK = "#UNLINK#"; public static final String NULL = "#NULL#"; protected static final String[] EMPTY_STRING_ARRAY = {}; @SuppressWarnings("rawtypes") private static final Map DISPOSED_CHILDREN = Collections.emptyMap(); /** * A value used for {@link #rootNodeCache} to indicate the node has been * disposed and the root node cache is to be considered indefinitely * invalid. */ protected static final ParentNode DISPOSED = new ParentNode() { private static final long serialVersionUID = 6155494069158034123L; @Override public void asyncRemoveNode(INode node) { throw new Error(); } }; protected transient Map children = createChildMap(); /** * A cached value for the root node of this parent node. This makes it * possible to optimize {@link #getRootNode()} so that not all invocations * go through the whole node hierarchy to find the root. This helps in * making {@link ILookupService} located in the root node perform better and * therefore be more useful. * * @see #DISPOSED */ protected transient volatile ParentNode rootNodeCache; protected Map createChildMap() { return new THashMap(1); } public final TC addNode(Class a) { return addNode(java.util.UUID.randomUUID().toString(), a); } @SuppressWarnings("unchecked") public TC addNode(String id, TC child) { child.setParent(this); children.put(id, (T)child); child.init(); childrenChanged(); return (TC)child; } @SuppressWarnings("unchecked") public TC attachNode(String id, TC child) { child.setParent(this); children.put(id, (T)child); child.attach(); childrenChanged(); return (TC)child; } @SuppressWarnings("unchecked") public TC detachNode(String id) { T child = children.remove(id); if (child == null) return null; child.setParent(null); childrenChanged(); return (TC) child; } @SuppressWarnings("unchecked") public TC addNode(String id, Class a) { // a must be extended from Node if(!Node.class.isAssignableFrom(a)) { throw new IllegalArgumentException(a + " is not extended from org.simantics.scenegraph.Node"); } INode child = null; try { child = (Node) a.newInstance(); } catch (InstantiationException e) { throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e); } catch (IllegalAccessException e) { throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e); } child.setParent(this); children.put(id, (T)child); child.init(); childrenChanged(); return (TC)child; } @SuppressWarnings("unchecked") public final TC getOrAttachNode(String id, TC a) { synchronized (children) { if (children.containsKey(id)) return (TC) children.get(id); } return attachNode(id, a); } @SuppressWarnings("unchecked") public final TC getOrCreateNode(String id, Class a) { synchronized (children) { if (children.containsKey(id)) return (TC) children.get(id); } return addNode(id, a); } public final void removeNode(String id) { INode child = null; synchronized (children) { child = children.remove(id); } if (child != null) { if (child instanceof ParentNode) { ((ParentNode) child).removeNodes(); } child.cleanup(); child.setParent(null); childrenChanged(); if (propertyChangeListener != null) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name } } } public final void removeNode(INode child) { synchronized (children) { String id = null; // FIXME: damn slow, needs more data structure to be supported well // One option would be to store the id<->node mappings in a BidiMap. for (String tmp : children.keySet()) { if (children.get(tmp).equals(child)) { id = tmp; break; } } if(id == null) return; children.remove(id); childrenChanged(); } if (child instanceof ParentNode) { ((ParentNode) child).removeNodes(); } child.cleanup(); child.setParent(null); if (propertyChangeListener != null) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name } } /** * This method removes and disposes all children of the node (and their * children). */ public final void removeNodes() { synchronized (children) { boolean changed = false; String[] keys = children.keySet().toArray(EMPTY_STRING_ARRAY); for (String key : keys) { INode child = children.remove(key); if (child != null) { changed = true; if (child instanceof ParentNode) { ((ParentNode) child).removeNodes(); } child.cleanup(); child.setParent(null); if (propertyChangeListener != null) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name } } } if (changed) childrenChanged(); } } /** * Invoked every time the set of child changes for a {@link ParentNode}. * Extending implementations may override to perform their own actions, such * as invalidating possible caches. */ protected void childrenChanged() { } public abstract void asyncRemoveNode(INode node); public T getNode(String id) { return children.get(id); } /** * @return the IDs of the child nodes as a collection. Returns internal * state which must not be directly modified by client code! */ public Collection getNodeIds() { return children.keySet(); } /** * @return the collection of this node's children in an unspecified order */ public Collection getNodes() { return children.values(); } public int getNodeCount() { return children.size(); } /** * Recursively set the PropertyChangeListener * * @param propertyChangeListener */ public void setPropertyChangeListener(PropertyChangeListener propertyChangeListener) { this.propertyChangeListener = propertyChangeListener; synchronized(children) { for(T t : children.values()) { INode child = t; if(child instanceof ParentNode) { ((ParentNode)child).setPropertyChangeListener(propertyChangeListener); } else { ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME } } } } @SuppressWarnings("unchecked") @Override public void cleanup() { retractMapping(); if (children != DISPOSED_CHILDREN) { synchronized(children) { for(T child : children.values()) { ((INode)child).cleanup(); ((INode)child).setParent(null); } children.clear(); children = DISPOSED_CHILDREN; childrenChanged(); } rootNodeCache = DISPOSED; } } @Override public void delete() { if(parent == null) { return; } synchronized (children) { // 1. Add children under parent parent.appendChildren(children); // 2. Clear children children.clear(); } // 3. Remove this node from parent parent.unlinkChild(this); // 4. Cleanup, this node is now disposed cleanup(); } /** * Helper method for delete() * @param children */ @SuppressWarnings("unchecked") protected void appendChildren(Map children) { synchronized(this.children) { for(String id : children.keySet()) { INode child = (INode)children.get(id); this.children.put(id, (T)child); // Hopefully cast works child.setParent(this); // Send notify only if we are on server side (or standalone) if (propertyChangeListener != null && location.equals(Location.LOCAL)) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", null, EXISTING)); // "children" is a special field name } } } } @SuppressWarnings("unchecked") protected void appendChild(String id, INode child) { children.put(id, (T)child); child.setParent(this); // Send notify only if we are on server side (or standalone) if (propertyChangeListener != null && location.equals(Location.LOCAL)) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+(child).getId()+"]", null, EXISTING)); // "children" is a special field name } } /** * Same as removeNode, but does not perform cleanup for the child * @param child */ protected void unlinkChild(INode child) { synchronized(children) { String id = null; // FIXME: damn slow, needs more data structure to be supported well // One option would be to store the id<->node mappings in a BidiMap. for(String tmp : children.keySet()) { if(children.get(tmp).equals(child)) { id = tmp; break; } } if(id == null) return; children.remove(id); childrenChanged(); } if(propertyChangeListener != null && location.equals(Location.LOCAL)) { propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), UNLINK)); // "children" is a special field name } } @Override public String toString() { return super.toString() + " [#child=" + children.size() + "]"; } @Override public ParentNode getRootNode() { // Note: using double-checked locking idiom with volatile keyword. // Works with Java 1.5+ ParentNode result = rootNodeCache; if (result == DISPOSED) return null; if (result == null) { synchronized (this) { result = rootNodeCache; if (result == null) { if (parent != null) { result = parent.getRootNode(); rootNodeCache = result; } } } } return result; } }