/* * 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.IOException; import java.io.LineNumberReader; import java.io.Reader; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.mozilla.javascript.Function; import org.w3c.dom.Attr; import org.w3c.dom.CDATASection; import org.w3c.dom.Comment; import org.w3c.dom.DOMConfiguration; import org.w3c.dom.DOMException; import org.w3c.dom.DOMImplementation; import org.w3c.dom.DocumentFragment; import org.w3c.dom.DocumentType; import org.w3c.dom.Element; import org.w3c.dom.EntityReference; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.w3c.dom.UserDataHandler; import org.w3c.dom.html2.HTMLCollection; import org.w3c.dom.html2.HTMLDocument; import org.w3c.dom.html2.HTMLElement; import org.w3c.dom.html2.HTMLLinkElement; import org.w3c.dom.views.AbstractView; import org.w3c.dom.views.DocumentView; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import com.nvarghese.beowulf.common.cobra.html.HtmlRendererContext; import com.nvarghese.beowulf.common.cobra.html.HttpRequest; import com.nvarghese.beowulf.common.cobra.html.ReadyStateChangeListener; import com.nvarghese.beowulf.common.cobra.html.UserAgentContext; import com.nvarghese.beowulf.common.cobra.html.io.WritableLineReader; import com.nvarghese.beowulf.common.cobra.html.js.Executor; import com.nvarghese.beowulf.common.cobra.html.js.Location; import com.nvarghese.beowulf.common.cobra.html.js.Window; import com.nvarghese.beowulf.common.cobra.html.parser.HtmlParser; import com.nvarghese.beowulf.common.cobra.util.Domains; import com.nvarghese.beowulf.common.cobra.util.Urls; import com.nvarghese.beowulf.common.cobra.util.WeakValueHashMap; import com.nvarghese.beowulf.common.cobra.util.io.EmptyReader; /** * Implementation of the W3C <code>HTMLDocument</code> interface. */ public class HTMLDocumentImpl extends NodeImpl implements HTMLDocument, DocumentView { /** * */ private static final long serialVersionUID = 4471936533882307438L; private static final Logger logger = Logger.getLogger(HTMLDocumentImpl.class.getName()); private final ElementFactory factory; private final HtmlRendererContext rcontext; private final UserAgentContext ucontext; private final Window window; private final transient Map elementsById = new WeakValueHashMap(); private String documentURI; private java.net.URL documentURL; private WritableLineReader reader; public HTMLDocumentImpl(HtmlRendererContext rcontext) { this(rcontext.getUserAgentContext(), rcontext, null, null); } public HTMLDocumentImpl(UserAgentContext ucontext) { this(ucontext, null, null, null); } public HTMLDocumentImpl(final UserAgentContext ucontext, final HtmlRendererContext rcontext, WritableLineReader reader, String documentURI) { this.factory = ElementFactory.getInstance(); this.rcontext = rcontext; this.ucontext = ucontext; this.reader = reader; this.documentURI = documentURI; try { java.net.URL docURL = new java.net.URL(documentURI); SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Do not allow creation of HTMLDocumentImpl if there's // no permission to connect to the host of the URL. // This is so that cookies cannot be written arbitrarily // with setCookie() method. sm.checkPermission(new java.net.SocketPermission(docURL.getHost(), "connect")); } this.documentURL = docURL; this.domain = docURL.getHost(); } catch (java.net.MalformedURLException mfu) { logger.warning("HTMLDocumentImpl(): Document URI [" + documentURI + "] is malformed."); } this.document = this; // Get Window object Window window; if (rcontext != null) { window = Window.getWindow(rcontext); } else { // Plain parsers may use Javascript too. window = new Window(null, ucontext); } // Window must be retained or it will be garbage collected. this.window = window; window.setDocument(this); // Set up Javascript scope this.setUserData(Executor.SCOPE_KEY, window.getWindowScope(), null); } private Set locales; /** * Gets an <i>immutable</i> set of locales previously set for this document. */ public Set getLocales() { return locales; } /** * Sets the locales of the document. This helps determine whether specific * fonts can display text in the languages of all the locales. * * @param locales * An <i>immutable</i> set of <code>java.util.Locale</code> * instances. */ public void setLocales(Set locales) { this.locales = locales; } String getDocumentHost() { URL docUrl = this.documentURL; return docUrl == null ? null : docUrl.getHost(); } public URL getDocumentURL() { // TODO: Security considerations? return this.documentURL; } /** * Caller should synchronize on document. */ void setElementById(String id, Element element) { synchronized (this) { this.elementsById.put(id, element); } } void removeElementById(String id) { synchronized (this) { this.elementsById.remove(id); } } private volatile String baseURI; /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getbaseURI() */ public String getBaseURI() { String buri = this.baseURI; return buri == null ? this.documentURI : buri; } public void setBaseURI(String value) { this.baseURI = value; } private String defaultTarget; public String getDefaultTarget() { return this.defaultTarget; } public void setDefaultTarget(String value) { this.defaultTarget = value; } public AbstractView getDefaultView() { return (AbstractView) this.window; } public String getTextContent() throws DOMException { return null; } public void setTextContent(String textContent) throws DOMException { // NOP, per spec } private String title; public String getTitle() { return this.title; } public void setTitle(String title) { this.title = title; } private String referrer; public String getReferrer() { return this.referrer; } public void setReferrer(String value) { this.referrer = value; } private String domain; public String getDomain() { return this.domain; } public void setDomain(String domain) { String oldDomain = this.domain; if (oldDomain != null && Domains.isValidCookieDomain(domain, oldDomain)) { this.domain = domain; } else { throw new SecurityException("Cannot set domain to '" + domain + "' when current domain is '" + oldDomain + "'"); } } public HTMLElement getBody() { synchronized (this) { return this.body; } } private HTMLCollection images; private HTMLCollection applets; private HTMLCollection links; private HTMLCollection forms; private HTMLCollection anchors; private HTMLCollection frames; public HTMLCollection getImages() { synchronized (this) { if (this.images == null) { this.images = new DescendentHTMLCollection(this, new ImageFilter(), this.treeLock); } return this.images; } } public HTMLCollection getApplets() { synchronized (this) { if (this.applets == null) { // TODO: Should include OBJECTs that are applets? this.applets = new DescendentHTMLCollection(this, new AppletFilter(), this.treeLock); } return this.applets; } } public HTMLCollection getLinks() { synchronized (this) { if (this.links == null) { this.links = new DescendentHTMLCollection(this, new LinkFilter(), this.treeLock); } return this.links; } } public HTMLCollection getForms() { synchronized (this) { if (this.forms == null) { this.forms = new DescendentHTMLCollection(this, new FormFilter(), this.treeLock); } return this.forms; } } public HTMLCollection getFrames() { synchronized (this) { if (this.frames == null) { this.frames = new DescendentHTMLCollection(this, new FrameFilter(), this.treeLock); } return this.frames; } } public HTMLCollection getAnchors() { synchronized (this) { if (this.anchors == null) { this.anchors = new DescendentHTMLCollection(this, new AnchorFilter(), this.treeLock); } return this.anchors; } } public String getCookie() { SecurityManager sm = System.getSecurityManager(); if (sm != null) { return (String) AccessController.doPrivileged(new PrivilegedAction() { // Justification: A caller (e.g. Google Analytics script) // might want to get cookies from the parent document. // If the caller has access to the document, it appears // they should be able to get cookies on that document. // Note that this Document instance cannot be created // with an arbitrary URL. // TODO: Security: Review rationale. public Object run() { return ucontext.getCookie(documentURL); } }); } else { return this.ucontext.getCookie(this.documentURL); } } public void setCookie(final String cookie) throws DOMException { SecurityManager sm = System.getSecurityManager(); if (sm != null) { AccessController.doPrivileged(new PrivilegedAction() { // Justification: A caller (e.g. Google Analytics script) // might want to set cookies on the parent document. // If the caller has access to the document, it appears // they should be able to set cookies on that document. // Note that this Document instance cannot be created // with an arbitrary URL. public Object run() { ucontext.setCookie(documentURL, cookie); return null; } }); } else { this.ucontext.setCookie(this.documentURL, cookie); } } public void open() { synchronized (this.treeLock) { if (this.reader != null) { if (this.reader instanceof LocalWritableLineReader) { try { this.reader.close(); } catch (IOException ioe) { // ignore } this.reader = null; } else { // Already open, return. // Do not close http/file documents in progress. return; } } this.removeAllChildrenImpl(); this.reader = new LocalWritableLineReader(new EmptyReader()); } } /** * Loads the document from the reader provided when the current instance of * <code>HTMLDocumentImpl</code> was constructed. It then closes the reader. * * @throws IOException * @throws SAXException * @throws UnsupportedEncodingException */ public void load() throws IOException, SAXException, UnsupportedEncodingException { this.load(true); } public void load(boolean closeReader) throws IOException, SAXException, UnsupportedEncodingException { WritableLineReader reader; synchronized (this.treeLock) { this.removeAllChildrenImpl(); this.setTitle(null); this.setBaseURI(null); this.setDefaultTarget(null); // this.styleSheets.clear(); // this.styleSheetAggregator = null; reader = this.reader; } if (reader != null) { try { ErrorHandler errorHandler = new LocalErrorHandler(); String systemId = this.documentURI; String publicId = systemId; HtmlParser parser = new HtmlParser(this.ucontext, this, errorHandler, publicId, systemId); parser.parse(reader); } finally { if (closeReader) { try { reader.close(); } catch (Exception err) { logger.log(Level.WARNING, "load(): Unable to close stream", err); } synchronized (this.treeLock) { this.reader = null; } } } } } public void close() { synchronized (this.treeLock) { if (this.reader instanceof LocalWritableLineReader) { try { this.reader.close(); } catch (java.io.IOException ioe) { // ignore } this.reader = null; } else { // do nothing - could be parsing document off the web. } // TODO: cause it to render } } public void write(String text) { synchronized (this.treeLock) { if (this.reader != null) { try { // This can end up in openBufferChanged this.reader.write(text); } catch (IOException ioe) { // ignore } } } } public void writeln(String text) { synchronized (this.treeLock) { if (this.reader != null) { try { // This can end up in openBufferChanged this.reader.write(text + "\r\n"); } catch (IOException ioe) { // ignore } } } } private void openBufferChanged(String text) { // Assumed to execute in a lock // Assumed that text is not broken up HTML. ErrorHandler errorHandler = new LocalErrorHandler(); String systemId = this.documentURI; String publicId = systemId; HtmlParser parser = new HtmlParser(this.ucontext, this, errorHandler, publicId, systemId); StringReader strReader = new StringReader(text); try { // This sets up another Javascript scope Window. Does it matter? parser.parse(strReader); } catch (Exception err) { this.warn("Unable to parse written HTML text. BaseURI=[" + this.getBaseURI() + "].", err); } } /** * Gets the collection of elements whose <code>name</code> attribute is * <code>elementName</code>. */ public NodeList getElementsByName(String elementName) { return this.getNodeList(new ElementNameFilter(elementName)); } private DocumentType doctype; public DocumentType getDoctype() { return this.doctype; } public void setDoctype(DocumentType doctype) { this.doctype = doctype; } public Element getDocumentElement() { synchronized (this.treeLock) { ArrayList nl = this.nodeList; if (nl != null) { Iterator i = nl.iterator(); while (i.hasNext()) { Object node = i.next(); if (node instanceof Element) { return (Element) node; } } } return null; } } public Element createElement(String tagName) throws DOMException { return this.factory.createElement(this, tagName); } /* * (non-Javadoc) * * @see org.w3c.dom.Document#createDocumentFragment() */ public DocumentFragment createDocumentFragment() { // TODO: According to documentation, when a document // fragment is added to a node, its children are added, // not itself. DocumentFragmentImpl node = new DocumentFragmentImpl(); node.setOwnerDocument(this); return node; } public Text createTextNode(String data) { TextImpl node = new TextImpl(data); node.setOwnerDocument(this); return node; } public Comment createComment(String data) { CommentImpl node = new CommentImpl(data); node.setOwnerDocument(this); return node; } public CDATASection createCDATASection(String data) throws DOMException { CDataSectionImpl node = new CDataSectionImpl(data); node.setOwnerDocument(this); return node; } public ProcessingInstruction createProcessingInstruction(String target, String data) throws DOMException { HTMLProcessingInstruction node = new HTMLProcessingInstruction(target, data); node.setOwnerDocument(this); return node; } public Attr createAttribute(String name) throws DOMException { return new AttrImpl(name); } public EntityReference createEntityReference(String name) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "HTML document"); } /** * Gets all elements that match the given tag name. * * @param tagname * The element tag name or an asterisk character (*) to match all * elements. */ public NodeList getElementsByTagName(String tagname) { if ("*".equals(tagname)) { return this.getNodeList(new ElementFilter()); } else { return this.getNodeList(new TagNameFilter(tagname)); } } public Node importNode(Node importedNode, boolean deep) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Not implemented"); } public Element createElementNS(String namespaceURI, String qualifiedName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "HTML document"); } public Attr createAttributeNS(String namespaceURI, String qualifiedName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "HTML document"); } public NodeList getElementsByTagNameNS(String namespaceURI, String localName) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "HTML document"); } public Element getElementById(String elementId) { Element element; synchronized (this) { element = (Element) this.elementsById.get(elementId); } return element; } private final Map elementsByName = new HashMap(0); public Element namedItem(String name) { Element element; synchronized (this) { element = (Element) this.elementsByName.get(name); } return element; } void setNamedItem(String name, Element element) { synchronized (this) { this.elementsByName.put(name, element); } } void removeNamedItem(String name) { synchronized (this) { this.elementsByName.remove(name); } } private String inputEncoding; public String getInputEncoding() { return this.inputEncoding; } private String xmlEncoding; public String getXmlEncoding() { return this.xmlEncoding; } private boolean xmlStandalone; public boolean getXmlStandalone() { return this.xmlStandalone; } public void setXmlStandalone(boolean xmlStandalone) throws DOMException { this.xmlStandalone = xmlStandalone; } private String xmlVersion = null; public String getXmlVersion() { return this.xmlVersion; } public void setXmlVersion(String xmlVersion) throws DOMException { this.xmlVersion = xmlVersion; } private boolean strictErrorChecking = true; public boolean getStrictErrorChecking() { return this.strictErrorChecking; } public void setStrictErrorChecking(boolean strictErrorChecking) { this.strictErrorChecking = strictErrorChecking; } public String getDocumentURI() { return this.documentURI; } public void setDocumentURI(String documentURI) { // TODO: Security considerations? Chaging documentURL? this.documentURI = documentURI; } public Node adoptNode(Node source) throws DOMException { if (source instanceof NodeImpl) { NodeImpl node = (NodeImpl) source; node.setOwnerDocument(this, true); return node; } else { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Invalid Node implementation"); } } private DOMConfiguration domConfig; public DOMConfiguration getDomConfig() { synchronized (this) { if (this.domConfig == null) { this.domConfig = new DOMConfigurationImpl(); } return this.domConfig; } } public void normalizeDocument() { // TODO: Normalization options from domConfig synchronized (this.treeLock) { this.visitImpl(new NodeVisitor() { public void visit(Node node) { node.normalize(); } }); } } public Node renameNode(Node n, String namespaceURI, String qualifiedName) throws DOMException { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "No renaming"); } private DOMImplementation domImplementation; /* * (non-Javadoc) * * @see org.w3c.dom.Document#getImplementation() */ public DOMImplementation getImplementation() { synchronized (this) { if (this.domImplementation == null) { this.domImplementation = new DOMImplementationImpl(this.ucontext); } return this.domImplementation; } } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getLocalName() */ public String getLocalName() { // Always null for document return null; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeName() */ public String getNodeName() { return "#document"; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeType() */ public short getNodeType() { return Node.DOCUMENT_NODE; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#getNodeValue() */ public String getNodeValue() throws DOMException { // Always null for document return null; } /* * (non-Javadoc) * * @see org.xamjwg.html.domimpl.NodeImpl#setNodeValue(java.lang.String) */ public void setNodeValue(String nodeValue) throws DOMException { throw new DOMException(DOMException.INVALID_MODIFICATION_ERR, "Cannot set node value of document"); } public final HtmlRendererContext getHtmlRendererContext() { return this.rcontext; } public UserAgentContext getUserAgentContext() { return this.ucontext; } public final URL getFullURL(String uri) { try { String baseURI = this.getBaseURI(); final URLStreamHandler streamHandler = new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return null; } }; URL documentURL = baseURI == null ? null : new URL(null, baseURI, streamHandler); return Urls.createURL(documentURL, uri); } catch (MalformedURLException mfu) { // Try agan, without the baseURI. try { final URLStreamHandler streamHandler = new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return null; } }; return new URL(null, uri, streamHandler); } catch (MalformedURLException mfu2) { logger.log(Level.WARNING, "Unable to create URL for URI=[" + uri + "], with base=[" + this.getBaseURI() + "].", mfu); return null; } } } public final Location getLocation() { return this.window.getLocation(); } public void setLocation(String location) { this.getLocation().setHref(location); } public String getURL() { return this.documentURI; } private HTMLElement body; public void setBody(HTMLElement body) { synchronized (this) { this.body = body; } } /* * private final Collection styleSheets = new CSSStyleSheetList(); * * public class CSSStyleSheetList extends ArrayList { public int * getLength(){ return this.size(); } * * public CSSStyleSheet item(int index){ return (CSSStyleSheet) get(index); * } } * * final void addStyleSheet(CSSStyleSheet ss) { synchronized(this.treeLock) * { this.styleSheets.add(ss); this.styleSheetAggregator = null; // Need to * invalidate all children up to // this point. this.forgetRenderState(); * //TODO: this might be ineffcient. 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(true); } } } } this.allInvalidated(); } * * public void allInvalidated(boolean forgetRenderStates) { * if(forgetRenderStates) { synchronized(this.treeLock) { * this.styleSheetAggregator = null; // Need to invalidate all children up * to // this point. this.forgetRenderState(); //TODO: this might be * ineffcient. 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(true); * } } } } } this.allInvalidated(); } * * public Collection getStyleSheets(){ return this.styleSheets; } * * private StyleSheetAggregator styleSheetAggregator = null; * * final StyleSheetAggregator getStyleSheetAggregator() { * synchronized(this.treeLock) { StyleSheetAggregator ssa = * this.styleSheetAggregator; if(ssa == null) { ssa = new * StyleSheetAggregator(this); try { ssa.addStyleSheets(this.styleSheets); } * catch(MalformedURLException mfu) { * logger.log(Level.WARNING,"getStyleSheetAggregator()", mfu); } * this.styleSheetAggregator = ssa; } return ssa; } } */ private final ArrayList documentNotificationListeners = new ArrayList(1); /** * Adds a document notification listener, which is informed about changes to * the document. * * @param listener * An instance of {@link DocumentNotificationListener}. */ public void addDocumentNotificationListener(DocumentNotificationListener listener) { ArrayList listenersList = this.documentNotificationListeners; synchronized (listenersList) { listenersList.add(listener); } } public void removeDocumentNotificationListener(DocumentNotificationListener listener) { ArrayList listenersList = this.documentNotificationListeners; synchronized (listenersList) { listenersList.remove(listener); } } public void sizeInvalidated(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.sizeInvalidated(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } /** * Called if something such as a color or decoration has changed. This would * be something which does not affect the rendered size, and can be * revalidated with a simple repaint. * * @param node */ public void lookInvalidated(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.lookInvalidated(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } /** * Changed if the position of the node in a parent has changed. * * @param node */ public void positionInParentInvalidated(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.positionInvalidated(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } /** * This is called when the node has changed, but it is unclear if it's a * size change or a look change. An attribute change should trigger this. * * @param node */ public void invalidated(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.invalidated(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } /** * This is called when children of the node might have changed. * * @param node */ public void structureInvalidated(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.structureInvalidated(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } public void nodeLoaded(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.nodeLoaded(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } public void externalScriptLoading(NodeImpl node) { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.externalScriptLoading(node); } catch (IndexOutOfBoundsException iob) { // ignore } } } /** * Informs listeners that the whole document has been invalidated. */ public void allInvalidated() { ArrayList listenersList = this.documentNotificationListeners; int size; synchronized (listenersList) { size = listenersList.size(); } // Traverse list outside synchronized block. // (Shouldn't call listener methods in synchronized block. // Deadlock is possible). But assume list could have // been changed. for (int i = 0; i < size; i++) { try { DocumentNotificationListener dnl = (DocumentNotificationListener) listenersList.get(i); dnl.allInvalidated(); } catch (IndexOutOfBoundsException iob) { // ignore } } } /* * protected RenderState createRenderState(RenderState prevRenderState) { * return new StyleSheetRenderState(this); } */ private final Map imageInfos = new HashMap(4); private final ImageEvent BLANK_IMAGE_EVENT = new ImageEvent(this, null); /** * Loads images asynchronously such that they are shared if loaded * simultaneously from the same URI. Informs the listener immediately if an * image is already known. * * @param relativeUri * @param imageListener */ protected void loadImage(String relativeUri, ImageListener imageListener) { HtmlRendererContext rcontext = this.getHtmlRendererContext(); if (rcontext == null || !rcontext.isImageLoadingEnabled()) { // Ignore image loading when there's no renderer context. // Consider Cobra users who are only using the parser. imageListener.imageLoaded(BLANK_IMAGE_EVENT); return; } final URL url = this.getFullURL(relativeUri); if (url == null) { imageListener.imageLoaded(BLANK_IMAGE_EVENT); return; } final String urlText = url.toExternalForm(); final Map map = this.imageInfos; ImageEvent event = null; synchronized (map) { ImageInfo info = (ImageInfo) map.get(urlText); if (info != null) { if (info.loaded) { // TODO: This can't really happen because ImageInfo // is removed right after image is loaded. event = info.imageEvent; } else { info.addListener(imageListener); } } else { UserAgentContext uac = rcontext.getUserAgentContext(); final HttpRequest httpRequest = uac.createHttpRequest(); final ImageInfo newInfo = new ImageInfo(); map.put(urlText, newInfo); newInfo.addListener(imageListener); httpRequest.addReadyStateChangeListener(new ReadyStateChangeListener() { public void readyStateChanged() { if (httpRequest.getReadyState() == HttpRequest.STATE_COMPLETE) { java.awt.Image newImage = httpRequest.getResponseImage(); ImageEvent newEvent = newImage == null ? null : new ImageEvent(HTMLDocumentImpl.this, newImage); ImageListener[] listeners; synchronized (map) { newInfo.imageEvent = newEvent; newInfo.loaded = true; listeners = newEvent == null ? null : newInfo.getListeners(); // Must remove from map in the locked block // that got the listeners. Otherwise a new // listener might miss the event?? map.remove(urlText); } if (listeners != null) { int llength = listeners.length; for (int i = 0; i < llength; i++) { // Call holding no locks listeners[i].imageLoaded(newEvent); } } } } }); SecurityManager sm = System.getSecurityManager(); if (sm == null) { try { httpRequest.open("GET", url, true); httpRequest.send(null); } catch (java.io.IOException thrown) { logger.log(Level.WARNING, "loadImage()", thrown); } } else { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { // Code might have restrictions on accessing // items from elsewhere. try { httpRequest.open("GET", url, true); httpRequest.send(null); } catch (java.io.IOException thrown) { logger.log(Level.WARNING, "loadImage()", thrown); } return null; } }); } } } if (event != null) { // Call holding no locks. imageListener.imageLoaded(event); } } private Function onloadHandler; public Function getOnloadHandler() { return onloadHandler; } public void setOnloadHandler(Function onloadHandler) { this.onloadHandler = onloadHandler; } public Object setUserData(String key, Object data, UserDataHandler handler) { Function onloadHandler = this.onloadHandler; if (onloadHandler != null) { if (com.nvarghese.beowulf.common.cobra.html.parser.HtmlParser.MODIFYING_KEY.equals(key) && data == Boolean.FALSE) { // TODO: onload event object? Executor.executeFunction(this, onloadHandler, null); } } return super.setUserData(key, data, handler); } protected Node createSimilarNode() { return new HTMLDocumentImpl(this.ucontext, this.rcontext, this.reader, this.documentURI); } private static class ImageInfo { // Access to this class is synchronized on imageInfos. public ImageEvent imageEvent; public boolean loaded; private ArrayList listeners = new ArrayList(1); void addListener(ImageListener listener) { this.listeners.add(listener); } ImageListener[] getListeners() { return (ImageListener[]) this.listeners.toArray(ImageListener.EMPTY_ARRAY); } } private class ImageFilter implements NodeFilter { public boolean accept(Node node) { return "IMG".equalsIgnoreCase(node.getNodeName()); } } private class AppletFilter implements NodeFilter { public boolean accept(Node node) { // TODO: "OBJECT" elements that are applets too. return "APPLET".equalsIgnoreCase(node.getNodeName()); } } private class LinkFilter implements NodeFilter { public boolean accept(Node node) { return node instanceof HTMLLinkElement; } } private class AnchorFilter implements NodeFilter { public boolean accept(Node node) { String nodeName = node.getNodeName(); return "A".equalsIgnoreCase(nodeName) || "ANCHOR".equalsIgnoreCase(nodeName); } } private class FormFilter implements NodeFilter { public boolean accept(Node node) { String nodeName = node.getNodeName(); return "FORM".equalsIgnoreCase(nodeName); } } private class FrameFilter implements NodeFilter { public boolean accept(Node node) { return node instanceof org.w3c.dom.html2.HTMLFrameElement || node instanceof org.w3c.dom.html2.HTMLIFrameElement; } } private class ElementAttributeFilter implements NodeFilter { private final String attributeName; public ElementAttributeFilter(String attributeName) { this.attributeName = attributeName; } public boolean accept(Node node) { // TODO: Case sensitive? return (node instanceof Element) && ((Element) node).hasAttribute(attributeName); } } // private class BodyFilter implements NodeFilter { // public boolean accept(Node node) { // return node instanceof org.w3c.dom.html2.HTMLBodyElement; // } // } private class ElementNameFilter implements NodeFilter { private final String name; public ElementNameFilter(String name) { this.name = name; } public boolean accept(Node node) { // TODO: Case sensitive? return (node instanceof Element) && this.name.equals(((Element) node).getAttribute("name")); } } private class ElementFilter implements NodeFilter { public ElementFilter() { } public boolean accept(Node node) { return node instanceof Element; } } private class TagNameFilter implements NodeFilter { private final String name; public TagNameFilter(String name) { this.name = name; } public boolean accept(Node node) { if (!(node instanceof Element)) { return false; } String n = this.name; return n.equalsIgnoreCase(((Element) node).getTagName()); } } /** * Tag class that also notifies document when text is written to an open * buffer. * * @author J. H. S. */ private class LocalWritableLineReader extends WritableLineReader { /** * @param reader */ public LocalWritableLineReader(LineNumberReader reader) { super(reader); } /** * @param reader */ public LocalWritableLineReader(Reader reader) { super(reader); } public void write(String text) throws IOException { super.write(text); if ("".equals(text)) { openBufferChanged(text); } } } private String xssToken; public String getXssToken() { return xssToken; } public void setXssToken(String xssToken) { this.xssToken = xssToken; } /** * Gets the collection of elements whose <code>name</code> attribute is * <code>elementName</code>. <br> * <b>Note:</b> This is not a standard API */ public NodeList getElementsByAtrributeName(String attributeName) { return this.getNodeList(new ElementAttributeFilter(attributeName)); } }