/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.smodel; import jetbrains.mps.logging.Logger; import jetbrains.mps.smodel.legacy.ConceptMetaInfoConverter; import jetbrains.mps.util.AbstractSequentialList; import jetbrains.mps.util.EqualUtil; import jetbrains.mps.util.InternUtil; import jetbrains.mps.util.containers.EmptyIterable; import org.apache.log4j.LogManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SAbstractConcept; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.language.SProperty; import org.jetbrains.mps.openapi.language.SReferenceLink; import org.jetbrains.mps.openapi.model.SNodeAccessUtil; import org.jetbrains.mps.openapi.model.SNodeReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import static jetbrains.mps.util.SNodeOperations.getDebugText; /** * As a tribute to legacy code, we do allow access to constant and meta-info objects of a node without read access. * It's not encouraged for a new code, though, and might change in future, that's why it's stated here and not in openapi.SNode */ public class SNode implements org.jetbrains.mps.openapi.model.SNode { private static final Logger LOG = Logger.wrap(LogManager.getLogger(SNode.class)); private static final String[] EMPTY_ARRAY = new String[0]; private static final Object USER_OBJECT_LOCK = new Object(); /** * inv: all children of a node, inclusive, have the same owner */ @NotNull private SNodeOwner myOwner = FreeFloatNodeOwner.INSTANCE; private SContainmentLink myRoleInParent; private jetbrains.mps.smodel.SReference[] myReferences = jetbrains.mps.smodel.SReference.EMPTY_ARRAY; private Object[] myProperties = null; private org.jetbrains.mps.openapi.model.SNodeId myId; private volatile Object[] myUserObjects; // key,value,key,value ; copy-on-write (!) private SConcept myConcept; //todo make final after 3.2 private SNode parent; /** * access only in firstChild()/firstChildInRole(role) */ private SNode first; private SNode next; // == null only for the last child in the list private SNode prev; // notNull, myFirstChild.myLeftSibling = the last child public SNode(@NotNull SConcept concept) { this(concept, SModel.generateUniqueId()); } public SNode(@NotNull SConcept concept, @NotNull org.jetbrains.mps.openapi.model.SNodeId id) { myConcept = concept; myId = id; } @NotNull @Override public SConcept getConcept() { // deliberately no assertCanRead(). It's constant field and meta-info. return myConcept; } @Override public boolean isInstanceOfConcept(@NotNull SAbstractConcept c) { return getConcept().isSubConceptOf(c); } @Override public void insertChildAfter(@NotNull SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child, @Nullable org.jetbrains.mps.openapi.model.SNode anchor) { if (anchor == null) { insertChildBefore(role, child, getFirstChild()); } else { insertChildBefore(role, child, anchor.getNextSibling()); } } protected final void assertCanRead() { myOwner.assertLegalRead(); } private void assertCanChange() { myOwner.assertLegalChange(); } @Override public org.jetbrains.mps.openapi.model.SNodeId getNodeId() { // deliberately no assertCanRead. It's constant field and sort of meta-info, why to constraint to read access? return myId; } @Override @NotNull public SNode getContainingRoot() { assertCanRead(); SNode current = this; while (true) { if (current.treeParent() == null) return current; current = current.treeParent(); myOwner.fireNodeRead(current, false); } } @Override public String getName() { assertCanRead(); if (getConcept().isSubConceptOf(SNodeUtil.concept_INamedConcept)) { return SNodeAccessUtil.getProperty(this, SNodeUtil.property_INamedConcept_name); } else { myOwner.fireNodeRead(this, false); return null; } } @Override final public SNode getParent() { assertCanRead(); SNode parent = treeParent(); if (parent != null) { myOwner.fireNodeRead(parent, true); } return parent; } /** * Removes child from current node. This affects only link between current node and its child, but not links in * subtree of child node. * <p/> * Differs from {@link SNode#delete()}. FIXME please explain how it differs from delete() * * @param child */ @Override public void removeChild(@NotNull org.jetbrains.mps.openapi.model.SNode child) { assertCanChange(); assert child.getParent() == this : "Can't remove a node not from it's parent node: removing " + child.getReference().toString() + " from " + getReference().toString(); final SNode wasChild = (SNode) child; final SContainmentLink wasRole = wasChild.getContainmentLink(); final SNode anchorPrev = firstChild() == wasChild ? null : wasChild.treePrevious(); final SNode anchorNext = wasChild.treeNext(); assert wasRole != null; myOwner.fireBeforeNodeRemove(this, wasRole, wasChild, anchorPrev); children_remove(wasChild); wasChild.myRoleInParent = null; SModel model = myOwner.getModel(); // FIXME what if myOwner is DetachedNodeOwner - shall we make node free-floating or leave it as detached? wasChild.detach(model == null ? myOwner : new DetachedNodeOwner(model)); myOwner.performUndoableAction(this, new RemoveChildUndoableAction(this, anchorNext, wasRole, wasChild)); myOwner.fireNodeRemove(this, wasRole, wasChild, anchorPrev); } /** * Deletes all nodes in subtree starting with current. Differs from {@link SNode#removeChild(org.jetbrains.mps.openapi.model.SNode)}. */ @Override public void delete() { assertCanChange(); SNode p = getParent(); if (p != null) { p.removeChild(this); } else if (myOwner.getModel() != null) { myOwner.getModel().removeRootNode(this); } } @Override public String getPresentation() { if (!getConcept().isValid()) { String persistentName = findProperty(SNodeUtil.property_INamedConcept_name); if (persistentName == null) { String conceptName = myConcept.getName(); persistentName = (conceptName == null ? myConcept.toString() : conceptName); } return "?" + persistentName + "?"; } return String.valueOf(SNodeUtil.getPresentation(this)); } @Override public String toString() { String s = null; try { s = findProperty(SNodeUtil.property_BaseConcept_alias); if (s == null) { s = getPresentation(); } } catch (RuntimeException t) { LOG.error(t, this); } if (s == null) { return "???"; } return s; } @NotNull @Override public SNodeReference getReference() { return new jetbrains.mps.smodel.SNodePointer(this); } @Override public Object getUserObject(Object key) { final Object[] userObjects = myUserObjects; if (userObjects == null) return null; for (int i = 0; i < userObjects.length; i += 2) { if (userObjects[i].equals(key)) { return userObjects[i + 1]; } } return null; } @Override public void putUserObject(Object key, @Nullable Object value) { synchronized (USER_OBJECT_LOCK) { if (value == null) { if (myUserObjects == null) return; for (int i = 0; i < myUserObjects.length; i += 2) { if (myUserObjects[i].equals(key)) { Object[] newarr = new Object[myUserObjects.length - 2]; if (i > 0) { System.arraycopy(myUserObjects, 0, newarr, 0, i); } if (i + 2 < myUserObjects.length) { System.arraycopy(myUserObjects, i + 2, newarr, i, newarr.length - i); } myUserObjects = newarr; break; } } if (myUserObjects.length == 0) { myUserObjects = null; } return; } if (myUserObjects == null) { myUserObjects = new Object[]{key, value}; return; } for (int i = 0; i < myUserObjects.length; i += 2) { if (myUserObjects[i].equals(key)) { Object[] newarr = new Object[myUserObjects.length]; System.arraycopy(myUserObjects, 0, newarr, 0, myUserObjects.length); newarr[i + 1] = value; myUserObjects = newarr; return; } } Object[] newarr = new Object[myUserObjects.length + 2]; System.arraycopy(myUserObjects, 0, newarr, 2, myUserObjects.length); newarr[0] = key; newarr[1] = value; myUserObjects = newarr; } } @NotNull @Override public List<SNode> getChildren() { return getChildren((SContainmentLink) null); } @NotNull @Override public List<jetbrains.mps.smodel.SReference> getReferences() { assertCanRead(); myOwner.fireNodeRead(this, true); return Arrays.asList(myReferences); } @Override public org.jetbrains.mps.openapi.model.SNode getFirstChild() { assertCanRead(); SNode child = firstChild(); if (child != null) { myOwner.fireNodeRead(child, false); } return child; } @Override public org.jetbrains.mps.openapi.model.SNode getLastChild() { assertCanRead(); SNode fc = firstChild(); if (fc == null) { return null; } SNode lc = fc.treePrevious(); if (lc != null) { myOwner.fireNodeRead(lc, false); } return lc; } @Override public SNode getPrevSibling() { assertCanRead(); SNode p = treeParent(); if (p == null) { return null; } myOwner.fireNodeRead(p, true); SNode tp = treePrevious(); SNode ps = tp.next == null ? null : tp; if (ps != null) { myOwner.fireNodeRead(ps, true); } return ps; } @Override public SNode getNextSibling() { assertCanRead(); SNode p = treeParent(); if (p == null) { return null; } myOwner.fireNodeRead(p, true); SNode tn = treeNext(); if (tn != null) { myOwner.fireNodeRead(tn, true); // although it used to send 2, not 3 notification, don't see any reason to have it different for parent and sibling } return tn; } @Override public Iterable<Object> getUserObjectKeys() { assertCanRead(); final Object[] userObjects = myUserObjects; if (userObjects == null || userObjects.length == 0) return EmptyIterable.getInstance(); return new Iterable<Object>() { @Override public Iterator<Object> iterator() { return new Iterator<Object>() { private int myIndex = 0; @Override public boolean hasNext() { return myIndex < userObjects.length; } @Override public Object next() { myIndex += 2; return userObjects[myIndex - 2]; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } @Override public org.jetbrains.mps.openapi.model.SModel getModel() { final SModel persistenceModel = myOwner.getModel(); return persistenceModel == null ? null : persistenceModel.getModelDescriptor(); } //---------------------------------------------------------- //----------------USAGES IN REFACTORINGS ONLY--------------- //---------------------------------------------------------- public void setId(@Nullable org.jetbrains.mps.openapi.model.SNodeId id) { if (EqualUtil.equals(id, myId)) return; if (myOwner.getModel() == null) { myId = id; } else { LOG.error("can't set id to registered node " + getDebugText(this), new Throwable()); } } void attach(@NotNull SNodeOwner nodeOwner) { nodeOwner.registerNode(this); myOwner = nodeOwner; for (SReference ref : myReferences) { ref.makeIndirect(); } for (SNode child = firstChild(); child != null; child = child.treeNext()) { child.attach(nodeOwner); } } void detach(@NotNull SNodeOwner detachedOwner) { myOwner.unregisterNode(this); if (myOwner.getModel() != null && !myOwner.getModel().isUpdateMode()) { // FIXME refactor this code for (SReference ref : myReferences) { ref.makeDirect(); } } for (SNode child = firstChild(); child != null; child = child.treeNext()) { child.detach(detachedOwner); } myOwner = detachedOwner; } @NotNull /*package*/ SNodeOwner getNodeOwner() { // FIXME for consistency, shall use same approach to dispatch events from e.g. getParent(), where I use // owner of the child node (in assumption owner is identical for the whole tree) myOwner.fireNodeRead(parent, true); // and in ChildrenIterator, which I don't want to make non-static, nor don't want to pass SNodeOwner in there right now // FIXME revisit uses of this method, re-consider approach. E.g. perhaps SModel shall keep SNodeOwner instance? return myOwner; } //--------private------- // perform inner structures update, doesn't dispatch any events private void addReferenceInternal(final SReference reference) { int oldLen = myReferences.length; jetbrains.mps.smodel.SReference[] newArray = new jetbrains.mps.smodel.SReference[oldLen + 1]; System.arraycopy(myReferences, 0, newArray, 0, oldLen); newArray[oldLen] = reference; myReferences = newArray; myOwner.performUndoableAction(this, new InsertReferenceAtUndoableAction(this, reference)); } // perform inner structures update, doesn't dispatch any events private void removeReferenceInternal(final SReference ref) { int index = -1; for (int i = 0; i < myReferences.length; i++) { if (myReferences[i] == ref) { index = i; break; } } if (index == -1) { LOG.error("ref not found " + ref, new Throwable()); return; } jetbrains.mps.smodel.SReference[] newArray = new jetbrains.mps.smodel.SReference[myReferences.length - 1]; System.arraycopy(myReferences, 0, newArray, 0, index); System.arraycopy(myReferences, index + 1, newArray, index, myReferences.length - index - 1); myReferences = newArray; myOwner.performUndoableAction(this, new RemoveReferenceAtUndoableAction(this, ref)); } protected SNode firstChild() { return first; } protected SNode treePrevious() { return prev; } public SNode treeNext() { return next; } protected SNode treeParent() { return parent; } //-------------new methods working by id----------------- protected void children_insertBefore(SNode anchor, @NotNull SNode node) { //be sure that getFirstChild is called before any access to myFirstChild SNode firstChild = firstChild(); node.parent = this; if (firstChild == null) { assert anchor == null; first = node; node.next = null; node.prev = node; return; } if (anchor == null) { SNode lastChild = firstChild.prev; node.next = null; node.prev = lastChild; firstChild.prev = node; lastChild.next = node; return; } node.next = anchor; node.prev = anchor.prev; if (anchor != firstChild) { anchor.prev.next = node; } else { first = node; } anchor.prev = node; } protected void children_remove(@NotNull SNode node) { //be sure that getFirstChild is called before any access to myFirstChild SNode firstChild = firstChild(); if (firstChild == node) { first = node.next; if (first != null) { first.prev = node.prev; } } else { node.prev.next = node.next; if (node.next != null) { node.next.prev = node.prev; } else { firstChild.prev = node.prev; } } node.prev = node.next = null; node.parent = null; } @Override public SContainmentLink getContainmentLink() { return myRoleInParent; } @Override public boolean hasProperty(@NotNull SProperty property) { assertCanRead(); String val = findProperty(property); myOwner.firePropertyRead(this, property, val, true); return !SModelUtil_new.isEmptyPropertyValue(val); } @Override public String getProperty(@NotNull SProperty property) { assertCanRead(); String value = findProperty(property); myOwner.firePropertyRead(this, property, value, false); return value; } /** * Bare access, no notifications nor checks */ private String findProperty(SProperty property) { if (myProperties != null) { int index = getPropertyIndex(property); if (index != -1) { return (String) myProperties[index + 1]; } } return null; } @Override public void setProperty(@NotNull final SProperty property, String propertyValue) { assertCanChange(); propertyValue = InternUtil.intern(propertyValue); int index = getPropertyIndex(property); final String oldValue = index == -1 ? null : (String) myProperties[index + 1]; if (EqualUtil.equals(oldValue, propertyValue)) return; if (propertyValue == null) { Object[] oldProperties = myProperties; int newLength = oldProperties.length - 2; if (newLength == 0) { myProperties = null; } else { myProperties = new Object[newLength]; System.arraycopy(oldProperties, 0, myProperties, 0, index); System.arraycopy(oldProperties, index + 2, myProperties, index, newLength - index); } } else if (oldValue == null) { Object[] oldProperties = myProperties == null ? EMPTY_ARRAY : myProperties; myProperties = new Object[oldProperties.length + 2]; System.arraycopy(oldProperties, 0, myProperties, 0, oldProperties.length); myProperties[myProperties.length - 2] = property; myProperties[myProperties.length - 1] = propertyValue; } else { myProperties[index + 1] = propertyValue; } myOwner.performUndoableAction(this, new PropertyChangeUndoableAction(this, property, oldValue, propertyValue)); myOwner.firePropertyChange(this, property, oldValue, propertyValue); } @NotNull @Override public Iterable<SProperty> getProperties() { assertCanRead(); myOwner.fireNodeRead(this, true); if (myProperties == null) return new EmptyIterable<SProperty>(); List<SProperty> result = new ArrayList<SProperty>(myProperties.length / 2); for (int i = 0; i < myProperties.length; i += 2) { result.add((SProperty) myProperties[i]); } return result; } @Override public void setReferenceTarget(@NotNull SReferenceLink role, @Nullable org.jetbrains.mps.openapi.model.SNode target) { assertCanChange(); SReference toDelete = null; if (myReferences != null) { for (SReference reference : myReferences) { if (!reference.getLink().equals(role)) continue; toDelete = reference; break; } } if (toDelete == null && target == null) { return; } if (toDelete != null) { removeReferenceInternal(toDelete); } SReference newValue = null; if (target != null) { newValue = SReference.create(role, this, target); addReferenceInternal(newValue); } myOwner.fireReferenceChange(this, role, toDelete, newValue); } @Override public SNode getReferenceTarget(@NotNull SReferenceLink role) { assertCanRead(); SReference reference = findReference(role); SNode result = reference == null ? null : (SNode) reference.getTargetNode(); myOwner.fireReferenceRead(this, role, result); return result; } @Override public SReference getReference(@NotNull SReferenceLink role) { assertCanRead(); SReference result = findReference(role); myOwner.fireReferenceRead(this, role, null); return result; } /** * Bare access, no notifications nor checks */ private SReference findReference(@NotNull SReferenceLink role) { for (SReference reference : myReferences) { if (role.equals(reference.getLink())) { return reference; } } return null; } // FIXME odd to have role parameter, while SReference.getLink gives exactly what's needed (and doesn't violate consistency) // to clear reference, one could use setReferenceTarget(). Alternatively, SReference shall not keep // the meta-object, and query its source node for role instead (as a free-floating Reference shall not answer its SReferenceLink). @Override public void setReference(@NotNull SReferenceLink role, org.jetbrains.mps.openapi.model.SReference toAdd) { assertCanChange(); SReference toRemove = null; for (SReference r : myReferences) { if (!r.getLink().equals(role)) continue; toRemove = r; break; } if (toRemove != null) { removeReferenceInternal(toRemove); } if (toAdd != null) { assert toAdd.getSourceNode() == this; addReferenceInternal((SReference) toAdd); } myOwner.fireReferenceChange(this, role, toRemove, toAdd); } public void insertChildBefore(@NotNull final SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child, @Nullable final org.jetbrains.mps.openapi.model.SNode anchor) { assertCanChange(); final SNode schild = (SNode) child; SNode parentOfChild = schild.getParent(); if (parentOfChild != null) { final String fmt = "%s already has parent: %s\nCouldn't add it to: %s"; final String m = String.format(fmt, getDebugText(schild), getDebugText(parentOfChild), getDebugText(this)); throw new IllegalModelAccessException(m); } final SModel childModel = schild.getNodeOwner().getModel(); if (childModel != null) { if (childModel.isRoot(schild)) { final String fmt = "Attempt to add root %s from model %s to node %s."; throw new IllegalModelAccessException(String.format(fmt, getDebugText(schild), childModel, getDebugText(this))); } else { final String fmt = "Node to add (%s) belongs to a model. Couldn't add it to %s. Shall detach it/remove from the model %s first."; throw new IllegalModelAccessException(String.format(fmt, getDebugText(schild), getDebugText(this), childModel)); } } if (getContainingRoot() == child) { throw new IllegalModelAccessException("Trying to create a cyclic tree"); } if (anchor != null) { if (anchor.getParent() != this) { throw new IllegalModelAccessException( "anchor is not a child of this node" + " | " + "this: " + getDebugText(this) + " | " + "anchor: " + getDebugText(anchor) ); } } schild.myRoleInParent = role; children_insertBefore(((SNode) anchor), schild); myOwner.startUndoTracking(this, schild); schild.attach(myOwner); myOwner.performUndoableAction(this, new InsertChildAtUndoableAction(this, anchor, role, child)); myOwner.fireNodeAdd(this, role, schild, (SNode) anchor); } @Override public void addChild(@NotNull SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child) { insertChildBefore(role, child, null); } @Override @NotNull public List<SNode> getChildren(SContainmentLink role) { SNode firstChild = firstChild(); if (role != null) { while (firstChild != null && !role.equals(firstChild.getContainmentLink())) { firstChild = firstChild.treeNext(); } } if (firstChild == null) { return Collections.emptyList(); } return new ChildrenList(firstChild, role); } private int getPropertyIndex(SProperty id) { if (myProperties == null) return -1; for (int i = 0; i < myProperties.length; i += 2) { if (id.equals(myProperties[i])) return i; } return -1; } @Deprecated @Override public String getRoleInParent() { SContainmentLink cl = getContainmentLink(); if (cl == null) return null; return cl.getRoleName(); } @Deprecated @Override public final boolean hasProperty(String propertyName) { return hasProperty(convertToProp(propertyName)); } @Deprecated @Override public final String getProperty(String propertyName) { return getProperty(convertToProp(propertyName)); } @Deprecated @Override public void setProperty(String propertyName, String propertyValue) { SProperty prop = convertToProp(propertyName); setProperty(prop, propertyValue); } @Deprecated @Override public Collection<String> getPropertyNames() { List<String> res = new ArrayList<String>(myProperties == null ? 0 : myProperties.length / 2); for (SProperty p : getProperties()) { res.add(p.getName()); } return res; } @Deprecated @Override public void setReferenceTarget(String role, @Nullable org.jetbrains.mps.openapi.model.SNode target) { setReferenceTarget(convertToRef(role), target); } @Deprecated @Override public SNode getReferenceTarget(String role) { return getReferenceTarget(convertToRef(role)); } @Deprecated @Override public SReference getReference(String role) { return getReference(convertToRef(role)); } @Deprecated @Override public void setReference(String role, @Nullable org.jetbrains.mps.openapi.model.SReference reference) { setReference(convertToRef(role), reference); } @Deprecated public void insertChildBefore(@NotNull String role, org.jetbrains.mps.openapi.model.SNode child, @Nullable final org.jetbrains.mps.openapi.model.SNode anchor) { insertChildBefore(convertToLink(role), child, anchor); } @Deprecated @Override public void addChild(String role, org.jetbrains.mps.openapi.model.SNode child) { insertChildBefore(role, child, null); } @Deprecated @Override @NotNull public List<SNode> getChildren(String role) { return getChildren(convertToLink(role)); } @NotNull private SContainmentLink convertToLink(String role) { return ((ConceptMetaInfoConverter) myConcept).convertAggregation(role); } @NotNull private SReferenceLink convertToRef(String role) { return ((ConceptMetaInfoConverter) myConcept).convertAssociation(role); } @NotNull private SProperty convertToProp(String name) { return ((ConceptMetaInfoConverter) myConcept).convertProperty(name); } private static final class AlreadyConstructedException extends RuntimeException { public AlreadyConstructedException(@NotNull SNode node) { super("The node " + node + " has already been constructed."); } } private static class ChildrenList extends AbstractSequentialList<SNode> { @Nullable private final SContainmentLink myRole; public ChildrenList(SNode first, @Nullable SContainmentLink role) { super(first); myRole = role; } @Override protected AbstractSequentialIterator<SNode> createIterator(SNode first) { return new ChildrenIterator(first, myRole); } @NotNull @Override public List<SNode> subList(int fromIndex, int toIndex) { if (fromIndex < toIndex) { return new ChildrenList(get(fromIndex), myRole); } else { return Collections.emptyList(); } } private class ChildrenIterator extends AbstractSequentialIterator<SNode> { @Nullable private final SContainmentLink myRole; public ChildrenIterator(@NotNull SNode first, @Nullable SContainmentLink role) { super(first); myRole = role; } @Override protected SNode getNext(SNode node) { node.assertCanRead(); if (myRole == null) { return node.treeNext(); } do { node = node.treeNext(); } while (node != null && !myRole.equals(node.getContainmentLink())); return node; } @Override protected SNode getPrev(SNode node) { node.assertCanRead(); if (node.treeParent() == null) { return null; } SNode fc = node.treeParent().firstChild(); if (node == fc) { return null; } if (myRole == null) { return node.treePrevious(); } do { node = node.treePrevious(); } while (node != fc && !myRole.equals(node.getContainmentLink())); return myRole.equals(node.getContainmentLink()) ? node : null; } @Override public SNode next() { final SNode node = super.next(); if (node != null) { node.getNodeOwner().fireNodeRead(node, true); } return node; } } } }