org.cobraparser.html.domimpl.NodeImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Cobra Show documentation
Show all versions of Cobra Show documentation
Cobra is the rendering engine designed for LoboBrowser
/*
GNU LESSER GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: [email protected]
*/
/*
* Created on Sep 3, 2005
*/
package org.cobraparser.html.domimpl;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.cobraparser.html.HtmlRendererContext;
import org.cobraparser.html.js.Event;
import org.cobraparser.html.style.RenderState;
import org.cobraparser.html.style.StyleSheetRenderState;
import org.cobraparser.js.AbstractScriptableDelegate;
import org.cobraparser.js.HideFromJS;
import org.cobraparser.ua.UserAgentContext;
import org.cobraparser.util.Strings;
import org.cobraparser.util.Urls;
import org.mozilla.javascript.Function;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;
import org.w3c.dom.html.HTMLCollection;
import org.w3c.dom.html.HTMLDocument;
import cz.vutbr.web.css.CSSException;
import cz.vutbr.web.css.CSSFactory;
import cz.vutbr.web.css.CombinedSelector;
import cz.vutbr.web.css.RuleSet;
import cz.vutbr.web.css.Selector;
import cz.vutbr.web.css.StyleSheet;
// TODO: Implement org.w3c.dom.events.EventTarget ?
public abstract class NodeImpl extends AbstractScriptableDelegate implements Node, ModelNode {
private static final NodeImpl[] EMPTY_ARRAY = new NodeImpl[0];
private static final @NonNull RenderState BLANK_RENDER_STATE = new StyleSheetRenderState(null);
protected static final Logger logger = Logger.getLogger(NodeImpl.class.getName());
protected UINode uiNode;
protected ArrayList nodeList;
protected volatile Document document;
/**
* A tree lock is less deadlock-prone than a node-level lock. This is assigned
* in setOwnerDocument.
*/
protected volatile Object treeLock = this;
public NodeImpl() {
super();
}
@HideFromJS
public void setUINode(final UINode uiNode) {
// Called in GUI thread always.
this.uiNode = uiNode;
}
@HideFromJS
public UINode getUINode() {
// Called in GUI thread always.
return this.uiNode;
}
/**
* Tries to get a UINode associated with the current node. Failing that, it
* tries ancestors recursively. This method will return the closest
* block-level renderer node, if any.
*/
@HideFromJS
public UINode findUINode() {
// Called in GUI thread always.
final UINode uiNode = this.uiNode;
if (uiNode != null) {
return uiNode;
}
final NodeImpl parentNode = (NodeImpl) this.getParentNode();
return parentNode == null ? null : parentNode.findUINode();
}
public Node appendChild(final Node newChild) throws DOMException {
if (newChild != null) {
synchronized (this.treeLock) {
if (isInclusiveAncestorOf(newChild)) {
final Node prevParent = newChild.getParentNode();
if (prevParent instanceof NodeImpl) {
((NodeImpl) prevParent).removeChild(newChild);
}
} else if ((newChild instanceof NodeImpl) && ((NodeImpl) newChild).isInclusiveAncestorOf(this)) {
throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Trying to append an ancestor element.");
}
ArrayList nl = this.nodeList;
if (nl == null) {
nl = new ArrayList<>(3);
this.nodeList = nl;
}
nl.add(newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
}
this.postChildListChanged();
return newChild;
} else {
throw new DOMException(DOMException.INVALID_ACCESS_ERR, "Trying to append a null child!");
}
}
// TODO not used by anyone
protected void removeAllChildren() {
this.removeAllChildrenImpl();
}
protected void removeAllChildrenImpl() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl != null) {
for (final Node node : nl) {
if (node instanceof NodeImpl) {
((NodeImpl) node).handleDeletedFromParent();
}
}
this.nodeList = null;
}
}
this.postChildListChanged();
}
protected NodeList getNodeList(final NodeFilter filter) {
final Collection collection = new ArrayList<>();
synchronized (this.treeLock) {
this.appendChildrenToCollectionImpl(filter, collection);
}
return new NodeListImpl(collection);
}
/*
* TODO: If this is not a w3c DOM method, we can return an Iterator instead of
* creating a new array But, it changes the semantics slightly (when
* modifications are needed during iteration). For those cases, we can retain
* this method.
*/
public NodeImpl[] getChildrenArray() {
final ArrayList nl = this.nodeList;
synchronized (this.treeLock) {
return nl == null ? null : nl.toArray(NodeImpl.EMPTY_ARRAY);
}
}
int getChildCount() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
return nl == null ? 0 : nl.size();
}
}
// TODO: This is needed to be implemented only by Element, Document and DocumentFragment as per https://developer.mozilla.org/en-US/docs/Web/API/ParentNode
public HTMLCollection getChildren() {
return new DescendentHTMLCollection(this, new NodeFilter.ElementFilter(), this.treeLock);
}
/**
* Creates an ArrayList
of descendent nodes that the given filter
* condition.
*/
public ArrayList getDescendents(final NodeFilter filter, final boolean nestIntoMatchingNodes) {
final ArrayList al = new ArrayList<>();
synchronized (this.treeLock) {
this.extractDescendentsArrayImpl(filter, al, nestIntoMatchingNodes);
}
return al;
}
/**
* Extracts all descendents that match the filter, except those descendents of
* nodes that match the filter.
*
* @param filter
* @param al
*/
private void extractDescendentsArrayImpl(final NodeFilter filter, final ArrayList al, final boolean nestIntoMatchingNodes) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator i = nl.iterator();
while (i.hasNext()) {
final NodeImpl n = (NodeImpl) i.next();
if (filter.accept(n)) {
al.add(n);
if (nestIntoMatchingNodes) {
n.extractDescendentsArrayImpl(filter, al, nestIntoMatchingNodes);
}
} else if (n.getNodeType() == Node.ELEMENT_NODE) {
n.extractDescendentsArrayImpl(filter, al, nestIntoMatchingNodes);
}
}
}
}
private void appendChildrenToCollectionImpl(final NodeFilter filter, final Collection collection) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator i = nl.iterator();
while (i.hasNext()) {
final NodeImpl node = (NodeImpl) i.next();
if (filter.accept(node)) {
collection.add(node);
}
node.appendChildrenToCollectionImpl(filter, collection);
}
}
}
/**
* Should create a node with some cloned properties, like the node name, but
* not attributes or children.
*/
protected abstract Node createSimilarNode();
public Node cloneNode(final boolean deep) {
// TODO: Synchronize with treeLock?
try {
final Node newNode = this.createSimilarNode();
final NodeList children = this.getChildNodes();
final int length = children.getLength();
for (int i = 0; i < length; i++) {
final Node child = children.item(i);
final Node newChild = deep ? child.cloneNode(deep) : child;
newNode.appendChild(newChild);
}
if (newNode instanceof Element) {
final Element elem = (Element) newNode;
final NamedNodeMap nnmap = this.getAttributes();
if (nnmap != null) {
final int nnlength = nnmap.getLength();
for (int i = 0; i < nnlength; i++) {
final Attr attr = (Attr) nnmap.item(i);
elem.setAttributeNode((Attr) attr.cloneNode(true));
}
}
}
synchronized (this) {
if ((userDataHandlers != null) && (userData != null)) {
userDataHandlers.forEach((k, handler) -> handler.handle(UserDataHandler.NODE_CLONED, k, userData.get(k), this, newNode));
}
}
return newNode;
} catch (final Exception err) {
throw new IllegalStateException(err.getMessage());
}
}
private int getNodeIndex() {
final NodeImpl parent = (NodeImpl) this.getParentNode();
return parent == null ? -1 : parent.getChildIndex(this);
}
int getChildIndex(final Node child) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
return nl == null ? -1 : nl.indexOf(child);
}
}
Node getChildAtIndex(final int index) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
try {
return nl == null ? null : nl.get(index);
} catch (final IndexOutOfBoundsException iob) {
this.warn("getChildAtIndex(): Bad index=" + index + " for node=" + this + ".");
return null;
}
}
}
private boolean isAncestorOf(final Node other) {
final NodeImpl parent = (NodeImpl) other.getParentNode();
if (parent == this) {
return true;
} else if (parent == null) {
return false;
} else {
return this.isAncestorOf(parent);
}
}
private boolean isInclusiveAncestorOf(final Node other) {
if (other == this) {
return true;
} else if (other == null) {
return false;
} else {
return this.isAncestorOf(other);
}
}
public short compareDocumentPosition(final Node other) throws DOMException {
final Node parent = this.getParentNode();
if (!(other instanceof NodeImpl)) {
throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Unknwon node implementation");
}
if ((parent != null) && (parent == other.getParentNode())) {
final int thisIndex = this.getNodeIndex();
final int otherIndex = ((NodeImpl) other).getNodeIndex();
if ((thisIndex == -1) || (otherIndex == -1)) {
return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
}
if (thisIndex < otherIndex) {
return Node.DOCUMENT_POSITION_FOLLOWING;
} else {
return Node.DOCUMENT_POSITION_PRECEDING;
}
} else if (this.isAncestorOf(other)) {
return Node.DOCUMENT_POSITION_CONTAINED_BY;
} else if (((NodeImpl) other).isAncestorOf(this)) {
return Node.DOCUMENT_POSITION_CONTAINS;
} else {
return Node.DOCUMENT_POSITION_DISCONNECTED;
}
}
public NamedNodeMap getAttributes() {
return null;
}
public Document getOwnerDocument() {
return this.document;
}
void setOwnerDocument(final Document value) {
this.document = value;
this.treeLock = value == null ? this : (Object) value;
}
void setOwnerDocument(final Document value, final boolean deep) {
this.document = value;
this.treeLock = value == null ? this : (Object) value;
if (deep) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator i = nl.iterator();
while (i.hasNext()) {
final NodeImpl child = (NodeImpl) i.next();
child.setOwnerDocument(value, deep);
}
}
}
}
}
void visitImpl(final NodeVisitor visitor) {
try {
visitor.visit(this);
} catch (final SkipVisitorException sve) {
return;
} catch (final StopVisitorException sve) {
throw sve;
}
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator i = nl.iterator();
while (i.hasNext()) {
final NodeImpl child = (NodeImpl) i.next();
try {
// Call with child's synchronization
child.visit(visitor);
} catch (final StopVisitorException sve) {
throw sve;
}
}
}
}
void visit(final NodeVisitor visitor) {
synchronized (this.treeLock) {
this.visitImpl(visitor);
}
}
/*
public Node insertBefore(final Node newChild, final Node refChild) throws DOMException {
synchronized (this.treeLock) {
final ArrayList nl = getNonEmptyNodeList();
// int idx = nl == null ? -1 : nl.indexOf(refChild);
int idx = nl.indexOf(refChild);
if (idx == -1) {
// The exception was misleading. -1 could have resulted from an empty node list too. (but that is no more the case)
// throw new DOMException(DOMException.NOT_FOUND_ERR, "refChild not found");
// From what I understand from https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore
// an invalid refChild will add the new child at the end of the list
idx = nl.size();
}
nl.add(idx, newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
}
this.postChildListChanged();
return newChild;
}*/
// Ongoing issue : 152
// This is a changed and better version of the above. It gives the same number of pass / failures on http://web-platform.test:8000/dom/nodes/Node-insertBefore.html
// Pass 2: FAIL: 24
public Node insertBefore(final Node newChild, final Node refChild) throws DOMException {
if (newChild == null) {
throw new DOMException(DOMException.TYPE_MISMATCH_ERR, "child is null");
}
synchronized (this.treeLock) {
if (newChild instanceof NodeImpl) {
final NodeImpl newChildImpl = (NodeImpl) newChild;
if (newChildImpl.isInclusiveAncestorOf(this)) {
throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "new child is an ancestor");
}
}
// From what I understand from https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore
// a null or undefined refChild will cause the new child to be appended at the end of the list
// otherwise, this function will throw an exception if refChild is not found in the child list
final ArrayList nl = refChild == null ? getNonEmptyNodeList() : this.nodeList;
final int idx = refChild == null ? nl.size() : (nl == null ? -1 : nl.indexOf(refChild));
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "refChild not found");
}
nl.add(idx, newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
}
this.postChildListChanged();
return newChild;
}
// TODO: Use this wherever nodeList needs to be non empty
private @NonNull ArrayList getNonEmptyNodeList() {
ArrayList nl = this.nodeList;
if (nl == null) {
nl = new ArrayList<>();
this.nodeList = nl;
}
return nl;
}
protected Node insertAt(final Node newChild, final int idx) throws DOMException {
synchronized (this.treeLock) {
final ArrayList nl = getNonEmptyNodeList();
nl.add(idx, newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
}
this.postChildListChanged();
return newChild;
}
public Node replaceChild(final Node newChild, final Node oldChild) throws DOMException {
synchronized (this.treeLock) {
if (this.isInclusiveAncestorOf(newChild)) {
throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "newChild is already a child of the node");
}
if ((newChild instanceof NodeImpl) && ((NodeImpl) newChild).isInclusiveAncestorOf(this)) {
throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Trying to set an ancestor element as a child.");
}
final ArrayList nl = this.nodeList;
final int idx = nl == null ? -1 : nl.indexOf(oldChild);
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "oldChild not found");
}
nl.set(idx, newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
if (oldChild instanceof NodeImpl) {
((NodeImpl) oldChild).handleDeletedFromParent();
}
}
this.postChildListChanged();
return newChild;
}
public Node removeChild(final Node oldChild) throws DOMException {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if ((nl == null) || !nl.remove(oldChild)) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "oldChild not found");
}
if (oldChild instanceof NodeImpl) {
((NodeImpl) oldChild).handleDeletedFromParent();
}
}
this.postChildListChanged();
return oldChild;
}
@HideFromJS
public Node removeChildAt(final int index) throws DOMException {
try {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl == null) {
throw new DOMException(DOMException.INDEX_SIZE_ERR, "Empty list of children");
}
final Node n = nl.remove(index);
if (n == null) {
throw new DOMException(DOMException.INDEX_SIZE_ERR, "No node with that index");
}
if (n instanceof NodeImpl) {
((NodeImpl) n).handleDeletedFromParent();
}
return n;
}
} finally {
this.postChildListChanged();
}
}
public boolean hasChildNodes() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
return (nl != null) && !nl.isEmpty();
}
}
public String getBaseURI() {
final Document document = this.document;
return document == null ? null : document.getBaseURI();
}
public NodeList getChildNodes() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
return new NodeListImpl(nl == null ? Collections.emptyList() : nl);
}
}
public Node getFirstChild() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
try {
return nl == null ? null : nl.get(0);
} catch (final IndexOutOfBoundsException iob) {
return null;
}
}
}
public Node getLastChild() {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
try {
return nl == null ? null : nl.get(nl.size() - 1);
} catch (final IndexOutOfBoundsException iob) {
return null;
}
}
}
private Node getPreviousTo(final Node node) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
final int idx = nl == null ? -1 : nl.indexOf(node);
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "node not found");
}
try {
return nl.get(idx - 1);
} catch (final IndexOutOfBoundsException iob) {
return null;
}
}
}
private Node getNextTo(final Node node) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
final int idx = nl == null ? -1 : nl.indexOf(node);
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "node not found");
}
try {
return nl.get(idx + 1);
} catch (final IndexOutOfBoundsException iob) {
return null;
}
}
}
public Node getPreviousSibling() {
final NodeImpl parent = (NodeImpl) this.getParentNode();
return parent == null ? null : parent.getPreviousTo(this);
}
public Node getNextSibling() {
final NodeImpl parent = (NodeImpl) this.getParentNode();
return parent == null ? null : parent.getNextTo(this);
}
public Element getPreviousElementSibling() {
final NodeImpl parent = (NodeImpl) this.getParentNode();
if (parent != null) {
Node previous = this;
do {
previous = parent.getPreviousTo(previous);
if ((previous != null) && (previous instanceof Element)) {
return (Element) previous;
}
} while (previous != null);
return null;
} else {
return null;
}
}
public Element getNextElementSibling() {
final NodeImpl parent = (NodeImpl) this.getParentNode();
if (parent != null) {
Node next = this;
do {
next = parent.getNextTo(next);
if ((next != null) && (next instanceof Element)) {
return (Element) next;
}
} while (next != null);
return null;
} else {
return null;
}
}
public Object getFeature(final String feature, final String version) {
// TODO What should this do?
return null;
}
private Map userData;
// TODO: Inform handlers on cloning, etc.
private Map userDataHandlers;
protected volatile boolean notificationsSuspended = false;
public Object setUserData(final String key, final Object data, final UserDataHandler handler) {
if (org.cobraparser.html.parser.HtmlParser.MODIFYING_KEY.equals(key)) {
final boolean ns = (Boolean.TRUE == data);
this.notificationsSuspended = ns;
if (!ns) {
this.informNodeLoaded();
}
}
// here we spent some effort preventing our maps from growing too much
synchronized (this) {
if (handler != null) {
if (this.userDataHandlers == null) {
this.userDataHandlers = new HashMap<>();
} else {
this.userDataHandlers.remove(key);
}
this.userDataHandlers.put(key, handler);
}
Map userData = this.userData;
if (data != null) {
if (userData == null) {
userData = new HashMap<>();
this.userData = userData;
}
return userData.put(key, data);
} else if (userData != null) {
return userData.remove(key);
} else {
return null;
}
}
}
public Object getUserData(final String key) {
synchronized (this) {
final Map ud = this.userData;
return ud == null ? null : ud.get(key);
}
}
public abstract String getLocalName();
public boolean hasAttributes() {
return false;
}
public String getNamespaceURI() {
return null;
}
public abstract String getNodeName();
public abstract String getNodeValue() throws DOMException;
private volatile String prefix;
public String getPrefix() {
return this.prefix;
}
public void setPrefix(final String prefix) throws DOMException {
this.prefix = prefix;
}
public abstract void setNodeValue(String nodeValue) throws DOMException;
public abstract short getNodeType();
/**
* Gets the text content of this node and its descendents.
*/
public String getTextContent() throws DOMException {
final StringBuffer sb = new StringBuffer();
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator i = nl.iterator();
while (i.hasNext()) {
final Node node = i.next();
final short type = node.getNodeType();
switch (type) {
case Node.CDATA_SECTION_NODE:
case Node.TEXT_NODE:
case Node.ELEMENT_NODE:
final String textContent = node.getTextContent();
if (textContent != null) {
sb.append(textContent);
}
break;
default:
break;
}
}
}
}
return sb.toString();
}
public void setTextContent(final String textContent) throws DOMException {
synchronized (this.treeLock) {
this.removeChildrenImpl(new TextFilter());
if ((textContent != null) && !"".equals(textContent)) {
final TextImpl t = new TextImpl(textContent);
t.setOwnerDocument(this.document);
t.setParentImpl(this);
ArrayList nl = this.nodeList;
if (nl == null) {
nl = new ArrayList<>();
this.nodeList = nl;
}
nl.add(t);
}
}
this.postChildListChanged();
}
protected void removeChildren(final NodeFilter filter) {
synchronized (this.treeLock) {
this.removeChildrenImpl(filter);
}
this.postChildListChanged();
}
protected void removeChildrenImpl(final NodeFilter filter) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final int len = nl.size();
for (int i = len; --i >= 0;) {
final Node node = nl.get(i);
if (filter.accept(node)) {
final Node n = nl.remove(i);
if (n instanceof NodeImpl) {
((NodeImpl) n).handleDeletedFromParent();
}
}
}
}
}
@HideFromJS
public Node insertAfter(final Node newChild, final Node refChild) {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
final int idx = nl == null ? -1 : nl.indexOf(refChild);
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "refChild not found");
}
nl.add(idx + 1, newChild);
if (newChild instanceof NodeImpl) {
((NodeImpl) newChild).handleAddedToParent(this);
}
}
this.postChildListChanged();
return newChild;
}
@HideFromJS
public Text replaceAdjacentTextNodes(final Text node, final String textContent) {
try {
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl == null) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child");
}
final int idx = nl.indexOf(node);
if (idx == -1) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child");
}
int firstIdx = idx;
final List