/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.ui.debug; import static org.andork.reflect.ReflectionUtils.format; import static org.andork.reflect.ReflectionUtils.getComponentType; import static org.andork.reflect.ReflectionUtils.getRawType; import static org.andork.reflect.ReflectionUtils.getSupertypeParameters; import static org.andork.reflect.ReflectionUtils.getTypeParameterOrObject; import static org.andork.reflect.ReflectionUtils.isAssignableFrom; import static org.andork.reflect.ReflectionUtils.parameterize; import static org.andork.reflect.ReflectionUtils.resolveType; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreePath; import org.andork.collect.CollectionUtils; import org.andork.reflect.DefaultTypeFormatter; import org.andork.reflect.ReflectionUtils; import org.andork.reflect.TypeFormatter; import org.jdesktop.swingx.treetable.TreeTableModel; public class ObjectTreeTableModel implements TreeTableModel { public class ArrayAspect extends Aspect implements ListlikeAspect { public ArrayAspect(Node node) { super(node); } @Override public void addNewElement(int index, Object newElement) throws Exception { Class<?> componentType = node.storage.getStorageClass().getComponentType(); Object newArray = Array.newInstance(componentType, node.getChildCount() + 1); System.arraycopy(node.value, 0, newArray, 0, index); Array.set(newArray, index, newElement); System.arraycopy(node.value, index, newArray, index + 1, node.getChildCount() - index); node.setValue(newArray); } @Override public boolean canAddOrRemoveChildren() { return node.value != null && node.storage.isMutable(); } @Override protected void expand() { if (node.childNodes == null && node.value != null) { node.childNodes = new ArrayList<Node>(); for (int i = 0; i < Array.getLength(node.value); i++) { Object element = Array.get(node.value, i); Type resolvedType = getComponentType(node.storage.resolvedType); node.childNodes.add(new Node(node, element, new ArrayElementStorage(resolvedType, i))); } } } @Override protected void onValueChanged() { node.reexpand(); } @Override public void removeElement(int index) { Object newArray = Array.newInstance(node.storage.getStorageClass() .getComponentType(), node.getChildCount() - 1); System.arraycopy(node.value, 0, newArray, 0, index); System.arraycopy(node.value, index + 1, newArray, index, node.getChildCount() - index - 1); node.setValue(newArray); } } public class ArrayElementStorage extends ElementStorage { protected ArrayElementStorage(Type resolvedType, int index) { super(resolvedType, index); } @Override public boolean isMutable() { return true; } @Override public void store(Node node) { Array.set(node.parent.value, index, node.value); } @Override public String toString(Node node, TypeFormatter typeFormatter) { return "[" + index + "]: " + typeFormatter.format(resolvedType); } } public abstract class Aspect { protected final Node node; protected Aspect(Node node) { super(); this.node = node; } protected abstract void expand(); public boolean isEditable() { return false; } public boolean isLeaf() { return false; } protected abstract void onValueChanged(); } public abstract class ElementStorage extends Storage { protected final int index; protected ElementStorage(Type resolvedType, int index) { super(resolvedType); this.index = index; } public int getIndex() { return index; } } protected static class Entry<K, V> implements Map.Entry<K, V> { K key; V value; protected Entry(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; } @Override public V setValue(V value) { V result = this.value; this.value = value; return result; } } public class EntryAspect extends Aspect { public EntryAspect(Node node) { super(node); } @Override protected void expand() { if (node.childNodes == null) { Map.Entry<?, ?> entry = (Map.Entry<?, ?>) node.value; node.childNodes = new ArrayList<Node>(); Type resolvedKeyType = getTypeParameterOrObject(node.storage.resolvedType, 0); Type resolvedValueType = getTypeParameterOrObject(node.storage.resolvedType, 1); node.childNodes .add(new Node(node, entry.getKey(), new MapEntryKeyStorage(resolvedKeyType, entry.getKey()))); node.childNodes.add(new Node(node, entry.getValue(), new MapEntryValueStorage(resolvedValueType))); } } public Node getKeyNode() { return node.childNodes.get(0); } public Node getValueNode() { return node.childNodes.get(1); } @Override protected void onValueChanged() { } } private static class FieldNameComparator implements Comparator<Field> { @Override public int compare(Field o1, Field o2) { return o1.getName().compareTo(o2.getName()); } } public class FieldStorage extends Storage { final Field field; public FieldStorage(Type resolvedType, Field field) { super(resolvedType); this.field = field; } @Override public boolean isMutable() { return !Modifier.isFinal(field.getModifiers()); } @Override public void store(Node node) { try { field.set(node.parent.value, node.value); } catch (Exception e) { e.printStackTrace(); } } @Override public String toString(Node node, TypeFormatter typeFormatter) { return field.getName() + ": " + typeFormatter.format(resolvedType); } } // static { // ConsoleHandler ch = new ConsoleHandler(); // ch.setLevel(Level.FINE); // LOGGER.addHandler(ch); // LOGGER.setLevel(Level.FINE); // } public class LeafAspect extends Aspect { public LeafAspect(Node node) { super(node); } @Override protected void expand() { } @Override public boolean isEditable() { return isEditableLeafType(node.getStorageClass()) || node.getValue() != null && isEditableLeafType(node.getValue().getClass()); } @Override public boolean isLeaf() { return true; } @Override protected void onValueChanged() { if (node.getStorage() instanceof RootStorage) { setRoot(node.getValue()); } else { nodesChanged(node); } } } public class ListAspect extends Aspect implements ListlikeAspect { public ListAspect(Node node) { super(node); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void addNewElement(int index, Object newElement) throws Exception { ((List) node.value).add(index, newElement); node.reexpand(); } @Override public boolean canAddOrRemoveChildren() { return node.value != null; } @Override protected void expand() { Type resolvedComponentType = getTypeParameterOrObject(node.storage.resolvedType, 0); if (node.childNodes == null && node.value != null) { node.childNodes = new ArrayList<Node>(); int i = 0; for (Object element : (List<?>) node.value) { node.childNodes.add(new Node(node, element, new ListElementStorage(resolvedComponentType, i++))); } } } public Class<?> getElementType() { return getRawType(getTypeParameterOrObject(node.storage.resolvedType, 0)); } @Override protected void onValueChanged() { node.reexpand(); } @Override public void removeElement(int index) { ((List<?>) node.value).remove(index); node.reexpand(); } } public class ListElementStorage extends ElementStorage { protected ListElementStorage(Type resolvedType, int index) { super(resolvedType, index); } @Override public boolean isMutable() { return true; } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void store(Node node) { ((List) node.parent.value).set(index, node.value); } @Override public String toString(Node node, TypeFormatter typeFormatter) { return "[" + index + "]: " + typeFormatter.format(resolvedType); } } public interface ListlikeAspect { public void addNewElement(int index, Object newElement) throws Exception; public boolean canAddOrRemoveChildren(); public void removeElement(int index); } public class MapAspect extends Aspect implements MaplikeAspect { public MapAspect(Node node) { super(node); } @Override public boolean canAddOrRemoveChildren() { return node.value != null; } @Override protected void expand() { Type resolvedEntryType = parameterize(Map.Entry.class, getSupertypeParameters(Map.class, node.storage.resolvedType)); if (node.childNodes == null && node.value != null) { node.childNodes = new ArrayList<Node>(); for (Map.Entry<?, ?> entry : ((Map<?, ?>) node.value).entrySet()) { node.childNodes.add(new Node(node, entry, new MapEntryStorage(resolvedEntryType))); } } } @Override public Class<?> getKeyType() { return getRawType(getTypeParameterOrObject(node.storage.resolvedType, 0)); } @Override public Class<?> getValueType() { return getRawType(getTypeParameterOrObject(node.storage.resolvedType, 1)); } @Override protected void onValueChanged() { node.reexpand(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void putNewEntry(Object key, Object value) throws Exception { Map map = (Map) node.value; if (key != null) { map.put(key, value); } Type resolvedEntryType = parameterize(Map.Entry.class, getSupertypeParameters(Map.class, node.storage.resolvedType)); Map.Entry<Object, Object> newEntry = new Entry<Object, Object>(key, value); if (node.childNodes == null) { node.childNodes = new ArrayList<Node>(); } Node newNode = new Node(node, newEntry, new MapEntryStorage(resolvedEntryType)); node.childNodes.add(newNode); nodeStructureChanged(node); } @SuppressWarnings("rawtypes") @Override public void removeEntry(Node entryNode) { int removeIndex = -1; if (node.childNodes != null) { int i = 0; for (Node child : node.childNodes) { if (child == entryNode) { removeIndex = i; break; } i++; } } if (removeIndex >= 0) { Object key = ((EntryAspect) entryNode.aspect).getKeyNode().value; if (key != null) { ((Map) node.value).remove(key); } node.childNodes.remove(removeIndex); nodeStructureChanged(node); } } @Override public void removeEntry(Object key) { ((Map<?, ?>) node.value).remove(key); int removeIndex = -1; Node removeEntry = null; if (node.childNodes != null) { int i = 0; for (Node child : node.childNodes) { Object childKey = ((EntryAspect) child.aspect).getKeyNode().value; if (key.equals(childKey)) { removeIndex = i; removeEntry = child; break; } i++; } } if (removeEntry != null) { node.childNodes.remove(removeIndex); nodesRemoved(new TreeModelEvent(ObjectTreeTableModel.this, node.getPath(), new int[] { removeIndex }, new Object[] { removeEntry })); } } } public class MapEntryKeyStorage extends Storage { Object currentKey; public MapEntryKeyStorage(Type resolvedType, Object key) { super(resolvedType); currentKey = key; } @Override public boolean isMutable() { return true; } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void store(Node node) { Node entryNode = node.getParent(); EntryAspect entryAspect = (EntryAspect) entryNode.aspect; Node mapNode = entryNode.getParent(); Map map = (Map) mapNode.value; if (currentKey != null) { map.remove(currentKey); } currentKey = entryAspect.getKeyNode().value; if (currentKey != null) { map.put(currentKey, entryAspect.getValueNode().value); } } @Override public String toString(Node node, TypeFormatter typeFormatter) { return "key: " + typeFormatter.format(resolvedType); } } public class MapEntryStorage extends Storage { protected MapEntryStorage(Type resolvedType) { super(resolvedType); } @Override public boolean isMutable() { return false; } @Override public void store(Node node) { } @Override public String toString(Node node, TypeFormatter typeFormatter) { try { return "[" + ((EntryAspect) node.aspect).getKeyNode().value + "]: " + typeFormatter.format(resolvedType); } catch (Exception e) { return format(resolvedType); } } } public class MapEntryValueStorage extends Storage { protected MapEntryValueStorage(Type resolvedType) { super(resolvedType); } @Override public boolean isMutable() { return true; } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void store(Node node) { Node entryNode = node.getParent(); EntryAspect entryAspect = (EntryAspect) entryNode.aspect; Node mapNode = entryNode.getParent(); Map map = (Map) mapNode.value; map.put(entryAspect.getKeyNode().value, entryAspect.getValueNode().value); } @Override public String toString(Node node, TypeFormatter typeFormatter) { return "value: " + typeFormatter.format(resolvedType); } } public interface MaplikeAspect { public boolean canAddOrRemoveChildren(); public abstract Class<?> getKeyType(); public abstract Class<?> getValueType(); public void putNewEntry(Object key, Object value) throws Exception; public void removeEntry(Node entryNode); public void removeEntry(Object key); } public class Node { protected final int depth; protected final Node parent; protected Object value; protected final Storage storage; protected Aspect aspect; protected boolean aspectIsFixed; List<Node> childNodes; protected Node(Node parent, Object value, Storage storage) { super(); if (storage == null) { throw new IllegalArgumentException("storage must be non-null"); } depth = parent == null ? 0 : parent.depth + 1; this.parent = parent; this.value = value; this.storage = storage; chooseAspect(); expand(); } protected void chooseAspect() { Class<?> storageClass = storage.getStorageClass(); Class<?> valueClass = value == null ? Object.class : value.getClass(); if (storage instanceof MapEntryStorage) { aspect = new EntryAspect(this); } else if (isLeafType(storageClass) || isLeafType(valueClass) || storageClass.isEnum() || valueClass.isEnum()) { aspect = new LeafAspect(this); } else if (isAssignableFrom(List.class, storage.resolvedType) || List.class.isAssignableFrom(valueClass)) { aspect = new ListAspect(this); } else if (isAssignableFrom(Map.class, storage.resolvedType) || Map.class.isAssignableFrom(valueClass)) { aspect = new MapAspect(this); } else if (storage.resolvedType instanceof GenericArrayType || storageClass.isArray() || valueClass.isArray()) { aspect = new ArrayAspect(this); } else { aspect = new ObjectAspect(this); } } private void expand() { if (!isCycle() && depth < maxDepth) { if (LOGGER.getLevel() == Level.FINE) { StringBuffer sb = new StringBuffer(); sb.append("expanding "); for (int i = 0; i < depth; i++) { sb.append('\t'); } sb.append(this); LOGGER.fine(sb.toString()); } aspect.expand(); } } public Object findAncestralInstance(Class<?> enclosing) { Node n = this; while (n != null) { if (n.value != null && enclosing.isInstance(n.value)) { return n.value; } n = n.getParent(); } return null; } private Constructor<?> findInnerClassConstructor(Class<?> type) { for (Constructor<?> constructor : type.getDeclaredConstructors()) { if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0].equals(type.getEnclosingClass())) { return constructor; } } return null; } private Constructor<?> findNullaryConstructor(Class<?> type) { for (Constructor<?> constructor : type.getDeclaredConstructors()) { if (constructor.getDeclaringClass().equals(type) && constructor.getParameterTypes().length == 0) { return constructor; } } return null; } public Aspect getAspect() { return aspect; } public Node getChild(int index) { return childNodes.get(index); } public int getChildCount() { return childNodes == null ? 0 : childNodes.size(); } public Node getParent() { return parent; } public Object[] getPath() { int count = 0; Node n = this; while (n != null) { count++; n = n.getParent(); } Object[] result = new Object[count]; n = this; for (int i = count - 1; i >= 0; i--) { result[i] = n; n = n.getParent(); } return result; } public Storage getStorage() { return storage; } public Class<?> getStorageClass() { return storage.getStorageClass(); } public Object getValue() { return value; } public int indexOf(Node child) { if (childNodes == null) { return -1; } return childNodes.indexOf(child); } private boolean isCycle() { Node parent = this.parent; while (parent != null) { if (parent.value != null && parent.value == value) { return true; } parent = parent.getParent(); } return false; } public boolean isEditable() { return aspect.isEditable(); } public boolean isLeaf() { return value == null || aspect.isLeaf(); } public boolean isMutable() { return storage.isMutable(); } public boolean isPrimitive() { return storage.getStorageClass().isPrimitive(); } public Object newInstance(Class<?> type) throws Exception { if (type.isArray()) { return Array.newInstance(type.getComponentType(), 0); } Class<?> enclosing = type.getEnclosingClass(); if (enclosing != null && !Modifier.isStatic(type.getModifiers())) { Object enclosingInstance = findAncestralInstance(enclosing); Constructor<?> constructor = findInnerClassConstructor(type); if (constructor != null) { constructor.setAccessible(true); return constructor.newInstance(enclosingInstance); } throw new RuntimeException("Couldn't find constructor for instance class"); } else { Constructor<?> constructor = findNullaryConstructor(type); if (constructor != null) { constructor.setAccessible(true); return constructor.newInstance(); } } throw new RuntimeException("Unable to instantiate " + type.getSimpleName()); } public void reexpand() { childNodes = null; expand(); nodeStructureChanged(this); } public void setValue(Object newValue) { if (!storage.canStore(newValue)) { throw new IllegalArgumentException( "Can't set value to " + newValue + ", probably because it is the wrong type."); } if (value != newValue) { value = newValue; storage.store(this); if (!aspectIsFixed) { chooseAspect(); } aspect.onValueChanged(); } } public void setValueToNewInstance() throws Exception { setValue(newInstance(storage.getStorageClass())); } @Override public String toString() { return toString(typeFormatter); } public String toString(TypeFormatter typeFormatter) { return storage.toString(this, typeFormatter); } } public class ObjectAspect extends Aspect { List<Field> fields; public ObjectAspect(Node node) { super(node); } @Override protected void expand() { if (node.childNodes == null && node.value != null) { node.childNodes = new ArrayList<Node>(); for (Field field : getFields()) { try { Object fieldValue = field.get(node.value); Type resolvedType = resolveType(field.getGenericType(), node.storage.resolvedType); node.childNodes.add(new Node(node, fieldValue, new FieldStorage(resolvedType, field))); } catch (Exception e) { e.printStackTrace(); } } } } protected List<Field> getFields() { if (node.value == null) { return Collections.emptyList(); } if (fields == null) { fields = getSortedFields(node.value.getClass()); for (Field field : fields) { field.setAccessible(true); } } return fields; } @Override protected void onValueChanged() { fields = null; node.reexpand(); } } public class RootStorage extends Storage { protected RootStorage(Type resolvedType) { super(resolvedType); } @Override public boolean canStore(Object newValue) { return true; } @Override public boolean isMutable() { return rootNodeMutable; } @Override public void store(Node node) { if (node.getValue() != null) { resolvedType = node.getValue().getClass(); } } @Override public String toString(Node node, TypeFormatter typeFormatter) { return "Root"; } } public abstract class Storage { Type resolvedType; protected Storage(Type resolvedType) { super(); if (resolvedType == null) { throw new IllegalArgumentException("resolvedType must be non-null"); } this.resolvedType = resolvedType; } public boolean canStore(Object newValue) { return newValue == null || ReflectionUtils.isFieldSettableFrom(getStorageClass(), newValue.getClass()); } public Type getResolvedType() { return resolvedType; } public Class<?> getStorageClass() { return getRawType(resolvedType); } public abstract boolean isMutable(); public abstract void store(Node node); public abstract String toString(Node node, TypeFormatter typeFormatter); } private static final Logger LOGGER = Logger.getLogger(ObjectTreeTableModel.class.getName()); private static List<Field> getSortedFields(Class<?> type) { List<Field> result = new ArrayList<Field>(); while (type != null) { for (Field field : type.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers()) && !field.isSynthetic()) { result.add(field); } } type = type.getSuperclass(); } Collections.sort(result, new FieldNameComparator()); return result; } private Node root; private final List<TreeModelListener> listeners = new ArrayList<TreeModelListener>(); private boolean rootNodeMutable; private int maxDepth = 50; protected Set<Class<?>> leafClasses; protected Set<Class<?>> editableLeafClasses; protected TypeFormatter typeFormatter = new DefaultTypeFormatter(); public ObjectTreeTableModel() { init(); } @Override public void addTreeModelListener(TreeModelListener l) { if (!listeners.contains(l)) { listeners.add(l); } } public TreeModelEvent createNodesChangedOrInsertedEvent(Node... nodes) { Node parent = null; int[] childIndices = new int[nodes.length]; parent = nodes[0].getParent(); if (parent == null) { throw new IllegalArgumentException("all nodes must have the same non-null parent"); } childIndices[0] = parent.indexOf(nodes[0]); for (int i = 1; i < nodes.length; i++) { if (nodes[i].getParent() != parent) { throw new IllegalArgumentException("all nodes must have the same parent"); } childIndices[i] = parent.indexOf(nodes[i]); } TreeModelEvent event = new TreeModelEvent(this, parent.getPath(), childIndices, nodes); return event; } @Override public Object getChild(Object parent, int index) { return ((Node) parent).getChild(index); } @Override public int getChildCount(Object parent) { return ((Node) parent).getChildCount(); } @Override public Class<?> getColumnClass(int columnIndex) { return columnIndex == 0 ? Node.class : Object.class; } @Override public int getColumnCount() { return 2; } @Override public String getColumnName(int column) { return column == 0 ? "Name" : "Value"; } @Override public int getHierarchicalColumn() { return 0; } @Override public int getIndexOfChild(Object parent, Object child) { return ((Node) parent).childNodes.indexOf(child); } @Override public Object getRoot() { return root; } public Object getRootObject() { return root == null ? null : root.getValue(); } public TypeFormatter getTypeFormatter() { return typeFormatter; } @Override public Object getValueAt(Object node, int column) { return column == 0 ? node : ((Node) node).getValue(); } protected void init() { leafClasses = CollectionUtils.<Class<?>> asHashSet( boolean.class, Boolean.class, byte.class, Byte.class, short.class, Short.class, char.class, Character.class, int.class, Integer.class, float.class, Float.class, long.class, Long.class, double.class, Double.class, BigInteger.class, BigDecimal.class, String.class, Date.class, Calendar.class); editableLeafClasses = new HashSet<Class<?>>(leafClasses); } @Override public boolean isCellEditable(Object node, int column) { return column == 1 && ((Node) node).isEditable(); } protected boolean isEditableLeafType(Type type) { return Enum.class.isAssignableFrom(getRawType(type)) || editableLeafClasses.contains(type); } @Override public boolean isLeaf(Object node) { return ((Node) node).isLeaf(); } protected boolean isLeafType(Type type) { return leafClasses.contains(type); } protected void nodesChanged(List<Node> nodes) { nodesChanged(nodes.toArray(new Node[nodes.size()])); } protected void nodesChanged(Node... nodes) { TreeModelEvent event = createNodesChangedOrInsertedEvent(nodes); for (TreeModelListener listener : listeners) { listener.treeNodesChanged(event); } } protected void nodesInserted(List<Node> nodes) { nodesInserted(nodes.toArray(new Node[nodes.size()])); } protected void nodesInserted(Node... nodes) { TreeModelEvent event = createNodesChangedOrInsertedEvent(nodes); for (TreeModelListener listener : listeners) { listener.treeNodesInserted(event); } } protected void nodesRemoved(TreeModelEvent event) { for (TreeModelListener listener : listeners) { listener.treeNodesRemoved(event); } } protected void nodeStructureChanged(Node node) { TreeModelEvent event = new TreeModelEvent(this, node == null ? null : node.getPath()); for (TreeModelListener listener : listeners) { listener.treeStructureChanged(event); } } @Override public void removeTreeModelListener(TreeModelListener l) { listeners.remove(l); } public void setRoot(Object rootObj) { if (rootObj == null) { root = null; } else { root = new Node(null, rootObj, new RootStorage(rootObj.getClass())); nodeStructureChanged(root); } } public void setRoot(Object rootObj, Type resolvedRootType) { if (rootObj == null) { root = null; } else { root = new Node(null, rootObj, new RootStorage(resolvedRootType)); nodeStructureChanged(root); } } public void setTypeFormatter(TypeFormatter typeFormatter) { this.typeFormatter = typeFormatter; } @Override public void setValueAt(Object value, Object node, int column) { if (column != 1) { throw new IllegalArgumentException("invalid column: " + column); } Node nnode = (Node) node; try { nnode.setValue(value); } catch (Exception e) { e.printStackTrace(); } } @Override public void valueForPathChanged(TreePath path, Object newValue) { Node node = (Node) path.getLastPathComponent(); node.setValue(newValue); } }