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