org.cobraparser.html.domimpl.ElementImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Cobra Show documentation
Show all versions of Cobra Show documentation
Cobra is the rendering engine designed for LoboBrowser
/*
GNU LESSER GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: [email protected]
*/
/*
* Created on Oct 29, 2005
*/
package org.cobraparser.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.cobraparser.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 attributes;
/*
* (non-Javadoc)
*
* @see org.xamjwg.html.domimpl.NodeImpl#getattributes()
*/
@Override
public NamedNodeMap getAttributes() {
synchronized (this) {
Map 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 attrs = this.attributes;
return attrs == null ? false : !attrs.isEmpty();
}
}
@Override
public boolean equalAttributes(final Node arg) {
if (arg instanceof ElementImpl) {
synchronized (this) {
Map attrs1 = this.attributes;
if (attrs1 == null) {
attrs1 = Collections.emptyMap();
}
Map 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 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 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 descendents = new LinkedList<>();
synchronized (this.treeLock) {
final ArrayList nl = this.nodeList;
if (nl != null) {
final Iterator 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 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 nl = this.nodeList;
if (nl != null) {
final Iterator 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.cobraparser.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 nl = this.nodeList;
for (final Node n : nl) {
if (n instanceof Element) {
return (Element) n;
}
}
return null;
}
public Element getLastElementChild() {
final ArrayList 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 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;
}
}