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);
}
}