/* GNU GENERAL LICENSE Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2017 Lobo Evolution This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 3 of the License, or (at your option) any later version. This program 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 General License for more details. You should have received a copy of the GNU General Public along with this program. If not, see <http://www.gnu.org/licenses/>. Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it */ /* * Created on Sep 3, 2005 */ package org.lobobrowser.html.domimpl; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; 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 org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.dombl.ChildHTMLCollection; import org.lobobrowser.html.dombl.ModelNode; import org.lobobrowser.html.dombl.NodeVisitor; import org.lobobrowser.html.dombl.SkipVisitorException; import org.lobobrowser.html.dombl.StopVisitorException; import org.lobobrowser.html.dombl.UINode; import org.lobobrowser.html.domfilter.NodeFilter; import org.lobobrowser.html.domfilter.TextFilter; import org.lobobrowser.html.renderstate.RenderState; import org.lobobrowser.html.renderstate.StyleSheetRenderState; import org.lobobrowser.http.UserAgentContext; import org.lobobrowser.js.AbstractScriptableDelegate; import org.lobobrowser.util.Objects; import org.lobobrowser.util.Strings; import org.lobobrowser.util.Urls; 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; /** * The Class DOMNodeImpl. */ public abstract class DOMNodeImpl extends AbstractScriptableDelegate implements Node, ModelNode { /** The Constant EMPTY_ARRAY. */ private static final DOMNodeImpl[] EMPTY_ARRAY = new DOMNodeImpl[0]; /** The Constant INVALID_RENDER_STATE. */ private static final RenderState INVALID_RENDER_STATE = new StyleSheetRenderState(null); /** The Constant logger. */ protected static final Logger logger = LogManager.getLogger(DOMNodeImpl.class.getName()); /** The ui node. */ protected UINode uiNode; /** The node list. */ protected ArrayList<Node> nodeList; /** The document. */ protected volatile Document document; /** * A tree lock is less deadlock-prone than a node-level lock. This is * assigned in setOwnerDocument. */ private volatile Object treeLock = this; /** * Instantiates a new DOM node impl. */ public DOMNodeImpl() { super(); } /** Sets the UI node. * * @param uiNode * the new UI node */ public void setUINode(UINode uiNode) { // Called in GUI thread always. this.uiNode = uiNode; } /** Gets the UI node. * * @return the UI node */ 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. * * @return the UI node */ public UINode findUINode() { // Called in GUI thread always. UINode uiNode = this.uiNode; if (uiNode != null) { return uiNode; } DOMNodeImpl parentNode = (DOMNodeImpl) this.getParentNode(); return parentNode == null ? null : parentNode.findUINode(); } /* * (non-Javadoc) * @see org.w3c.dom.Node#appendChild(org.w3c.dom.Node) */ @Override public Node appendChild(Node newChild) throws DOMException { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl == null) { nl = new ArrayList<Node>(3); this.nodeList = nl; } nl.add(newChild); if (newChild instanceof DOMNodeImpl) { ((DOMNodeImpl) newChild).setParentImpl(this); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } return newChild; } /** * Removes the all children. */ public void removeAllChildren() { synchronized (this.getTreeLock()) { this.removeAllChildrenImpl(); } } /** * Removes the all children impl. */ protected void removeAllChildrenImpl() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl != null) { nl.clear(); // this.nodeList = null; } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } } /** * Gets the node list. * * @param filter * the filter * @return the node list */ public NodeList getNodeList(NodeFilter filter) { Collection<DOMNodeImpl> collection = new ArrayList<DOMNodeImpl>(); synchronized (this.getTreeLock()) { this.appendChildrenToCollectionImpl(filter, collection); } return new DOMNodeListImpl(collection); } /** Gets the children array. * * @return the children array */ public DOMNodeImpl[] getChildrenArray() { ArrayList<Node> nl = this.nodeList; synchronized (this.getTreeLock()) { return nl == null ? null : (DOMNodeImpl[]) nl .toArray(DOMNodeImpl.EMPTY_ARRAY); } } /** Gets the child count. * * @return the child count */ public int getChildCount() { ArrayList<Node> nl = this.nodeList; synchronized (this.getTreeLock()) { return nl == null ? 0 : nl.size(); } } /** The children collection. */ private ChildHTMLCollection childrenCollection; /** Gets the children. * * @return the children */ public ChildHTMLCollection getChildren() { // Method required by JavaScript synchronized (this) { ChildHTMLCollection collection = this.childrenCollection; if (collection == null) { collection = new ChildHTMLCollection(this); this.childrenCollection = collection; } return collection; } } /** * Creates an <code>ArrayList</code> of descendent nodes that the given * filter condition. * * @param filter * the filter * @param nestIntoMatchingNodes * the nest into matching nodes * @return the descendents */ public ArrayList<DOMNodeImpl> getDescendents(NodeFilter filter, boolean nestIntoMatchingNodes) { ArrayList<DOMNodeImpl> al = new ArrayList<DOMNodeImpl>(); synchronized (this.getTreeLock()) { 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 * the filter * @param al * the al * @param nestIntoMatchingNodes * the nest into matching nodes */ private void extractDescendentsArrayImpl(NodeFilter filter, ArrayList<DOMNodeImpl> al, boolean nestIntoMatchingNodes) { ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { DOMNodeImpl n = (DOMNodeImpl) 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); } } } } /** * Append children to collection impl. * * @param filter * the filter * @param collection * the collection */ private void appendChildrenToCollectionImpl(NodeFilter filter, Collection<DOMNodeImpl> collection) { ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { DOMNodeImpl node = (DOMNodeImpl) 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. * * @return the node */ protected abstract Node createSimilarNode(); /* * (non-Javadoc) * @see org.w3c.dom.Node#cloneNode(boolean) */ @Override public Node cloneNode(boolean deep) { try { Node newNode = this.createSimilarNode(); NodeList children = this.getChildNodes(); int length = children.getLength(); for (int i = 0; i < length; i++) { Node child = children.item(i); Node newChild = deep ? child.cloneNode(deep) : child; newNode.appendChild(newChild); } if (newNode instanceof Element) { Element elem = (Element) newNode; NamedNodeMap nnmap = this.getAttributes(); if (nnmap != null) { int nnlength = nnmap.getLength(); for (int i = 0; i < nnlength; i++) { Attr attr = (Attr) nnmap.item(i); elem.setAttributeNode((Attr) attr.cloneNode(true)); } } } synchronized (this) { if ((userDataHandlers != null) && (userData != null)) { for (Iterator handlers = userDataHandlers.entrySet() .iterator(); handlers.hasNext();) { Map.Entry entry = (Map.Entry) handlers.next(); UserDataHandler handler = (UserDataHandler) entry .getValue(); handler.handle(UserDataHandler.NODE_CLONED, (String) entry.getKey(), userData.get(entry.getKey()), this, newNode); } } } return newNode; } catch (Exception err) { throw new IllegalStateException(err.getMessage()); } } /** Gets the node index. * * @return the node index */ private int getNodeIndex() { DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode(); return parent == null ? -1 : parent.getChildIndex(this); } /** * Gets the child index. * * @param child * the child * @return the child index */ public int getChildIndex(Node child) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; return nl == null ? -1 : nl.indexOf(child); } } /** * Gets the child at index. * * @param index * the index * @return the child at index */ public Node getChildAtIndex(int index) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; try { return nl == null ? null : (Node) nl.get(index); } catch (IndexOutOfBoundsException iob) { logger.error("getChildAtIndex(): Bad index=" + index + " for node=" + this + "."); return null; } } } /** * Checks if is ancestor of. * * @param other * the other * @return true, if is ancestor of */ private boolean isAncestorOf(Node other) { DOMNodeImpl parent = (DOMNodeImpl) other.getParentNode(); if (parent == this) { return true; } else if (parent == null) { return false; } else { return this.isAncestorOf(parent); } } /* * (non-Javadoc) * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node) */ @Override public short compareDocumentPosition(Node other) throws DOMException { Node parent = this.getParentNode(); if (!(other instanceof DOMNodeImpl)) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Unknwon node implementation"); } if ((parent != null) && (parent == other.getParentNode())) { int thisIndex = this.getNodeIndex(); int otherIndex = ((DOMNodeImpl) 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 (((DOMNodeImpl) other).isAncestorOf(this)) { return Node.DOCUMENT_POSITION_CONTAINS; } else { return Node.DOCUMENT_POSITION_DISCONNECTED; } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getAttributes() */ @Override public NamedNodeMap getAttributes() { return null; } /* * (non-Javadoc) * @see org.w3c.dom.Node#getOwnerDocument() */ @Override public Document getOwnerDocument() { return this.document; } /** Sets the owner document. * * @param value * the new owner document */ public void setOwnerDocument(Document value) { this.document = value; this.setTreeLock(value == null ? this : (Object) value); } /** * Sets the owner document. * * @param value * the value * @param deep * the deep */ public void setOwnerDocument(Document value, boolean deep) { this.document = value; this.setTreeLock(value == null ? this : (Object) value); if (deep) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { DOMNodeImpl child = (DOMNodeImpl) i.next(); child.setOwnerDocument(value, deep); } } } } } /** * Visit impl. * * @param visitor * the visitor */ protected void visitImpl(NodeVisitor visitor) { try { visitor.visit(this); } catch (SkipVisitorException sve) { return; } catch (StopVisitorException sve) { throw sve; } ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { DOMNodeImpl child = (DOMNodeImpl) i.next(); try { // Call with child's synchronization child.visit(visitor); } catch (StopVisitorException sve) { throw sve; } } } } /** * Visit. * * @param visitor * the visitor */ public void visit(NodeVisitor visitor) { synchronized (this.getTreeLock()) { this.visitImpl(visitor); } } /* * (non-Javadoc) * @see org.w3c.dom.Node#insertBefore(org.w3c.dom.Node, org.w3c.dom.Node) */ @Override public Node insertBefore(Node newChild, Node refChild) throws DOMException { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; int idx = 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 DOMNodeImpl) { ((DOMNodeImpl) newChild).setParentImpl(this); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } return newChild; } /** * Insert at. * * @param newChild * the new child * @param idx * the idx * @return the node * @throws DOMException * the DOM exception */ protected Node insertAt(Node newChild, int idx) throws DOMException { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl == null) { nl = new ArrayList<Node>(); this.nodeList = nl; } nl.add(idx, newChild); if (newChild instanceof DOMNodeImpl) { ((DOMNodeImpl) newChild).setParentImpl(this); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } return newChild; } /* * (non-Javadoc) * @see org.w3c.dom.Node#replaceChild(org.w3c.dom.Node, org.w3c.dom.Node) */ @Override public Node replaceChild(Node newChild, Node oldChild) throws DOMException { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; 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 (!this.notificationsSuspended) { this.informStructureInvalid(); } return newChild; } /* * (non-Javadoc) * @see org.w3c.dom.Node#removeChild(org.w3c.dom.Node) */ @Override public Node removeChild(Node oldChild) throws DOMException { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if ((nl == null) || !nl.remove(oldChild)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "oldChild not found"); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } return oldChild; } /** * Removes the child at. * * @param index * the index * @return the node * @throws DOMException * the DOM exception */ public Node removeChildAt(int index) throws DOMException { try { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl == null) { throw new DOMException(DOMException.INDEX_SIZE_ERR, "Empty list of children"); } Node n = nl.remove(index); if (n == null) { throw new DOMException(DOMException.INDEX_SIZE_ERR, "No node with that index"); } return n; } } finally { if (!this.notificationsSuspended) { this.informStructureInvalid(); } } } /* * (non-Javadoc) * @see org.w3c.dom.Node#hasChildNodes() */ @Override public boolean hasChildNodes() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; return (nl != null) && !nl.isEmpty(); } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getBaseURI() */ @Override public String getBaseURI() { Document document = this.document; return document == null ? null : document.getBaseURI(); } /* * (non-Javadoc) * @see org.w3c.dom.Node#getChildNodes() */ @Override public NodeList getChildNodes() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; return new DOMNodeListImpl(nl == null ? Collections.EMPTY_LIST : nl); } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getFirstChild() */ @Override public Node getFirstChild() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; try { return nl == null ? null : (Node) nl.get(0); } catch (IndexOutOfBoundsException iob) { return null; } } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getLastChild() */ @Override public Node getLastChild() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; try { return nl == null ? null : (Node) nl.get(nl.size() - 1); } catch (IndexOutOfBoundsException iob) { return null; } } } /** * Gets the previous to. * * @param node * the node * @return the previous to */ private Node getPreviousTo(Node node) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; 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 (IndexOutOfBoundsException iob) { return null; } } } /** * Gets the next to. * * @param node * the node * @return the next to */ private Node getNextTo(Node node) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; 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 (IndexOutOfBoundsException iob) { return null; } } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getPreviousSibling() */ @Override public Node getPreviousSibling() { DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode(); return parent == null ? null : parent.getPreviousTo(this); } /* * (non-Javadoc) * @see org.w3c.dom.Node#getNextSibling() */ @Override public Node getNextSibling() { DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode(); return parent == null ? null : parent.getNextTo(this); } /* * (non-Javadoc) * @see org.w3c.dom.Node#getFeature(java.lang.String, java.lang.String) */ @Override public Object getFeature(String feature, String version) { // TODO What should this do? return null; } /** The user data. */ private Map<String, Object> userData; // TODO: Inform handlers on cloning, etc. /** The user data handlers. */ private Map<String, UserDataHandler> userDataHandlers; /** The notifications suspended. */ protected volatile boolean notificationsSuspended = false; /* * (non-Javadoc) * @see org.w3c.dom.Node#setUserData(java.lang.String, java.lang.Object, * org.w3c.dom.UserDataHandler) */ @Override public Object setUserData(String key, Object data, UserDataHandler handler) { if (org.lobobrowser.html.parser.HtmlParser.MODIFYING_KEY.equals(key)) { 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<String, UserDataHandler>(); } else { this.userDataHandlers.put(key, handler); } } Map<String, Object> userData = this.userData; if (data != null) { if (userData == null) { userData = new HashMap<String, Object>(); this.userData = userData; } return userData.put(key, data); } else if (userData != null) { return userData.remove(key); } else { return null; } } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getUserData(java.lang.String) */ @Override public Object getUserData(String key) { synchronized (this) { Map<String, Object> ud = this.userData; return ud == null ? null : ud.get(key); } } /* * (non-Javadoc) * @see org.w3c.dom.Node#getLocalName() */ @Override public abstract String getLocalName(); /* * (non-Javadoc) * @see org.w3c.dom.Node#hasAttributes() */ @Override public boolean hasAttributes() { return false; } /* * (non-Javadoc) * @see org.w3c.dom.Node#getNamespaceURI() */ @Override public String getNamespaceURI() { return null; } /* * (non-Javadoc) * @see org.w3c.dom.Node#getNodeName() */ @Override public abstract String getNodeName(); /* * (non-Javadoc) * @see org.w3c.dom.Node#getNodeValue() */ @Override public abstract String getNodeValue() throws DOMException; /** The prefix. */ private volatile String prefix; /* * (non-Javadoc) * @see org.w3c.dom.Node#getPrefix() */ @Override public String getPrefix() { return this.prefix; } /* * (non-Javadoc) * @see org.w3c.dom.Node#setPrefix(java.lang.String) */ @Override public void setPrefix(String prefix) throws DOMException { this.prefix = prefix; } /* * (non-Javadoc) * @see org.w3c.dom.Node#setNodeValue(java.lang.String) */ @Override public abstract void setNodeValue(String nodeValue) throws DOMException; /* * (non-Javadoc) * @see org.w3c.dom.Node#getNodeType() */ @Override public abstract short getNodeType(); /** * Gets the text content of this node and its descendents. * * @return the text content * @throws DOMException * the DOM exception */ @Override public String getTextContent() throws DOMException { StringBuffer sb = new StringBuffer(); synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { Node node = i.next(); short type = node.getNodeType(); switch (type) { case Node.CDATA_SECTION_NODE: case Node.TEXT_NODE: case Node.ELEMENT_NODE: String textContent = node.getTextContent(); if (textContent != null) { sb.append(textContent); } break; default: break; } } } } return sb.toString(); } /* * (non-Javadoc) * @see org.w3c.dom.Node#setTextContent(java.lang.String) */ @Override public void setTextContent(String textContent) throws DOMException { synchronized (this.getTreeLock()) { this.removeChildrenImpl(new TextFilter()); if ((textContent != null) && !"".equals(textContent)) { DOMTextImpl t = new DOMTextImpl(textContent); t.setOwnerDocument(this.document); t.setParentImpl(this); ArrayList<Node> nl = this.nodeList; if (nl == null) { nl = new ArrayList<Node>(); this.nodeList = nl; } nl.add(t); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } } /** * Removes the children. * * @param filter * the filter */ protected void removeChildren(NodeFilter filter) { synchronized (this.getTreeLock()) { this.removeChildrenImpl(filter); } if (!this.notificationsSuspended) { this.informStructureInvalid(); } } /** * Removes the children impl. * * @param filter * the filter */ protected void removeChildrenImpl(NodeFilter filter) { ArrayList<Node> nl = this.nodeList; if (nl != null) { int len = nl.size(); for (int i = len;--i >= 0;) { Node node = nl.get(i); if (filter.accept(node)) { nl.remove(i); } } } } /** * Insert after. * * @param newChild * the new child * @param refChild * the ref child * @return the node */ public Node insertAfter(Node newChild, Node refChild) { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; 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 DOMNodeImpl) { ((DOMNodeImpl) newChild).setParentImpl(this); } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } return newChild; } /** * Replace adjacent text nodes. * * @param node * the node * @param textContent * the text content * @return the text */ public Text replaceAdjacentTextNodes(Text node, String textContent) { try { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl == null) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child"); } int idx = nl.indexOf(node); if (idx == -1) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child"); } int firstIdx = idx; List<Object> toDelete = new LinkedList<Object>(); for (int adjIdx = idx;--adjIdx >= 0;) { Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { firstIdx = adjIdx; toDelete.add(child); } } int length = this.nodeList.size(); for (int adjIdx = idx; ++adjIdx < length;) { Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { toDelete.add(child); } } this.nodeList.removeAll(toDelete); DOMTextImpl textNode = new DOMTextImpl(textContent); textNode.setOwnerDocument(this.document); textNode.setParentImpl(this); this.nodeList.add(firstIdx, textNode); return textNode; } } finally { if (!this.notificationsSuspended) { this.informStructureInvalid(); } } } /** * Replace adjacent text nodes. * * @param node * the node * @return the text */ public Text replaceAdjacentTextNodes(Text node) { try { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl == null) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child"); } int idx = nl.indexOf(node); if (idx == -1) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Node not a child"); } StringBuffer textBuffer = new StringBuffer(); int firstIdx = idx; List<Object> toDelete = new LinkedList<Object>(); for (int adjIdx = idx;--adjIdx >= 0;) { Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { firstIdx = adjIdx; toDelete.add(child); textBuffer.append(((Text) child).getNodeValue()); } } int length = this.nodeList.size(); for (int adjIdx = idx; ++adjIdx < length;) { Object child = this.nodeList.get(adjIdx); if (child instanceof Text) { toDelete.add(child); textBuffer.append(((Text) child).getNodeValue()); } } this.nodeList.removeAll(toDelete); DOMTextImpl textNode = new DOMTextImpl(textBuffer.toString()); textNode.setOwnerDocument(this.document); textNode.setParentImpl(this); this.nodeList.add(firstIdx, textNode); return textNode; } } finally { if (!this.notificationsSuspended) { this.informStructureInvalid(); } } } /** The parent node. */ protected volatile Node parentNode; /* * (non-Javadoc) * @see org.w3c.dom.Node#getParentNode() */ @Override public Node getParentNode() { // Should it be synchronized? Could have side-effects. return this.parentNode; } /* * (non-Javadoc) * @see org.w3c.dom.Node#isSameNode(org.w3c.dom.Node) */ @Override public boolean isSameNode(Node other) { return this == other; } /* * (non-Javadoc) * @see org.w3c.dom.Node#isSupported(java.lang.String, java.lang.String) */ @Override public boolean isSupported(String feature, String version) { return ("HTML".equals(feature) && (version.compareTo("4.01") <= 0)); } /* * (non-Javadoc) * @see org.w3c.dom.Node#lookupNamespaceURI(java.lang.String) */ @Override public String lookupNamespaceURI(String prefix) { return null; } /** * Equal attributes. * * @param arg * the arg * @return true, if successful */ public boolean equalAttributes(Node arg) { return false; } /* * (non-Javadoc) * @see org.w3c.dom.Node#isEqualNode(org.w3c.dom.Node) */ @Override public boolean isEqualNode(Node arg) { return (arg instanceof DOMNodeImpl) && (this.getNodeType() == arg.getNodeType()) && Objects.equals(this.getNodeName(), arg.getNodeName()) && Objects.equals(this.getNodeValue(), arg.getNodeValue()) && Objects.equals(this.getLocalName(), arg.getLocalName()) && Objects.equals(this.nodeList, ((DOMNodeImpl) arg).nodeList) && this.equalAttributes(arg); } /* * (non-Javadoc) * @see org.w3c.dom.Node#isDefaultNamespace(java.lang.String) */ @Override public boolean isDefaultNamespace(String namespaceURI) { return namespaceURI == null; } /* * (non-Javadoc) * @see org.w3c.dom.Node#lookupPrefix(java.lang.String) */ @Override public String lookupPrefix(String namespaceURI) { return null; } /* * (non-Javadoc) * @see org.w3c.dom.Node#normalize() */ @Override public void normalize() { synchronized (this.getTreeLock()) { ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); List<Node> textNodes = new LinkedList<Node>(); boolean prevText = false; while (i.hasNext()) { 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()) { Text text = (Text) i.next(); this.replaceAdjacentTextNodes(text); } } } if (!this.notificationsSuspended) { this.informStructureInvalid(); } } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return this.getNodeName(); } /** Gets the user agent context. * * @return the user agent context */ public UserAgentContext getUserAgentContext() { Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getUserAgentContext(); } else { return null; } } /** Gets the html renderer context. * * @return the html renderer context */ public HtmlRendererContext getHtmlRendererContext() { Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getHtmlRendererContext(); } else { return null; } } /** Sets the parent impl. * * @param parent * the new parent impl */ protected final void setParentImpl(Node parent) { // Call holding treeLock. this.parentNode = parent; } //-----ModelNode implementation /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#getAlignmentX() */ /** Gets the alignment x. * * @return the alignment x */ public float getAlignmentX() { // TODO: Removable method? return 0.5f; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#getAlignmentY() */ /** Gets the alignment y. * * @return the alignment y */ public float getAlignmentY() { return 0.5f; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#getFullURL(String) */ @Override public URL getFullURL(String spec) throws MalformedURLException { Object doc = this.document; String cleanSpec = Urls.encodeIllegalCharacters(spec); if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getFullURL(cleanSpec); } else { return new URL(cleanSpec); } } /** Gets the document url. * * @return the document url */ public URL getDocumentURL() { Object doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getDocumentURL(); } else { return null; } } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#getDocumentItem( String) */ @Override public Object getDocumentItem(String name) { org.w3c.dom.Document document = this.document; return document == null ? null : document.getUserData(name); } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#setDocumentItem( String, * java.lang.Object) */ @Override public void setDocumentItem(String name, Object value) { org.w3c.dom.Document document = this.document; if (document == null) { return; } document.setUserData(name, value, null); } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RenderableContext#isEqualOrDescendentOf(org. * xamjwg.html.renderer.RenderableContext) */ @Override public final boolean isEqualOrDescendentOf(ModelNode otherContext) { if (otherContext == this) { return true; } Object parent = this.getParentNode(); if (parent instanceof HTMLElementImpl) { return ((HTMLElementImpl) parent) .isEqualOrDescendentOf(otherContext); } else { return false; } } /* * (non-Javadoc) * @see org.lobobrowser.html.dombl.ModelNode#getParentModelNode() */ @Override public final ModelNode getParentModelNode() { return (ModelNode) this.parentNode; } /** * Inform size invalid. */ public void informSizeInvalid() { HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.sizeInvalidated(this); } } /** * Inform look invalid. */ public void informLookInvalid() { this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.lookInvalidated(this); } } /** * Inform position invalid. */ public void informPositionInvalid() { HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.positionInParentInvalidated(this); } } /** * Inform invalid. */ public void informInvalid() { // This is called when an attribute or child changes. this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.invalidated(this); } } /** * Inform structure invalid. */ public void informStructureInvalid() { // This is called when an attribute or child changes. this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.structureInvalidated(this); } } /** * Inform node loaded. */ protected void informNodeLoaded() { // This is called when an attribute or child changes. this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.nodeLoaded(this); } } /** * Inform external script loading. */ protected void informExternalScriptLoading() { // This is called when an attribute or child changes. this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.externalScriptLoading(this); } } /** * Inform layout invalid. */ public void informLayoutInvalid() { // This is called by the style properties object. this.forgetRenderState(); HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.invalidated(this); } } /** * Inform document invalid. */ public void informDocumentInvalid() { // This is called when an attribute or child changes. HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; if (doc != null) { doc.allInvalidated(true); } } /** The render state. */ private RenderState renderState = INVALID_RENDER_STATE; /* * (non-Javadoc) * @see org.lobobrowser.html.dombl.ModelNode#getRenderState() */ @Override public RenderState getRenderState() { // Generally called from the GUI thread, except for // offset properties. RenderState rs; synchronized (this.getTreeLock()) { rs = this.renderState; if (rs != INVALID_RENDER_STATE) { return rs; } Object parent = this.parentNode; if ((parent != null) || (this instanceof Document)) { RenderState prs = this.getParentRenderState(parent); rs = this.createRenderState(prs); this.renderState = rs; return rs; } else { // Return null without caching. // Scenario is possible due to Javascript. return null; } } } /** * Gets the parent render state. * * @param parent * the parent * @return the parent render state */ protected final RenderState getParentRenderState(Object parent) { if (parent instanceof DOMNodeImpl) { return ((DOMNodeImpl) parent).getRenderState(); } else { return null; } } /** * Creates the render state. * * @param prevRenderState * the prev render state * @return the render state */ protected RenderState createRenderState(RenderState prevRenderState) { return prevRenderState; } /** * Forget render state. */ protected void forgetRenderState() { synchronized (this.getTreeLock()) { if (this.renderState != INVALID_RENDER_STATE) { this.renderState = INVALID_RENDER_STATE; // Note that getRenderState() "validates" // ancestor states as well. ArrayList<Node> nl = this.nodeList; if (nl != null) { Iterator<Node> i = nl.iterator(); while (i.hasNext()) { ((DOMNodeImpl) i.next()).forgetRenderState(); } } } } } /** Gets the inner html. * * @return the inner html */ public String getInnerHTML() { StringBuffer buffer = new StringBuffer(); synchronized (this) { this.appendInnerHTMLImpl(buffer); } return buffer.toString(); } /** * Append inner html impl. * * @param buffer * the buffer */ protected void appendInnerHTMLImpl(StringBuffer buffer) { ArrayList<Node> nl = this.nodeList; int size; if ((nl != null) && ((size = nl.size()) > 0)) { for (int i = 0; i < size; i++) { 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) { String text = ((Text) child).getTextContent(); String encText = this.htmlEncodeChildText(text); buffer.append(encText); } else if (child instanceof ProcessingInstruction) { buffer.append(child.toString()); } } } } /** * Html encode child text. * * @param text * the text * @return the string */ protected String htmlEncodeChildText(String text) { return Strings.strictHtmlEncode(text, false); } /** Gets the inner text. * * @return the inner text */ public String getInnerText() { StringBuffer buffer = new StringBuffer(); synchronized (this.getTreeLock()) { this.appendInnerTextImpl(buffer); } return buffer.toString(); } /** * Append inner text impl. * * @param buffer * the buffer */ protected void appendInnerTextImpl(StringBuffer buffer) { ArrayList<Node> nl = this.nodeList; if (nl == null) { return; } int size = nl.size(); if (size == 0) { return; } for (int i = 0; i < size; i++) { Node child = nl.get(i); if (child instanceof DOMElementImpl) { ((DOMElementImpl) child).appendInnerTextImpl(buffer); } if (child instanceof Comment) { // skip } else if (child instanceof Text) { buffer.append(((Text) child).getTextContent()); } } } /** Gets the a tree lock is less deadlock-prone than a node-level lock. * * @return the a tree lock is less deadlock-prone than a node-level lock */ public Object getTreeLock() { return treeLock; } /** Sets the a tree lock is less deadlock-prone than a node-level lock. * * @param treeLock * the new a tree lock is less deadlock-prone than a node-level * lock */ public void setTreeLock(Object treeLock) { this.treeLock = treeLock; } }