*******************************************************************************/
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.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.
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
}
};
- protected transient Map<String, T> children = createChildMap();
+ 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
*/
protected transient volatile ParentNode<?> rootNodeCache;
- protected Map<String, T> createChildMap() {
- return new THashMap<String, T>(1);
+ 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);
}
- @SuppressWarnings("unchecked")
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);
- children.put(id, (T)child);
+ if (addToChildren)
+ children.put(id, child);
+
+ childrenIdMap.put(child.getId(), id);
- child.init();
+ if (init)
+ child.init();
childrenChanged();
return (TC)child;
-
}
- @SuppressWarnings("unchecked")
public <TC extends INode> TC attachNode(String id, TC child) {
-
- child.setParent(this);
- children.put(id, (T)child);
-
- child.attach();
- childrenChanged();
- return (TC)child;
-
+ return addNodeInternal(id, child, false, true);
}
@SuppressWarnings("unchecked")
public <TC extends INode> TC detachNode(String id) {
- T child = children.remove(id);
+ INode child = children.remove(id);
if (child == null)
return null;
+ childrenIdMap.remove(child.getId());
child.setParent(null);
childrenChanged();
return (TC) child;
}
- @SuppressWarnings("unchecked")
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");
} 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;
+ return (TC) addNodeInternal(id, child, true, addToChildren);
}
@SuppressWarnings("unchecked")
public final <TC extends INode> TC getOrAttachNode(String id, TC a) {
- synchronized (children) {
- if (children.containsKey(id))
- return (TC) children.get(id);
- }
- return attachNode(id, a);
+ return (TC) children.computeIfAbsent(id, key -> {
+ return (T) addNodeInternal(id, a, false, false);
+ });
}
@SuppressWarnings("unchecked")
public final <TC> TC getOrCreateNode(String id, Class<TC> 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
- }
- }
+ 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);
-
- if (propertyChangeListener != null) {
- propertyChangeListener.propertyChange(new PropertyChangeEvent(this, "children["+child.getId()+"]", child.getClass(), NULL)); // "children" is a special field name
- }
+ String key = childrenIdMap.get(child.getId());
+ removeNode(key);
}
/**
* 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);
}
}
public abstract void asyncRemoveNode(INode node);
+ @SuppressWarnings("unchecked")
public T getNode(String id) {
- return children.get(id);
+ return (T) children.get(id);
}
/**
/**
* @return the collection of this node's children in an unspecified order
*/
+ @SuppressWarnings("unchecked")
public Collection<T> getNodes() {
- return children.values();
+ return children.isEmpty() ? Collections.emptyList() : (Collection<T>) children.values();
}
public int getNodeCount() {
*/
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();
- }
+ children.forEach((id, child) -> {
+ child.cleanup();
+ child.setParent(null);
+ });
+ children.clear();
+ childrenIdMap.clear();
+ children = DISPOSED_CHILDREN;
+ childrenIdMap = DISPOSED_CHILDREN_ID_MAP;
+ childrenChanged();
rootNodeCache = DISPOSED;
}
}
return;
}
- synchronized (children) {
- // 1. Add children under parent
- parent.appendChildren(children);
+ // 1. Add children under parent
+ parent.appendChildren(children);
- // 2. Clear children
- children.clear();
- }
+ // 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);
* Helper method for delete()
* @param children
*/
- @SuppressWarnings("unchecked")
- protected void appendChildren(Map<String, ?> 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
- }
- }
- }
+ protected void appendChildren(Map<String, INode> children) {
+ children.forEach((key, value) -> {
+ appendChildInternal(key, value);
+ });
}
- @SuppressWarnings("unchecked")
protected void appendChild(String id, INode child) {
- children.put(id, (T)child);
+ appendChildInternal(id, child);
+ }
+
+ private void appendChildInternal(String id, INode child) {
+ children.put(id, child);
+ childrenIdMap.put(child.getId(), id);
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
- }
+ triggerPropertyChangeEvent(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;
+ String id = childrenIdMap.remove(child.getId());
+ if(id != null) {
children.remove(id);
childrenChanged();
+ triggerPropertyChangeEvent(child);
}
+ }
- if(propertyChangeListener != null && location.equals(Location.LOCAL)) {
+ 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
}
}