/* 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 Oct 29, 2005 */ package org.lobobrowser.html.domimpl; import java.util.ArrayList; 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.lobobrowser.util.Strings; import org.w3c.dom.Attr; import org.w3c.dom.Comment; import org.w3c.dom.DOMException; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.w3c.dom.TypeInfo; import org.w3c.dom.events.Event; import org.w3c.dom.events.EventException; import org.w3c.dom.events.EventListener; import org.w3c.dom.events.EventTarget; public class ElementImpl extends NodeImpl implements Element, EventTarget { private final String name; public ElementImpl(final String name) { super(); this.name = name; } protected Map<String, String> attributes; /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getattributes() */ @Override public NamedNodeMap getAttributes() { synchronized (this) { Map<String, String> attrs = this.attributes; // TODO: Check if NamedNodeMapImpl can be changed to dynamically query the attributes field // instead of keeping a reference to it. This will allow the NamedNodeMap to be live as well // as avoid allocating of a HashMap here when attributes are empty. if (attrs == null) { attrs = new HashMap<>(); this.attributes = attrs; } return new NamedNodeMapImpl(this, this.attributes); } } @Override public boolean hasAttributes() { synchronized (this) { final Map<String, String> attrs = this.attributes; return attrs == null ? false : !attrs.isEmpty(); } } @Override public boolean equalAttributes(final Node arg) { if (arg instanceof ElementImpl) { synchronized (this) { Map<String, String> attrs1 = this.attributes; if (attrs1 == null) { attrs1 = Collections.emptyMap(); } Map<String, String> attrs2 = ((ElementImpl) arg).attributes; if (attrs2 == null) { attrs2 = Collections.emptyMap(); } return java.util.Objects.equals(attrs1, attrs2); } } else { return false; } } public String getId() { // TODO: Check if a cache is useful for this attribute. Original gngr code had a cache here. final String id = this.getAttribute("id"); return id == null ? "" : id; } public void setId(final String id) { this.setAttribute("id", id); } // private String title; public String getTitle() { return this.getAttribute("title"); } public void setTitle(final String title) { this.setAttribute("title", title); } public String getLang() { return this.getAttribute("lang"); } public void setLang(final String lang) { this.setAttribute("lang", lang); } public String getDir() { return this.getAttribute("dir"); } public void setDir(final String dir) { this.setAttribute("dir", dir); } public final String getAttribute(final String name) { final String normalName = normalizeAttributeName(name); synchronized (this) { final Map<String, String> attributes = this.attributes; return attributes == null ? null : attributes.get(normalName); } } private Attr getAttr(final String normalName, final String value) { // TODO: "specified" attributes return new AttrImpl(normalName, value, true, this, "id".equals(normalName)); } public Attr getAttributeNode(final String name) { final String normalName = normalizeAttributeName(name); synchronized (this) { final Map<String, String> attributes = this.attributes; final String value = attributes == null ? null : attributes.get(normalName); return value == null ? null : this.getAttr(normalName, value); } } public Attr getAttributeNodeNS(final String namespaceURI, final String localName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public String getAttributeNS(final String namespaceURI, final String localName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } protected static boolean isTagName(final Node node, final String name) { return node.getNodeName().equalsIgnoreCase(name); } public NodeList getElementsByTagName(final String name) { final boolean matchesAll = "*".equals(name); final List<Node> descendents = new LinkedList<>(); synchronized (this.treeLock) { final ArrayList<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> i = nl.iterator(); while (i.hasNext()) { final Node child = i.next(); if (child instanceof Element) { final Element childElement = (Element) child; if (matchesAll || isTagName(childElement, name)) { descendents.add(child); } final NodeList sublist = childElement.getElementsByTagName(name); final int length = sublist.getLength(); for (int idx = 0; idx < length; idx++) { descendents.add(sublist.item(idx)); } } } } } return new NodeListImpl(descendents); } public NodeList getElementsByTagNameNS(final String namespaceURI, final String localName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public TypeInfo getSchemaTypeInfo() { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public String getTagName() { // In HTML, tag names are supposed to be returned in upper-case, but in XHTML they are returned in original case // as per https://developer.mozilla.org/en-US/docs/Web/API/Element.tagName return this.getNodeName().toUpperCase(); } public boolean hasAttribute(final String name) { final String normalName = normalizeAttributeName(name); synchronized (this) { final Map<String, String> attributes = this.attributes; return attributes == null ? false : attributes.containsKey(normalName); } } public boolean hasAttributeNS(final String namespaceURI, final String localName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public void removeAttribute(final String name) throws DOMException { changeAttribute(name, null); } public Attr removeAttributeNode(final Attr oldAttr) throws DOMException { final String attrName = oldAttr.getName(); final String oldValue = changeAttribute(attrName, null); final String normalName = normalizeAttributeName(attrName); return oldValue == null ? null : this.getAttr(normalName, oldValue); } public void removeAttributeNS(final String namespaceURI, final String localName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } /* protected void assignAttributeField(final String normalName, final String value) { // Note: overriders assume that processing here is only done after // checking attribute names, i.e. they may not call the super // implementation if an attribute is already taken care of. // TODO: Need to move this to a separate function, similar to updateIdMap() // TODO: Need to update the name map, whenever attachment changes if (isAttachedToDocument()) { final HTMLDocumentImpl document = (HTMLDocumentImpl) this.document; if ("name".equals(normalName)) { final String oldName = this.getAttribute("name"); if (oldName != null) { document.removeNamedItem(oldName); } document.setNamedItem(value, this); } } }*/ protected final static String normalizeAttributeName(final String name) { return name.toLowerCase(); } public void setAttribute(final String name, final String value) throws DOMException { // Convert null to "null" : String. // This is how Firefox behaves and is also consistent with DOM 3 final String valueNonNull = value == null ? "null" : value; changeAttribute(name, valueNonNull); } public Attr setAttributeNode(final Attr newAttr) throws DOMException { changeAttribute(newAttr.getName(), newAttr.getValue()); return newAttr; } public Attr setAttributeNodeNS(final Attr newAttr) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } public void setIdAttribute(final String name, final boolean isId) throws DOMException { final String normalName = normalizeAttributeName(name); if (!"id".equals(normalName)) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "IdAttribute can't be anything other than ID"); } } public void setIdAttributeNode(final Attr idAttr, final boolean isId) throws DOMException { final String normalName = normalizeAttributeName(idAttr.getName()); if (!"id".equals(normalName)) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "IdAttribute can't be anything other than ID"); } } public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Namespaces not supported"); } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getLocalName() */ @Override public String getLocalName() { return this.getNodeName(); } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeName() */ @Override public String getNodeName() { return this.name; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeType() */ @Override public short getNodeType() { return Node.ELEMENT_NODE; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeValue() */ @Override public String getNodeValue() throws DOMException { return null; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#setNodeValue(java.lang.String) */ @Override public void setNodeValue(final String nodeValue) throws DOMException { // nop } /** * Gets inner text of the element, possibly including text in comments. This * can be used to get Javascript code out of a SCRIPT element. * * @param includeComment */ protected String getRawInnerText(final boolean includeComment) { synchronized (this.treeLock) { final ArrayList<Node> nl = this.nodeList; if (nl != null) { final Iterator<Node> i = nl.iterator(); StringBuffer sb = null; while (i.hasNext()) { final Object node = i.next(); if (node instanceof Text) { final Text tn = (Text) node; final String txt = tn.getNodeValue(); if (!"".equals(txt)) { if (sb == null) { sb = new StringBuffer(); } sb.append(txt); } } else if (node instanceof ElementImpl) { final ElementImpl en = (ElementImpl) node; final String txt = en.getRawInnerText(includeComment); if (!"".equals(txt)) { if (sb == null) { sb = new StringBuffer(); } sb.append(txt); } } else if (includeComment && (node instanceof Comment)) { final Comment cn = (Comment) node; final String txt = cn.getNodeValue(); if (!"".equals(txt)) { if (sb == null) { sb = new StringBuffer(); } sb.append(txt); } } } return sb == null ? "" : sb.toString(); } else { return ""; } } } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(this.getNodeName()); sb.append(" ["); final NamedNodeMap attribs = this.getAttributes(); final int length = attribs.getLength(); for (int i = 0; i < length; i++) { final Attr attr = (Attr) attribs.item(i); sb.append(attr.getNodeName()); sb.append('='); sb.append(attr.getNodeValue()); if ((i + 1) < length) { sb.append(','); } } sb.append("]"); return sb.toString(); } public void setInnerText(final String newText) { // TODO: Is this check for owner document really required? final org.w3c.dom.Document document = this.document; if (document == null) { this.warn("setInnerText(): Element " + this + " does not belong to a document."); return; } removeAllChildrenImpl(); // Create node and call appendChild outside of synchronized block. final Node textNode = document.createTextNode(newText); this.appendChild(textNode); } @Override protected Node createSimilarNode() { final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; return doc == null ? null : doc.createElement(this.getTagName()); } @Override protected String htmlEncodeChildText(final String text) { if (org.lobobrowser.html.parser.HtmlParser.isDecodeEntities(this.name)) { return Strings.strictHtmlEncode(text, false); } else { return text; } } /** * To be overridden by Elements that need a notification of attribute changes. * * This is called only when the element is attached to a document at the time * the attribute is changed. If an attribute is changed while not attached to * a document, this function is *not* called when the element is attached to a * document. We chose this design because it covers our current use cases * well. * * If, in the future, a notification is always desired then the design can be * altered easily later. * * @param name * normalized name * @param oldValue * null, if the attribute was absent * @param newValue * null, if the attribute is now removed */ protected void handleAttributeChanged(final String name, final String oldValue, final String newValue) { // TODO: Need to move this to a separate function, similar to updateIdMap() // TODO: Need to update the name map, whenever attachment changes final HTMLDocumentImpl document = (HTMLDocumentImpl) this.document; if ("name".equals(name)) { if (oldValue != null) { document.removeNamedItem(oldValue); } document.setNamedItem(newValue, this); } } /** * changes an attribute to the specified value. If the specified value is * null, the attribute is removed * * @return the old attribute value. null if not set previously. */ private String changeAttribute(final String name, final String newValue) { final String normalName = normalizeAttributeName(name); String oldValue = null; synchronized (this) { if (newValue == null) { if (attributes != null) { oldValue = attributes.remove(normalName); } } else { if (attributes == null) { attributes = new HashMap<>(2); } oldValue = attributes.put(normalName, newValue); } } if ("id".equals(normalName)) { updateIdMap(oldValue, newValue); } if (isAttachedToDocument()) { handleAttributeChanged(normalName, oldValue, newValue); } return oldValue; } protected void updateIdMap(final boolean isAttached) { if (hasAttribute("id")) { final String id = getId(); if (isAttached) { ((HTMLDocumentImpl) document).setElementById(id, this); } else { ((HTMLDocumentImpl) document).removeElementById(getId()); } } } private void updateIdMap(final String oldIdValue, final String newIdValue) { if (isAttachedToDocument() && !java.util.Objects.equals(oldIdValue, newIdValue)) { if (oldIdValue != null) { ((HTMLDocumentImpl) document).removeElementById(oldIdValue); } if (newIdValue != null) { ((HTMLDocumentImpl) document).setElementById(newIdValue, this); } } } // TODO: GH #88 Need to implement these for Document and DocumentFragment as part of ParentNode API public Element getFirstElementChild() { final ArrayList<Node> nl = this.nodeList; for (final Node n : nl) { if (n instanceof Element) { return (Element) n; } } return null; } public Element getLastElementChild() { final ArrayList<Node> nl = this.nodeList; final int N = nl.size(); for (int i = N - 1; i >= 0; i--) { final Node n = nl.get(i); if (n instanceof Element) { return (Element) n; } } return null; } public int getChildElementCount() { final ArrayList<Node> nl = this.nodeList; int count = 0; for (final Node n : nl) { if (n instanceof Element) { count++; } } return count; } @Override public void addEventListener(String type, EventListener listener, boolean useCapture) { // TODO Auto-generated method stub System.out.println("TODO: addEventListener() in ElementImpl"); } @Override public void removeEventListener(String type, EventListener listener, boolean useCapture) { // TODO Auto-generated method stub System.out.println("TODO: removeEventListener() in ElementImpl"); } @Override public boolean dispatchEvent(Event evt) throws EventException { // TODO Auto-generated method stub System.out.println("TODO: dispatchEvent() in ElementImpl"); return false; } }