/* 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: lobochief@users.sourceforge.net */ /* * Created on Sep 3, 2005 */ package org.lobobrowser.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.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.js.Event; import org.lobobrowser.html.style.RenderState; import org.lobobrowser.html.style.StyleSheetRenderState; import org.lobobrowser.js.AbstractScriptableDelegate; import org.lobobrowser.js.HideFromJS; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.util.Strings; import org.lobobrowser.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<Node> 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 * <i>block-level</i> 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<Node> 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<Node> 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<Node> 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<Node> nl = this.nodeList; synchronized (this.treeLock) { return nl == null ? null : nl.toArray(NodeImpl.EMPTY_ARRAY); } } int getChildCount() { synchronized (this.treeLock) { final ArrayList<Node> 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 <code>ArrayList</code> of descendent nodes that the given filter * condition. */ public ArrayList<NodeImpl> getDescendents(final NodeFilter filter, final boolean nestIntoMatchingNodes) { final ArrayList<NodeImpl> 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<NodeImpl> al, final boolean nestIntoMatchingNodes) { final ArrayList<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> 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<Node> collection) { final ArrayList<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> 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<Node> nl = this.nodeList; return nl == null ? -1 : nl.indexOf(child); } } Node getChildAtIndex(final int index) { synchronized (this.treeLock) { final ArrayList<Node> 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<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> 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<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> 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<Node> 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<Node> 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<Node> getNonEmptyNodeList() { ArrayList<Node> 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<Node> 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<Node> 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<Node> 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<Node> 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<Node> 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<Node> nl = this.nodeList; return new NodeListImpl(nl == null ? Collections.emptyList() : nl); } } public Node getFirstChild() { synchronized (this.treeLock) { final ArrayList<Node> 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<Node> 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<Node> 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<Node> 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<String, Object> userData; // TODO: Inform handlers on cloning, etc. private Map<String, UserDataHandler> userDataHandlers; protected volatile boolean notificationsSuspended = false; public Object setUserData(final String key, final Object data, final UserDataHandler handler) { if (org.lobobrowser.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<String, Object> 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<String, Object> 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<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> 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<Node> 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<Node> 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<Node> 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<Node> 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<Object> toDelete = new LinkedList<>(); for (int adjIdx = idx; --adjIdx >= 0;) { final Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { firstIdx = adjIdx; toDelete.add(child); } } final int length = this.nodeList.size(); for (int adjIdx = idx; ++adjIdx < length;) { final Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { toDelete.add(child); } } this.nodeList.removeAll(toDelete); final TextImpl textNode = new TextImpl(textContent); textNode.setOwnerDocument(this.document); textNode.setParentImpl(this); this.nodeList.add(firstIdx, textNode); return textNode; } } finally { this.postChildListChanged(); } } @HideFromJS public Text replaceAdjacentTextNodes(final Text node) { try { synchronized (this.treeLock) { final ArrayList<Node> 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"); } final StringBuffer textBuffer = new StringBuffer(); int firstIdx = idx; final List<Object> toDelete = new LinkedList<>(); for (int adjIdx = idx; --adjIdx >= 0;) { final Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { firstIdx = adjIdx; toDelete.add(child); textBuffer.append(((Text) child).getNodeValue()); } } final int length = this.nodeList.size(); for (int adjIdx = idx; ++adjIdx < length;) { final Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { toDelete.add(child); textBuffer.append(((Text) child).getNodeValue()); } } this.nodeList.removeAll(toDelete); final TextImpl textNode = new TextImpl(textBuffer.toString()); textNode.setOwnerDocument(this.document); textNode.setParentImpl(this); this.nodeList.add(firstIdx, textNode); return textNode; } } finally { this.postChildListChanged(); } } protected volatile Node parentNode; public Node getParentNode() { // Should it be synchronized? Could have side-effects. return this.parentNode; } public boolean isSameNode(final Node other) { return this == other; } public boolean isSupported(final String feature, final String version) { return ("HTML".equals(feature) && (version.compareTo("4.01") <= 0)); } public String lookupNamespaceURI(final String prefix) { return null; } public boolean equalAttributes(final Node arg) { return false; } public boolean isEqualNode(final Node arg) { return (arg instanceof NodeImpl) && (this.getNodeType() == arg.getNodeType()) && java.util.Objects.equals(this.getNodeName(), arg.getNodeName()) && java.util.Objects.equals(this.getNodeValue(), arg.getNodeValue()) && java.util.Objects.equals(this.getLocalName(), arg.getLocalName()) && java.util.Objects.equals(this.nodeList, ((NodeImpl) arg).nodeList) && this.equalAttributes(arg); } public boolean isDefaultNamespace(final String namespaceURI) { return namespaceURI == null; } public String lookupPrefix(final String namespaceURI) { return null; } public void normalize() { synchronized (this.treeLock) { final ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); final List<Node> textNodes = new LinkedList<>(); boolean prevText = false; while (i.hasNext()) { final Node child = i.next(); if (child.getNodeType() == Node.TEXT_NODE) { if (!prevText) { prevText = true; textNodes.add(child); } } else { prevText = false; } } i = textNodes.iterator(); while (i.hasNext()) { final Text text = (Text) i.next(); this.replaceAdjacentTextNodes(text); } } } this.postChildListChanged(); } @Override public String toString() { return this.getNodeName(); } public UserAgentContext getUserAgentContext() { final Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getUserAgentContext(); } else { return null; } } public HtmlRendererContext getHtmlRendererContext() { final Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getHtmlRendererContext(); } else { return null; } } final void setParentImpl(final Node parent) { // Call holding treeLock. this.parentNode = parent; } // ----- ModelNode implementation /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.RenderableContext#getFullURL(java.lang.String) */ public @NonNull URL getFullURL(final String spec) throws MalformedURLException { final Object doc = this.document; final String cleanSpec = Urls.encodeIllegalCharacters(spec); if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getFullURL(cleanSpec); } else { return new java.net.URL(cleanSpec); } } public URL getDocumentURL() { final Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getDocumentURL(); } else { return null; } } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.RenderableContext#getDocumentItem(java.lang.String * ) */ public Object getDocumentItem(final String name) { final org.w3c.dom.Document document = this.document; return document == null ? null : document.getUserData(name); } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.RenderableContext#setDocumentItem(java.lang.String * , java.lang.Object) */ public void setDocumentItem(final String name, final Object value) { final org.w3c.dom.Document document = this.document; if (document == null) { return; } document.setUserData(name, value, null); } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.RenderableContext#isEqualOrDescendentOf(org.xamjwg * .html.renderer.RenderableContext) */ public final boolean isEqualOrDescendentOf(final ModelNode otherContext) { if (otherContext == this) { return true; } final Object parent = this.getParentNode(); if (parent instanceof HTMLElementImpl) { return ((HTMLElementImpl) parent).isEqualOrDescendentOf(otherContext); } else { return false; } } public final ModelNode getParentModelNode() { return (ModelNode) this.parentNode; } public void warn(final String message, final Throwable err) { logger.log(Level.WARNING, message, err); } public void warn(final String message) { logger.log(Level.WARNING, message); } public void informSizeInvalid() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.sizeInvalidated(this); } } public void informLookInvalid() { this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.lookInvalidated(this); } } public void informPositionInvalid() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.positionInParentInvalidated(this); } } public void informInvalid() { // This is called when an attribute or child changes. this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.invalidated(this); } } public void informStructureInvalid() { // This is called when an attribute or child changes. this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.structureInvalidated(this); } } protected void informNodeLoaded() { // This is called when an attribute or child changes. this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.nodeLoaded(this); } } protected void informExternalScriptLoading() { // This is called when an attribute or child changes. this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.externalScriptLoading(this); } } public void informLayoutInvalid() { // This is called by the style properties object. this.forgetRenderState(); final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.invalidated(this); } } public void informDocumentInvalid() { // This is called when an attribute or child changes. final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.allInvalidated(true); } } private RenderState renderState = null; public @NonNull RenderState getRenderState() { // Generally called from the GUI thread, except for // offset properties. synchronized (this.treeLock) { RenderState rs = this.renderState; rs = this.renderState; if (rs != null) { return rs; } final Object parent = this.parentNode; if ((parent != null) || (this instanceof Document)) { final RenderState prs = getParentRenderState(parent); rs = this.createRenderState(prs); this.renderState = rs; return rs; } else { // Scenario is possible due to Javascript. return BLANK_RENDER_STATE; } } } private final static RenderState getParentRenderState(final Object parent) { if (parent instanceof NodeImpl) { return ((NodeImpl) parent).getRenderState(); } else { return null; } } // abstract protected RenderState createRenderState(final RenderState prevRenderState); protected @NonNull RenderState createRenderState(final RenderState prevRenderState) { if (prevRenderState == null) { return BLANK_RENDER_STATE; } else { return prevRenderState; } } protected void forgetRenderState() { synchronized (this.treeLock) { if (this.renderState != null) { this.renderState = null; // Note that getRenderState() "validates" // ancestor states as well. final java.util.ArrayList<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> i = nl.iterator(); while (i.hasNext()) { ((NodeImpl) i.next()).forgetRenderState(); } } } } } public String getInnerHTML() { final StringBuffer buffer = new StringBuffer(); synchronized (this) { this.appendInnerHTMLImpl(buffer); } return buffer.toString(); } protected void appendInnerHTMLImpl(final StringBuffer buffer) { final ArrayList<Node> nl = this.nodeList; int size; if ((nl != null) && ((size = nl.size()) > 0)) { for (int i = 0; i < size; i++) { final Node child = nl.get(i); if (child instanceof HTMLElementImpl) { ((HTMLElementImpl) child).appendOuterHTMLImpl(buffer); } else if (child instanceof Comment) { buffer.append("<!--" + ((Comment) child).getTextContent() + "-->"); } else if (child instanceof Text) { final String text = ((Text) child).getTextContent(); final String encText = this.htmlEncodeChildText(text); buffer.append(encText); } else if (child instanceof ProcessingInstruction) { buffer.append(child.toString()); } } } } protected String htmlEncodeChildText(final String text) { return Strings.strictHtmlEncode(text, false); } /** * Attempts to convert the subtree starting at this point to a close text * representation. BR elements are converted to line breaks, and so forth. */ public String getInnerText() { final StringBuffer buffer = new StringBuffer(); synchronized (this.treeLock) { this.appendInnerTextImpl(buffer); } return buffer.toString(); } protected void appendInnerTextImpl(final StringBuffer buffer) { final ArrayList<Node> nl = this.nodeList; if (nl == null) { return; } final int size = nl.size(); if (size == 0) { return; } for (int i = 0; i < size; i++) { final Node child = nl.get(i); if (child instanceof ElementImpl) { ((ElementImpl) child).appendInnerTextImpl(buffer); } if (child instanceof Comment) { // skip } else if (child instanceof Text) { buffer.append(((Text) child).getTextContent()); } } } /* protected void dispatchEventToHandlers(final Event event, final List<Function> handlers) { if (handlers != null) { // We clone the collection and check if original collection still contains // the handler before dispatching // This is to avoid ConcurrentModificationException during dispatch // TODO: Event Bubbling final ArrayList<Function> handlersCopy = new ArrayList<>(handlers); for (final Function h : handlersCopy) { if (handlers.contains(h)) { Executor.executeFunction(this, h, event); } } } } private final Map<String, List<Function>> onEventHandlers = new HashMap<>(); public void addEventListener(final String type, final Function listener) { addEventListener(type, listener, false); } public void addEventListener(final String type, final Function listener, final boolean useCapture) { // TODO System.out.println("node by name: " + getNodeName() + " adding Event listener of type: " + type); List<Function> handlerList = null; if (onEventHandlers.containsKey(type)) { handlerList = onEventHandlers.get(type); } else { handlerList = new ArrayList<>(); onEventHandlers.put(type, handlerList); } handlerList.add(listener); } public void removeEventListener(final String type, final Function listener, final boolean useCapture) { // TODO System.out.println("node remove Event listener: " + type); if (onEventHandlers.containsKey(type)) { onEventHandlers.get(type).remove(listener); } } public boolean dispatchEvent(final Event evt) { System.out.println("Dispatching event: " + evt); dispatchEventToHandlers(evt, onEventHandlers.get(evt.getType())); return false; }*/ private volatile boolean attachedToDocument = this instanceof HTMLDocument; /** * @return the attachment with the document. true if the element is attached * to the document, false otherwise. Document nodes are considered * attached by default. */ protected final boolean isAttachedToDocument() { return this.attachedToDocument; } /** * This method is intended to be overriden by subclasses that are interested * in processing their child-list whenever it is updated. */ protected void handleChildListChanged() { } /** * This method is intended to be overriden by subclasses that are interested * in performing some operation when they are attached/detached from the * document. */ protected void handleDocumentAttachmentChanged() { } /** * This method will be called on a node whenever it is being appended to a * parent node. * * NOTE: changeDocumentAttachment will call updateIds() which needs to be tree * locked, and hence these methods are also being tree locked */ private void handleAddedToParent(final NodeImpl parent) { this.setParentImpl(parent); changeDocumentAttachment(parent.isAttachedToDocument()); } /** * This method will be called on a node whenever it is being deleted from a * parent node. * * NOTE: changeDocumentAttachment will call updateIds() which needs to be tree * locked, and hence these methods are also being tree locked */ private void handleDeletedFromParent() { this.setParentImpl(null); changeDocumentAttachment(false); } /** * This method will change the attachment of a node with the document. It will * also change the attachment of all its descendant nodes. * * @param attached * the attachment with the document. true when attached, false * otherwise. */ private void changeDocumentAttachment(final boolean attached) { if (this.attachedToDocument != attached) { this.attachedToDocument = attached; handleDocumentAttachmentChanged(); if (this instanceof ElementImpl) { final ElementImpl elementImpl = (ElementImpl) this; elementImpl.updateIdMap(attached); } } if (nodeList != null) { for (final Node node : this.nodeList) { if (node instanceof NodeImpl) { ((NodeImpl) node).changeDocumentAttachment(attached); } } } } /** * Common tasks to be performed when the NodeList of an element is changed. */ private void postChildListChanged() { this.handleChildListChanged(); if (!this.notificationsSuspended) { this.informStructureInvalid(); } } /* public void addEventListener(final String type, final EventListener listener) { addEventListener(type, listener, false); } public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { if (useCapture) { throw new UnSupportedOperationException(); } } public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { // TODO Auto-generated method stub } public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { // TODO Auto-generated method stub return false; }*/ public void addEventListener(final String type, final Function listener) { addEventListener(type, listener, false); } public void addEventListener(final String type, final Function listener, final boolean useCapture) { // TODO System.out.println("node by name: " + getNodeName() + " adding Event listener of type: " + type); // System.out.println(" txt content: " + getInnerText()); ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().addEventListener(this, type, listener); } public void removeEventListener(final String type, final Function listener, final boolean useCapture) { // TODO System.out.println("node remove Event listener: " + type); ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().removeEventListener(this, type, listener, useCapture); } public boolean dispatchEvent(final Event evt) { System.out.println("Dispatching event: " + evt); // dispatchEventToHandlers(evt, onEventHandlers.get(evt.getType())); ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().dispatchEvent(this, evt); return false; } /* public void addEventListener(final String type, final EventListener listener) { addEventListener(type, listener, false); } public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { if (useCapture) { throw new UnSupportedOperationException(); } } public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { // TODO Auto-generated method stub } public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { // TODO Auto-generated method stub return false; }*/ public Element querySelector(final String query) { // TODO: Optimize: Avoid getting all matches. Only first match is sufficient. final NodeList matchingElements = querySelectorAll(query); if (matchingElements.getLength() > 0) { return (Element) matchingElements.item(0); } else { return null; } } private static CombinedSelector[] makeSelectors(final String query) throws IOException, CSSException { // this is quick way to parse the selectors. TODO: check if jStyleParser supports a better option. final String tempBlock = query + " { display: none}"; final StyleSheet styleSheet = CSSFactory.parseString(tempBlock, null); final RuleSet firstRuleBlock = (RuleSet) styleSheet.get(0); final CombinedSelector[] selectors = firstRuleBlock.getSelectors(); return selectors; } /* protected Collection<Node> getMatchingChildren(CombinedSelector selectors) { final Collection<Node> matchingElements = new LinkedList<>(); final NodeImpl[] childrenArray = getChildrenArray(); if (childrenArray != null) { for (final NodeImpl n : childrenArray) { if (n instanceof ElementImpl) { final ElementImpl element = (ElementImpl) n; if (selectors.stream().anyMatch(selector -> selector.matches(element))) { System.out.println("Found match: " + element + " of class: " + element.getClass()); matchingElements.add(element); } matchingElements.addAll(element.getMatchingChildren(selectors)); } } } return matchingElements; }*/ protected Collection<Node> getMatchingChildren(final List<Selector> selectors) { final Collection<Node> matchingElements = new LinkedList<>(); final int numSelectors = selectors.size(); if (numSelectors > 0) { final Selector firstSelector = selectors.get(0); final NodeImpl[] childrenArray = getChildrenArray(); if (childrenArray != null) { for (final NodeImpl n : childrenArray) { if (n instanceof ElementImpl) { final ElementImpl element = (ElementImpl) n; if (firstSelector.matches(element)) { if (numSelectors > 1) { final List<Selector> tailSelectors = selectors.subList(1, numSelectors); matchingElements.addAll(element.getMatchingChildren(tailSelectors)); } else { matchingElements.add(element); } } matchingElements.addAll(element.getMatchingChildren(selectors)); } } } } return matchingElements; } public NodeList querySelectorAll(final String query) { try { final CombinedSelector[] selectors = makeSelectors(query); final LinkedList<Node> matches = new LinkedList<>(); for (final CombinedSelector selector : selectors) { matches.addAll(getMatchingChildren(selector)); } return new NodeListImpl(matches); } catch (final IOException | CSSException e) { e.printStackTrace(); throw new DOMException(DOMException.SYNTAX_ERR, "Couldn't parse selector: " + query); } } public NodeList getElementsByClassName(final String classNames) { final String[] classNamesArray = classNames.split("\\s"); // TODO: escape commas in class-names final String query = Arrays.stream(classNamesArray) .filter(cn -> cn.length() > 0) .map(cn -> "." + cn) .collect(Collectors.joining(",")); return querySelectorAll(query); } public NodeList getElementsByTagName(final String classNames) { final String[] classNamesArray = classNames.split("\\s"); // TODO: escape commas in class-names final String query = Arrays.stream(classNamesArray).collect(Collectors.joining(",")); return querySelectorAll(query); } // TODO: This is a plug public String getNameSpaceURI() { final short nodeType = getNodeType(); if (nodeType == ELEMENT_NODE || nodeType == ATTRIBUTE_NODE) { return "http://www.w3.org/1999/xhtml"; } else { return null; } } }