X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.scenegraph%2Fsrc%2Forg%2Fsimantics%2Fscenegraph%2FParentNode.java;h=510bbabb096ff7534f32ad56cfd37b5549058e4b;hb=e8b96542ea305825ae4dbf7c35f11275689ef564;hp=36efc8dc01252e0a79b870e1b667e76a9127eebd;hpb=969bd23cab98a79ca9101af33334000879fb60c5;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java index 36efc8dc0..510bbabb0 100644 --- a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java +++ b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/ParentNode.java @@ -1,118 +1,156 @@ -/******************************************************************************* - * 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 - *******************************************************************************/ +/******************************************************************************* + * 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); - } - +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); } - - @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; - } - + + 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; + INode child = null; try { child = (Node) a.newInstance(); } catch (InstantiationException e) { @@ -120,122 +158,80 @@ public abstract class ParentNode extends Node { } catch (IllegalAccessException e) { throw new NodeException("Node " + Node.getSimpleClassName(a) + " instantiation failed, see exception for details.", e); } + return (TC) addNodeInternal(id, child, true, addToChildren); + } - child.setParent(this); - children.put(id, (T)child); - - child.init(); - childrenChanged(); - return (TC)child; + @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 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); + return (TC) children.computeIfAbsent(id, key -> { + return (T) addNodeInternal0(id, a, false); + }); } - 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(String id) { + INode child = children.remove(id); + if (child != null) + removeNodeInternal(child, true); } 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); + String key = childrenIdMap.get(child.getId()); + removeNode(key); + } - 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). - */ + /** + * 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 - } - } + 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(); } - if (changed) + 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. - */ + /** + * 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 children.get(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! - */ + /** + * @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(); } @@ -243,12 +239,13 @@ public abstract class ParentNode extends Node { /** * @return the collection of this node's children in an unspecified order */ + @SuppressWarnings("unchecked") public Collection getNodes() { - return children.values(); + return children.isEmpty() ? Collections.emptyList() : (Collection) children.values(); } public int getNodeCount() { - return children.size(); + return children.size(); } /** @@ -258,137 +255,118 @@ public abstract class ParentNode extends Node { */ 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 - } + children.forEach((id, child) -> { + 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; + 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; - } - - 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 - } - } + 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; - } - + @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; + } + }