/*
* 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;
}
}
}
}