/******************************************************************************* * 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 java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import gnu.trove.map.TLongObjectMap; import gnu.trove.map.hash.TLongObjectHashMap; /** * 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#"; /** * 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(); } }; private static class ImmutableIdMap extends TLongObjectHashMap { private static final String MSG = "immutable singleton instance"; @Override public String put(long key, String value) { throw new UnsupportedOperationException(MSG); } @Override public void putAll(Map map) { throw new UnsupportedOperationException(MSG); } @Override public void putAll(TLongObjectMap map) { throw new UnsupportedOperationException(MSG); } @Override public String putIfAbsent(long key, String value) { throw new UnsupportedOperationException(MSG); } } /** * This is the value given to {@link #children} when this node is disposed and * cleaned up. */ private static final Map DISPOSED_CHILDREN = Collections.emptyMap(); /** * This is the value given to {@link #childrenIdMap} when this node is disposed * and cleaned up. */ private static final TLongObjectMap DISPOSED_CHILDREN_ID_MAP = new ImmutableIdMap(); protected transient Map children = createChildMap(); private transient TLongObjectMap childrenIdMap = new TLongObjectHashMap<>(); /** * 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 createChildMap(1); } protected Map createChildMap(int initialCapacity) { // With JDK 1.8 HashMap is faster than Trove return new HashMap<>(initialCapacity); } public final TC addNode(Class a) { return addNode(java.util.UUID.randomUUID().toString(), a); } public TC addNode(String id, TC child) { return addNodeInternal(id, child, true, true); } private TC addNodeInternal(String id, TC child, boolean init, boolean addToChildren) { child.setParent(this); if (addToChildren) children.put(id, child); childrenIdMap.put(child.getId(), id); if (init) child.init(); childrenChanged(); return (TC)child; } public TC attachNode(String id, TC child) { return addNodeInternal(id, child, false, true); } @SuppressWarnings("unchecked") public TC detachNode(String id) { INode child = children.remove(id); if (child == null) return null; childrenIdMap.remove(child.getId()); child.setParent(null); childrenChanged(); return (TC) child; } public TC addNode(String id, Class a) { return addNodeInternal0(id, a, true); } @SuppressWarnings("unchecked") private TC addNodeInternal0(String id, Class a, boolean addToChildren) { // 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); } return (TC) addNodeInternal(id, child, true, addToChildren); } @SuppressWarnings("unchecked") public final TC getOrAttachNode(String id, TC a) { return (TC) children.computeIfAbsent(id, key -> { return (T) addNodeInternal(id, a, false, false); }); } @SuppressWarnings("unchecked") public final TC getOrCreateNode(String id, Class a) { return (TC) children.computeIfAbsent(id, key -> { return (T) addNodeInternal0(id, a, false); }); } public final void removeNode(String id) { INode child = children.remove(id); if (child != null) removeNodeInternal(child, true); } public final void removeNode(INode child) { String key = childrenIdMap.get(child.getId()); removeNode(key); } /** * This method removes and disposes all children of the node (and their * children). */ public final void removeNodes() { boolean changed = children.size() > 0; children.forEach((id, child) -> removeNodeInternal(child, false)); children.clear(); childrenIdMap.clear(); if (changed) childrenChanged(); } private void removeNodeInternal(INode child, boolean triggerChildrenChanged) { if (child != null) { if (child instanceof ParentNode) { ((ParentNode) child).removeNodes(); } child.cleanup(); child.setParent(null); if (triggerChildrenChanged) childrenChanged(); triggerPropertyChangeEvent(child); } } /** * 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); @SuppressWarnings("unchecked") public T getNode(String id) { return (T) 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 */ @SuppressWarnings("unchecked") public Collection getNodes() { return children.isEmpty() ? Collections.emptyList() : (Collection) children.values(); } public int getNodeCount() { return children.size(); } /** * Recursively set the PropertyChangeListener * * @param propertyChangeListener */ public void setPropertyChangeListener(PropertyChangeListener propertyChangeListener) { this.propertyChangeListener = propertyChangeListener; children.forEach((id, child) -> { if(child instanceof ParentNode) { ((ParentNode)child).setPropertyChangeListener(propertyChangeListener); } else { ((Node)child).propertyChangeListener = propertyChangeListener; // FIXME } }); } @Override public void cleanup() { retractMapping(); if (children != DISPOSED_CHILDREN) { children.forEach((id, child) -> { child.cleanup(); child.setParent(null); }); children.clear(); childrenIdMap.clear(); children = DISPOSED_CHILDREN; childrenIdMap = DISPOSED_CHILDREN_ID_MAP; childrenChanged(); rootNodeCache = DISPOSED; } } @Override public void delete() { if(parent == null) { return; } // 1. Add children under parent parent.appendChildren(children); // 2. Clear child maps to prevent cleanup from deleting them in step 4. children.clear(); childrenIdMap.clear(); // 3. Remove this node from parent parent.unlinkChild(this); // 4. Cleanup, this node is now disposed cleanup(); } /** * Helper method for delete() * @param children */ protected void appendChildren(Map children) { children.forEach((key, value) -> { appendChildInternal(key, value); }); } protected void appendChild(String id, INode child) { appendChildInternal(id, child); } private void appendChildInternal(String id, INode child) { children.put(id, child); childrenIdMap.put(child.getId(), id); child.setParent(this); triggerPropertyChangeEvent(child); } /** * Same as removeNode, but does not perform cleanup for the child * @param child */ protected void unlinkChild(INode child) { String id = childrenIdMap.remove(child.getId()); if(id != null) { children.remove(id); childrenChanged(); triggerPropertyChangeEvent(child); } } private void triggerPropertyChangeEvent(INode child) { // 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()+"]", 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; } }