core/kernel/source/jetbrains/mps/smodel/SNode.java (763 lines of code) (raw):

/* * Copyright 2003-2025 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.extapi.model.ResolveInfoExt; import jetbrains.mps.logging.Logger; import jetbrains.mps.smodel.AssociationData.DirectNode; import jetbrains.mps.smodel.AssociationData.DynamicPtr; import jetbrains.mps.smodel.AssociationData.IndirectNodePtr; import jetbrains.mps.smodel.AssociationData.LocalNodePtr; import jetbrains.mps.smodel.AssociationData.SNodeAssociationUpdate; import jetbrains.mps.smodel.event.SModelPropertyEvent; import jetbrains.mps.util.containers.EmptyIterable; 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.ResolveInfo; import org.jetbrains.mps.openapi.model.ResolveInfo.D; import org.jetbrains.mps.openapi.model.ResolveInfo.N; import org.jetbrains.mps.openapi.model.SNodeReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; 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 * * How come this one is in [kernel], not [smodel]? */ public class SNode implements org.jetbrains.mps.openapi.model.SNode, SNodeAssociationUpdate { private static final Logger LOG = Logger.getLogger(SNode.class); private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; private static final Object USER_OBJECT_LOCK = new Object(); /** * For an attached node, owner of the node and all its children is the same, the same instance of AttachedNodeOwner. * For a detached node, owner is DetachedNodeOwner. Its children, however, may be FreeFloatNodeOwner. * For a newly created, free-floating node, owner if FreeFloatNodeOwner. Children of a free-floating node, however, could have DetachedNodeOwner in case * they were previously removed (aka detached) from a model. * Once node is attached to a model, {@link AttachedNodeOwner#registerNode(SNode)} is responsible to ensure complete hierarchy of added node, whether it's * detached, free-floating or mixed, is initialized with the same AttachedNodeOwner of the model itself. */ @NotNull private SNodeOwner myOwner = FreeFloatNodeOwner.INSTANCE; private SContainmentLink myRoleInParent; // field is never null; but individual elements may be null. References are unordered! // Note, when switch to keeping ref data only, may introduce a placeholder that would // simplify makeReferencesDirect() implementation (empty methods instead of != null check) // Like myProperties, keeps pairs of [SReferenceLink, AssociationData] private Object[] myReferences = EMPTY_OBJECT_ARRAY; // Keeps pairs [SProperty, String] private Object[] myProperties = null; private org.jetbrains.mps.openapi.model.SNodeId myId; /** * seems to be ok, and still extremely fragile since it is important to reset the reference to the field on _any_ element change. * I'd rather use some atomic array primitive from java.lang.concurrent */ @SuppressWarnings("VolatileArrayField") private volatile Object[] myUserObjects; // key,value,key,value ; copy-on-write (!) private final SConcept myConcept; 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 getProperty(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; myOwner.unregisterNode(wasChild); myOwner.performUndoableAction(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); return String.format("%s (concept is not found)", persistentName != null ? persistentName : myConcept.getName()); } return String.valueOf(SNodeUtil.getPresentation(this)); } @Override public String toString() { String s = null; try { s = findProperty(SNodeUtil.property_INamedConcept_name); if (s == null) { s = String.format("(instance of %s)", getConcept().getName()); } } catch (RuntimeException t) { LOG.error("Failed to get string presentation of a node", t, this); } if (s == null) { return "???"; } return s; } @NotNull @Override public SNodeReference getReference() { return new jetbrains.mps.smodel.SNodePointer(myOwner.lastKnownModel(), myId); } @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; } final int curLen = myUserObjects.length; for (int i = 0; i < curLen; i += 2) { if (myUserObjects[i].equals(key)) { Object[] newarr = new Object[myUserObjects.length]; System.arraycopy(myUserObjects, 0, newarr, 0, curLen); newarr[i + 1] = value; myUserObjects = newarr; return; } } Object[] newarr = new Object[curLen + 2]; System.arraycopy(myUserObjects, 0, newarr, 0, curLen); newarr[curLen] = key; newarr[curLen+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); ArrayList<jetbrains.mps.smodel.SReference> rv = new ArrayList<>(myReferences.length >>> 1); for (int i = 1, x = myReferences.length; i < x; i+=2) { final Object d = myReferences[i]; if (d != null) { final SReferenceLink link = (SReferenceLink) myReferences[i - 1]; rv.add(toAPI(link, d)); } } // XXX there's override of the method in mps-extensions that doesn't allow me to use openapi.SReference at the moment // (without simultaneous change in MPS-extensions as well) return rv; } @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 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 (Objects.equals(id, myId)) return; if (myOwner.getModel() == null) { myId = id; } else { LOG.error("can't set id to registered node " + getDebugText(this), new Throwable()); } } /*package*/ final void _setId(@NotNull org.jetbrains.mps.openapi.model.SNodeId id) { myId = id; } /*package*/ SReference toAPI(SReferenceLink link, Object associationData) { // both arguments not null if (associationData instanceof DynamicPtr) { return new DynamicReference(link, this, (DynamicPtr) associationData); } else { return new StaticReference(link, this, (AssociationData) associationData); } } @Override public void updateAssociation(SReferenceLink link, AssociationData oldRef, AssociationData newRef) { for (int i = 0, x = myReferences.length; i < x; i+=2) { if (link.equals(myReferences[i])) { assert myReferences[i+1] == oldRef; myReferences[i+1] = newRef; return; } } assert false : String.format("attempt to update missing association link %s", link); } /** * for a subtree starting at this node, apply the function to each association link known to the node. * This operation usually follows attachment of a node/subtree or precedes detachment of a node/subtree. */ /*package*/ final void forEachAssociationDeep(Function<AssociationData, AssociationData> translate) { for (int i = 1, x = myReferences.length; i < x; i+=2) { AssociationData d = (AssociationData) myReferences[i]; if (d == null) { continue; } myReferences[i] = translate.apply(d); } for (SNode child = firstChild(); child != null; child = child.treeNext()) { child.forEachAssociationDeep(translate); } } /** * apply the function to each association link known to the node. * unlike {@link #forEachAssociationDeep(Function)}, doesn't visit children */ /*package*/ final void forEachAssociationShallow(BiFunction<SReferenceLink, AssociationData, AssociationData> translate) { for (int i = 1, x = myReferences.length; i < x; i += 2) { AssociationData d = (AssociationData) myReferences[i]; if (d == null) { continue; } myReferences[i] = translate.apply((SReferenceLink) myReferences[i-1], d); } } // final void forEachInSubtree(Consumer<SNode> c) { // c.accept(this); // for (SNode child = firstChild(); child != null; child = child.treeNext()) { // child.forEachInSubtree(c); // } // } @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; } /*package*/ void setNodeOwner(/*NotNull*/ SNodeOwner owner) { myOwner = owner; } 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 !SModelPropertyEvent.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(); int index = getPropertyIndex(property); final String oldValue = index == -1 ? null : (String) myProperties[index + 1]; if (Objects.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_STRING_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(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<>(); List<SProperty> result = new ArrayList<>(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(); final AssociationData newValue; if (target == null) { newValue = null; // basically, == dropReference() without extra assertCanChange } else { if (getModel() != null && target.getModel() != null) { // 'mature' reference if (getModel() == target.getModel()) { newValue = new LocalNodePtr(target.getNodeId(), target.getName()); } else { newValue = new IndirectNodePtr(target.getModel().getReference(), target.getNodeId(), target.getName()); } } else { newValue = new DirectNode(target); } } doSetAssociation(role, newValue); } @Override public void setReference(@NotNull SReferenceLink role, ResolveInfo resolveInfo) { if (resolveInfo instanceof ResolveInfoExt) { setReference(role, ((ResolveInfoExt) resolveInfo).create(this, role)); } else if (resolveInfo instanceof ResolveInfo.S) { String ri = ((ResolveInfo.S) resolveInfo).getValue(); assertCanChange(); doSetAssociation(role, new DynamicPtr(ri)); } else if (resolveInfo instanceof ResolveInfo.PS) { ResolveInfo.PS ri = (ResolveInfo.PS) resolveInfo; assertCanChange(); SNodeReference target = ri.getTargetNode(); doSetAssociation(role, new IndirectNodePtr(target.getModelReference(), target.getNodeId(), ri.getValue())); } else if (resolveInfo instanceof ResolveInfo.N) { assertCanChange(); doSetAssociation(role, new DirectNode(((N) resolveInfo).getTargetNode())); } else if (resolveInfo instanceof ResolveInfo.D) { assertCanChange(); doSetAssociation(role, new LocalNodePtr(((ResolveInfo.D) resolveInfo).getTargetNode(), ((D) resolveInfo).getValue())); } else if (resolveInfo == null) { LOG.warning("Unexpected use of ResolveInfo == null. Reference would be removed, although explicit dropReference() has to be used", new Throwable()); dropReference(role); } else { LOG.warning(String.format("Unexpected ResolveInfo kind: %s(%s)", resolveInfo, resolveInfo.getClass()), new Throwable()); } } @Override public void setReference(@NotNull SReferenceLink role, @NotNull SNodeReference target) { assertCanChange(); if (target.getModelReference() != null && target.getModelReference().equals(getReference().getModelReference())) { doSetAssociation(role, new LocalNodePtr(target.getNodeId(), null)); } else { doSetAssociation(role, new IndirectNodePtr(target.getModelReference(), target.getNodeId(), null)); } } @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 (int i = 0, x = myReferences.length; i < x; i+=2) { final Object link = myReferences[i]; if (role.equals(link)) { return toAPI(role, myReferences[i+1]); } } return null; } // clear or replace an SReference // package visibility for undoable actions /*package*/ void doSetAssociation(/*not null*/ SReferenceLink role, /*nullable*/ AssociationData newValue) { AssociationData oldValue = null; int i = 1, x = myReferences.length, empty = x; for (; i < x; i+=2) { Object r = myReferences[i]; if (r == null) { empty = i; // fine to take the latest empty, but if not, may add (&& empty == x) into condition continue; } if (role.equals(myReferences[i-1])) { oldValue = ((AssociationData) r); // don't assign oldValue unless matched break; } } if (i >= x && newValue == null) { // none found and nothing to add/replace with. no referenceChanged event. return; } if (i < x) { // found existing, just replace myOwner.performUndoableAction(new RemoveReferenceAtUndoableAction(this, role, oldValue)); if (newValue != null) { myReferences[i] = newValue; myOwner.performUndoableAction(new InsertReferenceAtUndoableAction(this, role, newValue)); } else { // drop myReferences[i-1] = null; myReferences[i] = null; // check if we just replaced the last existing reference with null boolean allNulls = true; for (int j = 1; j < x; j += 2) { if (myReferences[j] != null) { allNulls = false; break; } } if (allNulls) { myReferences = EMPTY_OBJECT_ARRAY; } } myOwner.fireReferenceChange(this, role, oldValue, newValue); return; } assert i >= x && newValue != null; // in fact, with +=2, it's i > x if (empty < x) { // there's available slot myReferences[empty-1] = role; myReferences[empty] = newValue; } else { // no space available, storage have to grow. Allocate 2 slots (x2 Objects) right away Object[] newArray = new Object[x + 4]; System.arraycopy(myReferences, 0, newArray, 0, x); newArray[x] = role; newArray[x+1] = newValue; myReferences = newArray; } myOwner.performUndoableAction(new InsertReferenceAtUndoableAction(this, role, newValue)); myOwner.fireReferenceChange(this, role, null, newValue); } // 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). // // XXX besides, unlike setReference(role, (SNode) null), used to send out referenceChanged event even if no respective link was found. @Override public void setReference(@NotNull SReferenceLink role, org.jetbrains.mps.openapi.model.SReference toAdd) { // FIXME why assert, not RuntimeException?! OTOH, as I retire uses of the method, shall I care? assert toAdd == null || toAdd.getSourceNode() == this; assert toAdd == null || role.equals(toAdd.getLink()); assertCanChange(); doSetAssociation(role, toAdd == null ? null : ((SReference) toAdd).getData()); } @Override public void dropReference(@NotNull SReferenceLink role) { assertCanChange(); doSetAssociation(role, null); } 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.registerNode(schild); myOwner.performUndoableAction(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 ImmutableChildrenList(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 new SNodeLegacy(this).hasProperty(propertyName); } @Deprecated @Override public final String getProperty(String propertyName) { return new SNodeLegacy(this).getProperty(propertyName); } @Deprecated @Override public void setProperty(String propertyName, String propertyValue) { new SNodeLegacy(this).setProperty(propertyName, propertyValue); } @Deprecated @Override public Collection<String> getPropertyNames() { List<String> res = new ArrayList<>(myProperties == null ? 0 : myProperties.length / 2); for (SProperty p : getProperties()) { res.add(p.getName()); } return res; } @Deprecated @Override public SNode getReferenceTarget(String role) { return new SNodeLegacy(this).getReferenceTarget(role); } @Deprecated public void insertChildBefore(@NotNull String role, org.jetbrains.mps.openapi.model.SNode child, @Nullable final org.jetbrains.mps.openapi.model.SNode anchor) { new SNodeLegacy(this).insertChildBefore(role, child, anchor); } @Deprecated @Override public void addChild(String role, org.jetbrains.mps.openapi.model.SNode child) { new SNodeLegacy(this).insertChildBefore(role, child, null); } @Deprecated @Override @NotNull public List<SNode> getChildren(String role) { return new SNodeLegacy(this).getChildren(role); } }