/*
* 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 com.nvarghese.beowulf.common.cobra.html.domimpl;
// org.cobra_grendel.html.style.*;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.html2.HTMLElement;
import com.nvarghese.beowulf.common.cobra.html.FormInput;
import com.nvarghese.beowulf.common.cobra.html.parser.HtmlParser;
import com.nvarghese.beowulf.common.cobra.util.Strings;
public class HTMLElementImpl extends ElementImpl implements HTMLElement /*
* ,
* CSS2PropertiesContext
*/{
private final boolean noStyleSheet;
public HTMLElementImpl(String name, boolean noStyleSheet) {
super(name);
this.noStyleSheet = noStyleSheet;
}
public HTMLElementImpl(String name) {
super(name);
this.noStyleSheet = false;
}
/*
* private volatile AbstractCSS2Properties currentStyleDeclarationState;
* private volatile AbstractCSS2Properties localStyleDeclarationState;
*
* protected final void forgetLocalStyle() { synchronized(this) {
* this.currentStyleDeclarationState = null; this.localStyleDeclarationState
* = null; this.computedStyles = null; } }
*
*
* protected final void forgetStyle(boolean deep) { //TODO: OPTIMIZATION: If
* we had a ComputedStyle map in //window (Mozilla model) the map could be
* cleared in one shot. synchronized(this) {
* this.currentStyleDeclarationState = null; this.computedStyles = null;
* this.isHoverStyle = null; this.hasHoverStyleByElement = null; if(deep) {
* java.util.ArrayList nl = this.nodeList; if(nl != null) { Iterator i =
* nl.iterator(); while(i.hasNext()) { Object node = i.next(); if(node
* instanceof HTMLElementImpl) { ((HTMLElementImpl) node).forgetStyle(deep);
* } } } } } }
*
* /** Gets the style object associated with the element. It may return null
* only if the type of element does not handle stylesheets.
*/
/*
* public AbstractCSS2Properties getCurrentStyle() { AbstractCSS2Properties
* sds; synchronized(this) { sds = this.currentStyleDeclarationState; if(sds
* != null) { return sds; } } // Can't do the following in synchronized
* block (reverse locking order with document). // First, add declarations
* from stylesheet sds = this.createDefaultStyleSheet(); sds =
* this.addStyleSheetDeclarations(sds, this.getPseudoNames()); // Now add
* local style if any. AbstractCSS2Properties localStyle = this.getStyle();
* if(sds == null) { sds = new ComputedCSS2Properties(this);
* sds.setLocalStyleProperties(localStyle); } else {
* sds.setLocalStyleProperties(localStyle); } synchronized(this) { // Check
* if style properties were set while outside // the synchronized block (can
* happen). AbstractCSS2Properties setProps =
* this.currentStyleDeclarationState; if(setProps != null) { return
* setProps; } this.currentStyleDeclarationState = sds; return sds; } }
*/
/**
* Gets the local style object associated with the element. The properties
* object returned only includes properties from the local style attribute.
* It may return null only if the type of element does not handle
* stylesheets.
*/
/*
* public AbstractCSS2Properties getStyle() { AbstractCSS2Properties sds;
* synchronized(this) { sds = this.localStyleDeclarationState; if(sds !=
* null) { return sds; } sds = new LocalCSS2Properties(this); // Add any
* declarations in style attribute (last takes precedence). String style =
* this.getAttribute("style"); if(style != null && style.length() != 0) {
* CSSOMParser parser = new CSSOMParser(); InputSource inputSource =
* this.getCssInputSourceForDecl(style); try { CSSStyleDeclaration sd =
* parser.parseStyleDeclaration(inputSource); sds.addStyleDeclaration(sd); }
* catch(Exception err) { String id = this.getId(); String withId = id ==
* null ? "" : " with ID '" + id + "'";
* this.warn("Unable to parse style attribute value for element " +
* this.getTagName() + withId + " in " + this.getDocumentURL() + ".", err);
* } } this.localStyleDeclarationState = sds; } // Synchronization note:
* Make sure getStyle() does not return multiple values. return sds; }
*
* protected AbstractCSS2Properties createDefaultStyleSheet() { // Override
* to provide element defaults. return null; }
*
* private Map computedStyles;
*
* public AbstractCSS2Properties getComputedStyle(String pseudoElement) {
* if(pseudoElement == null) { pseudoElement = ""; } synchronized(this) {
* Map cs = this.computedStyles; if(cs != null) { AbstractCSS2Properties sds
* = (AbstractCSS2Properties) cs.get(pseudoElement); if(sds != null) {
* return sds; } } } // Can't do the following in synchronized block
* (reverse locking order with document). // First, add declarations from
* stylesheet Set pes = pseudoElement.length() == 0 ? null :
* Collections.singleton(pseudoElement); AbstractCSS2Properties sds =
* this.createDefaultStyleSheet(); sds = this.addStyleSheetDeclarations(sds,
* pes); // Now add local style if any. AbstractCSS2Properties localStyle =
* this.getStyle(); if(sds == null) { sds = new
* ComputedCSS2Properties(this); sds.setLocalStyleProperties(localStyle); }
* else { sds.setLocalStyleProperties(localStyle); } synchronized(this) { //
* Check if style properties were set while outside // the synchronized
* block (can happen). We need to // return instance already set for
* consistency. Map cs = this.computedStyles; if(cs == null) { cs = new
* HashMap(2); this.computedStyles = cs; } else { AbstractCSS2Properties
* sds2 = (AbstractCSS2Properties) cs.get(pseudoElement); if(sds2 != null) {
* return sds2; } } cs.put(pseudoElement, sds); } return sds; }
*/
public void setStyle(Object value) {
throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot set style property");
}
public void setCurrentStyle(Object value) {
throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Cannot set currentStyle property");
}
public String getClassName() {
String className = this.getAttribute("class");
// Blank required instead of null.
return className == null ? "" : className;
}
public void setClassName(String className) {
this.setAttribute("class", className);
}
public String getCharset() {
return this.getAttribute("charset");
}
public void setCharset(String charset) {
this.setAttribute("charset", charset);
}
public void warn(String message, Throwable err) {
logger.log(Level.WARNING, message, err);
}
public void warn(String message) {
logger.log(Level.WARNING, message);
}
protected int getAttributeAsInt(String name, int defaultValue) {
String value = this.getAttribute(name);
try {
return Integer.parseInt(value);
} catch (Exception err) {
this.warn("Bad integer", err);
return defaultValue;
}
}
public boolean getAttributeAsBoolean(String name) {
return this.getAttribute(name) != null;
}
protected void assignAttributeField(String normalName, String value) {
if (!this.notificationsSuspended) {
this.informInvalidAttibute(normalName);
} else {
/*
* if("style".equals(normalName)) { this.forgetLocalStyle(); }
*/
}
super.assignAttributeField(normalName, value);
}
/*
* protected final InputSource getCssInputSourceForDecl(String text) {
* java.io.Reader reader = new StringReader("{" + text + "}"); InputSource
* is = new InputSource(reader); return is; }
*/
/**
* Adds style sheet declarations applicable to this element. A properties
* object is created if necessary when the one passed is <code>null</code>.
*
* @param style
*/
/*
* protected final AbstractCSS2Properties
* addStyleSheetDeclarations(AbstractCSS2Properties style, Set pseudoNames)
* { Node pn = this.parentNode; if(pn == null) { // do later return style; }
* String classNames = this.getClassName(); if(classNames != null &&
* classNames.length() != 0) { String id = this.getId(); String elementName
* = this.getTagName(); String[] classNameArray = Strings.split(classNames);
* for(int i = classNameArray.length; --i >= 0;) { String className =
* classNameArray[i]; Collection sds =
* this.findStyleDeclarations(elementName, id, className, pseudoNames);
* if(sds != null) { Iterator sdsi = sds.iterator(); while(sdsi.hasNext()) {
* CSSStyleDeclaration sd = (CSSStyleDeclaration) sdsi.next(); if(style ==
* null) { style = new ComputedCSS2Properties(this); }
* style.addStyleDeclaration(sd); } } } } else { String id = this.getId();
* String elementName = this.getTagName(); Collection sds =
* this.findStyleDeclarations(elementName, id, null, pseudoNames); if(sds !=
* null) { Iterator sdsi = sds.iterator(); while(sdsi.hasNext()) {
* CSSStyleDeclaration sd = (CSSStyleDeclaration) sdsi.next(); if(style ==
* null) { style = new ComputedCSS2Properties(this); }
* style.addStyleDeclaration(sd); } } } return style; }
*/
private boolean isMouseOver = false;
public void setMouseOver(boolean mouseOver) {
if (this.isMouseOver != mouseOver) {
// Change isMouseOver field before checking to invalidate.
this.isMouseOver = mouseOver;
// Check if descendents are affected (e.g. div:hover a { ... } )
this.invalidateDescendentsForHover();
if (this.hasHoverStyle()) {
// TODO: OPTIMIZATION: In some cases it should be much
// better to simply invalidate the "look" of the node.
this.informInvalid();
}
}
}
private void invalidateDescendentsForHover() {
synchronized (this.treeLock) {
this.invalidateDescendentsForHoverImpl(this);
}
}
private void invalidateDescendentsForHoverImpl(HTMLElementImpl ancestor) {
ArrayList nodeList = this.nodeList;
if (nodeList != null) {
int size = nodeList.size();
for (int i = 0; i < size; i++) {
Object node = nodeList.get(i);
if (node instanceof HTMLElementImpl) {
HTMLElementImpl descendent = (HTMLElementImpl) node;
if (descendent.hasHoverStyle(ancestor)) {
descendent.informInvalid();
}
descendent.invalidateDescendentsForHoverImpl(ancestor);
}
}
}
}
private Boolean isHoverStyle = null;
private Map hasHoverStyleByElement = null;
/*
* Disabled
*/
private boolean hasHoverStyle() {
Boolean ihs = Boolean.FALSE;
/*
* synchronized(this) { ihs = this.isHoverStyle; if(ihs != null) {
* return ihs.booleanValue(); } } HTMLDocumentImpl doc =
* (HTMLDocumentImpl) this.document; if(doc == null) { ihs =
* Boolean.FALSE; } else { StyleSheetAggregator ssa =
* doc.getStyleSheetAggregator(); String id = this.getId(); String
* elementName = this.getTagName(); String classNames =
* this.getClassName(); String[] classNameArray = null; if(classNames !=
* null && classNames.length() != 0) { classNameArray =
* Strings.split(classNames); } ihs =
* Boolean.valueOf(ssa.affectedByPseudoNameInAncestor(this, this,
* elementName, id, classNameArray, "hover")); } synchronized(this) {
* this.isHoverStyle = ihs; }
*/
return ihs.booleanValue();
}
/*
* Disabled
*/
private boolean hasHoverStyle(HTMLElementImpl ancestor) {
/*
* Map ihs; synchronized(this) { ihs = this.hasHoverStyleByElement;
* if(ihs != null) { Boolean f = (Boolean) ihs.get(ancestor); if(f !=
* null) { return f.booleanValue(); } } } Boolean hhs; HTMLDocumentImpl
* doc = (HTMLDocumentImpl) this.document; if(doc == null) { hhs =
* Boolean.FALSE; } else { StyleSheetAggregator ssa =
* doc.getStyleSheetAggregator(); String id = this.getId(); String
* elementName = this.getTagName(); String classNames =
* this.getClassName(); String[] classNameArray = null; if(classNames !=
* null && classNames.length() != 0) { classNameArray =
* Strings.split(classNames); } hhs =
* Boolean.valueOf(ssa.affectedByPseudoNameInAncestor(this, ancestor,
* elementName, id, classNameArray, "hover")); } synchronized(this) {
* ihs = this.hasHoverStyleByElement; if(ihs == null) { ihs = new
* HashMap(2); this.hasHoverStyleByElement = ihs; } ihs.put(ancestor,
* hhs); } return hhs.booleanValue();
*/
return false;
}
/**
* Gets the pseudo-element lowercase names currently applicable to this
* element. Method must return <code>null</code> if there are no such
* pseudo-elements.
*/
public Set getPseudoNames() {
Set pnset = null;
if (this.isMouseOver) {
if (pnset == null) {
pnset = new HashSet(1);
}
pnset.add("hover");
}
return pnset;
}
/*
* protected final Collection findStyleDeclarations(String elementName,
* String id, String className, Set pseudoNames) { HTMLDocumentImpl doc =
* (HTMLDocumentImpl) this.document; if(doc == null) { return null; }
* StyleSheetAggregator ssa = doc.getStyleSheetAggregator(); return
* ssa.getActiveStyleDeclarations(this, elementName, id, className,
* pseudoNames); }
*/
public void informInvalid() {
// This is called when an attribute or child changes.
// this.forgetStyle(false);
super.informInvalid();
}
public void informInvalidAttibute(String normalName) {
// This is called when an attribute changes while
// the element is allowing notifications.
if ("style".equals(normalName)) {
// this.forgetLocalStyle();
} else if ("id".equals(normalName) || "class".equals(normalName)) {
// this.forgetStyle(false);
}
// Call super implementation of informValid().
super.informInvalid();
}
/**
* Gets form input due to the current element. It should return
* <code>null</code> except when the element is a form input element.
*/
protected FormInput[] getFormInputs() {
// Override in input elements
return null;
}
private boolean classMatch(String classTL) {
String classNames = this.getClassName();
if (classNames == null || classNames.length() == 0) {
return classTL == null;
}
StringTokenizer tok = new StringTokenizer(classNames, " \t\r\n");
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
if (token.toLowerCase().equals(classTL)) {
return true;
}
}
return false;
}
/**
* Get an ancestor that matches the element tag name given and the style
* class given.
*
* @param elementTL
* An tag name in lowercase or an asterisk (*).
* @param classTL
* A class name in lowercase.
*/
public HTMLElementImpl getAncestorWithClass(String elementTL, String classTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
String pelementTL = parentElement.getTagName().toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && parentElement.classMatch(classTL)) {
return parentElement;
}
return parentElement.getAncestorWithClass(elementTL, classTL);
} else {
return null;
}
}
public HTMLElementImpl getParentWithClass(String elementTL, String classTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
String pelementTL = parentElement.getTagName().toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && parentElement.classMatch(classTL)) {
return parentElement;
}
}
return null;
}
public HTMLElementImpl getPreceedingSiblingElement() {
Node parentNode = this.getParentNode();
if (parentNode == null) {
return null;
}
NodeList childNodes = parentNode.getChildNodes();
if (childNodes == null) {
return null;
}
int length = childNodes.getLength();
HTMLElementImpl priorElement = null;
for (int i = 0; i < length; i++) {
Node child = childNodes.item(i);
if (child == this) {
return priorElement;
}
if (child instanceof HTMLElementImpl) {
priorElement = (HTMLElementImpl) child;
}
}
return null;
}
public HTMLElementImpl getPreceedingSiblingWithClass(String elementTL, String classTL) {
HTMLElementImpl psibling = this.getPreceedingSiblingElement();
if (psibling != null) {
String pelementTL = psibling.getTagName().toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && psibling.classMatch(classTL)) {
return psibling;
}
}
return null;
}
public HTMLElementImpl getAncestorWithId(String elementTL, String idTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
String pelementTL = parentElement.getTagName().toLowerCase();
String pid = parentElement.getId();
String pidTL = pid == null ? null : pid.toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && idTL.equals(pidTL)) {
return parentElement;
}
return parentElement.getAncestorWithId(elementTL, idTL);
} else {
return null;
}
}
public HTMLElementImpl getParentWithId(String elementTL, String idTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
String pelementTL = parentElement.getTagName().toLowerCase();
String pid = parentElement.getId();
String pidTL = pid == null ? null : pid.toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && idTL.equals(pidTL)) {
return parentElement;
}
}
return null;
}
public HTMLElementImpl getPreceedingSiblingWithId(String elementTL, String idTL) {
HTMLElementImpl psibling = this.getPreceedingSiblingElement();
if (psibling != null) {
String pelementTL = psibling.getTagName().toLowerCase();
String pid = psibling.getId();
String pidTL = pid == null ? null : pid.toLowerCase();
if (("*".equals(elementTL) || elementTL.equals(pelementTL)) && idTL.equals(pidTL)) {
return psibling;
}
}
return null;
}
public HTMLElementImpl getAncestor(String elementTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
if ("*".equals(elementTL)) {
return parentElement;
}
String pelementTL = parentElement.getTagName().toLowerCase();
if (elementTL.equals(pelementTL)) {
return parentElement;
}
return parentElement.getAncestor(elementTL);
} else {
return null;
}
}
public HTMLElementImpl getParent(String elementTL) {
Object nodeObj = this.getParentNode();
if (nodeObj instanceof HTMLElementImpl) {
HTMLElementImpl parentElement = (HTMLElementImpl) nodeObj;
if ("*".equals(elementTL)) {
return parentElement;
}
String pelementTL = parentElement.getTagName().toLowerCase();
if (elementTL.equals(pelementTL)) {
return parentElement;
}
}
return null;
}
public HTMLElementImpl getPreceedingSibling(String elementTL) {
HTMLElementImpl psibling = this.getPreceedingSiblingElement();
if (psibling != null) {
if ("*".equals(elementTL)) {
return psibling;
}
String pelementTL = psibling.getTagName().toLowerCase();
if (elementTL.equals(pelementTL)) {
return psibling;
}
}
return null;
}
protected Object getAncestorForJavaClass(Class javaClass) {
Object nodeObj = this.getParentNode();
if (nodeObj == null || javaClass.isInstance(nodeObj)) {
return nodeObj;
} else if (nodeObj instanceof HTMLElementImpl) {
return ((HTMLElementImpl) nodeObj).getAncestorForJavaClass(javaClass);
} else {
return null;
}
}
public void setInnerHTML(String newHtml) {
HTMLDocumentImpl document = (HTMLDocumentImpl) this.document;
if (document == null) {
this.warn("setInnerHTML(): Element " + this + " does not belong to a document.");
return;
}
HtmlParser parser = new HtmlParser(document.getUserAgentContext(), document, null, null, null);
synchronized (this) {
ArrayList nl = this.nodeList;
if (nl != null) {
nl.clear();
}
}
// Should not synchronize around parser probably.
try {
Reader reader = new StringReader(newHtml);
try {
parser.parse(reader, this);
} finally {
reader.close();
}
} catch (Exception thrown) {
this.warn("setInnerHTML(): Error setting inner HTML.", thrown);
}
}
public String getOuterHTML() {
StringBuffer buffer = new StringBuffer();
synchronized (this) {
this.appendOuterHTMLImpl(buffer);
}
return buffer.toString();
}
protected void appendOuterHTMLImpl(StringBuffer buffer) {
String tagName = this.getTagName();
buffer.append('<');
buffer.append(tagName);
Map attributes = this.attributes;
if (attributes != null) {
Iterator i = attributes.entrySet().iterator();
while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next();
String value = (String) entry.getValue();
if (value != null) {
buffer.append(' ');
buffer.append(entry.getKey());
buffer.append("=\"");
buffer.append(Strings.strictHtmlEncode(value, true));
buffer.append("\"");
}
}
}
ArrayList nl = this.nodeList;
if (nl == null || nl.size() == 0) {
buffer.append("/>");
return;
}
buffer.append('>');
this.appendInnerHTMLImpl(buffer);
buffer.append("</");
buffer.append(tagName);
buffer.append('>');
}
/*
* protected RenderState createRenderState(RenderState prevRenderState) { //
* Overrides NodeImpl method // Called in synchronized block already return
* new StyleSheetRenderState(prevRenderState, this); }
*/
public int getOffsetTop() {
// TODO: Sometimes this can be called while parsing, and
// browsers generally give the right answer.
UINode uiNode = this.getUINode();
return uiNode == null ? 0 : uiNode.getBoundsRelativeToBlock().y;
}
public int getOffsetLeft() {
UINode uiNode = this.getUINode();
return uiNode == null ? 0 : uiNode.getBoundsRelativeToBlock().x;
}
public int getOffsetWidth() {
UINode uiNode = this.getUINode();
return uiNode == null ? 0 : uiNode.getBoundsRelativeToBlock().width;
}
public int getOffsetHeight() {
UINode uiNode = this.getUINode();
return uiNode == null ? 0 : uiNode.getBoundsRelativeToBlock().height;
}
/*
* public AbstractCSS2Properties getParentStyle() { Object parent =
* this.parentNode; if(parent instanceof HTMLElementImpl) { return
* ((HTMLElementImpl) parent).getCurrentStyle(); } return null; }
*/
public String getDocumentBaseURI() {
HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
if (doc != null) {
return doc.getBaseURI();
} else {
return null;
}
}
public String toString() {
return super.toString(); // + "[currentStyle=" + this.getCurrentStyle()
// + "]";
}
}