/*
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 May 14, 2005
*/
package org.lobobrowser.primary.clientlets.html;
import java.awt.event.ActionEvent;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.swing.SwingUtilities;
import org.lobobrowser.clientlet.Clientlet;
import org.lobobrowser.clientlet.ClientletContext;
import org.lobobrowser.clientlet.ClientletException;
import org.lobobrowser.clientlet.ClientletResponse;
import org.lobobrowser.html.HtmlRendererContext;
import org.lobobrowser.html.dombl.DocumentNotificationListener;
import org.lobobrowser.html.domimpl.DOMNodeImpl;
import org.lobobrowser.html.domimpl.HTMLDocumentImpl;
import org.lobobrowser.html.gui.HtmlPanel;
import org.lobobrowser.html.parser.DocumentBuilderImpl;
import org.lobobrowser.html.parser.InputSourceImpl;
import org.lobobrowser.primary.info.RefreshInfo;
import org.lobobrowser.ua.NavigatorFrame;
import org.lobobrowser.ua.RequestType;
import org.lobobrowser.util.Strings;
import org.lobobrowser.util.Urls;
import org.lobobrowser.util.io.RecordedInputStream;
import org.lobobrowser.w3c.html.HTMLElement;
import org.w3c.dom.Element;
/**
* The Class HtmlClientlet.
*
* @author J. H. S.
*/
public class HtmlClientlet implements Clientlet {
/** The Constant logger. */
private static final Logger logger = LogManager.getLogger(HtmlClientlet.class);
/** The Constant NON_VISIBLE_ELEMENTS. */
private static final Set<String> NON_VISIBLE_ELEMENTS = new HashSet<String>();
// Maximum buffer size required to determine if a reload due
// to Http-Equiv is necessary.
/** The Constant MAX_IS_BUFFER_SIZE. */
private static final int MAX_IS_BUFFER_SIZE = 1024 * 100;
static {
// Elements that may be encountered and which
// by themselves don't warrant rendering the page yet.
Set<String> nve = NON_VISIBLE_ELEMENTS;
nve.add("html");
nve.add("body");
nve.add("head");
nve.add("title");
nve.add("meta");
nve.add("script");
nve.add("style");
nve.add("link");
}
/**
* Instantiates a new html clientlet.
*/
public HtmlClientlet() {
super();
}
/*
* (non-Javadoc)
*
* @see org.xamjwg.clientlet.Clientlet#parse(org.xamjwg.dom.XDocument)
*/
@Override
public void process(ClientletContext cc) throws ClientletException {
this.processImpl(cc, null, null);
}
/**
* Process impl.
*
* @param cc
* the cc
* @param httpEquivData
* the http equiv data
* @param rin
* the rin
* @throws ClientletException
* the clientlet exception
*/
private void processImpl(final ClientletContext cc, Map<String, String> httpEquivData, RecordedInputStream rin)
throws ClientletException {
// This method may be executed twice, depending on http-equiv meta
// elements.
try {
ClientletResponse response = cc.getResponse();
boolean charsetProvided = response.isCharsetProvided();
String contentLanguage = response.getHeader("Content-Language");
Set<Locale> locales = contentLanguage == null ? null : this.extractLocales(contentLanguage);
RefreshInfo refresh = null;
Iterator hi = response.getHeaderNames();
// TODO: What is the behavior if you have
// a Refresh header and also a Refresh HTTP-EQUIV?
while (hi.hasNext()) {
String headerName = (String) hi.next();
String[] headerValues = response.getHeaders(headerName);
if ((headerValues != null) && (headerValues.length > 0)) {
if ("refresh".equalsIgnoreCase(headerName)) {
refresh = this.extractRefresh(headerValues[headerValues.length - 1]);
}
}
}
String httpEquivCharset = null;
if (httpEquivData != null) {
Iterator<Map.Entry<String, String>> i = httpEquivData.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<String, String> entry = i.next();
String httpEquiv = entry.getKey();
String content = entry.getValue();
if (content != null) {
if ("content-type".equalsIgnoreCase(httpEquiv)) {
httpEquivCharset = this.extractCharset(response.getResponseURL(), content);
} else if ("refresh".equalsIgnoreCase(httpEquiv)) {
refresh = this.extractRefresh(content);
} else if ("content-language".equalsIgnoreCase(httpEquiv)) {
locales = this.extractLocales(content);
}
}
}
}
HtmlRendererContextImpl rcontext = HtmlRendererContextImpl.getHtmlRendererContext(cc.getNavigatorFrame());
DocumentBuilderImpl builder = new DocumentBuilderImpl(rcontext.getUserAgentContext(), rcontext);
if (rin == null) {
InputStream in = response.getInputStream();
rin = in instanceof RecordedInputStream ? (RecordedInputStream) in
: new RecordedInputStream(in, MAX_IS_BUFFER_SIZE);
rin.mark(Short.MAX_VALUE);
} else {
rin.reset();
}
URL responseURL = response.getResponseURL();
String uri = responseURL.toExternalForm();
String charset;
if (!charsetProvided) {
charset = httpEquivCharset;
} else {
// See bug # 2051468. A charset provided
// in headers takes precendence.
charset = response.getCharset();
}
if (charset == null) {
charset = "UTF-8";
}
if (logger.isInfoEnabled()) {
logger.info("process(): charset=" + charset + " for URI=[" + uri + "]");
}
InputSourceImpl is = new InputSourceImpl(rin, uri, charset);
HTMLDocumentImpl document = (HTMLDocumentImpl) builder.createDocument(is);
document.setLocales(locales);
String referrer = cc.getRequest().getReferrer();
document.setReferrer(referrer == null ? "" : referrer);
HtmlPanel panel = rcontext.getHtmlPanel();
// Create a listener that will switch to rendering when appropriate.
final HtmlContent content = new HtmlContent(document, panel, rin, charset);
LocalDocumentNotificationListener listener = new LocalDocumentNotificationListener(document, panel,
rcontext, cc, content, httpEquivData == null);
document.addDocumentNotificationListener(listener);
// Set resulting content before parsing
// to enable incremental rendering.
long time1 = System.currentTimeMillis();
// The load() call starts parsing.
try {
document.load(false);
} catch (HttpEquivRetryException retry) {
if (logger.isInfoEnabled()) {
logger.info("processImpl(): Resetting due to META http-equiv: " + uri);
}
// This is a recursive call, but it doesn't go further
// than one level deep.
this.processImpl(cc, retry.getHttpEquivData(), rin);
return;
}
long time2 = System.currentTimeMillis();
if (logger.isInfoEnabled()) {
logger.info("process(): Parse elapsed=" + (time2 - time1) + " ms.");
if (logger.isInfoEnabled()) {
logger.debug("process(): HTML follows:\r\n" + content.getSourceCode());
}
}
// We're done parsing, but let's make sure
// the listener actually renderered the document.
listener.ensureSwitchedToRendering();
// Scroll to see anchor.
String ref = responseURL.getRef();
if ((ref != null) && (ref.length() != 0)) {
panel.scrollToElement(ref);
}
if (refresh != null) {
String destUri = refresh.getDestinationUrl();
URL currentURL = response.getResponseURL();
URL destURL;
if (destUri == null) {
destURL = currentURL;
} else {
destURL = Urls.createURL(currentURL, destUri);
}
final URL finalURL = destURL;
java.awt.event.ActionListener action = new java.awt.event.ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NavigatorFrame frame = cc.getNavigatorFrame();
if (frame.getComponentContent() == content) {
// Navigate only if the original document is there.
// TODO: Address bar shouldn't change if it's being
// edited.
// TODO: A nagivation action should cancel this
// altogether.
frame.navigate(finalURL, RequestType.PROGRAMMATIC);
}
}
};
int waitMillis = refresh.getWaitSeconds() * 1000;
if (waitMillis <= 0) {
waitMillis = 1;
}
javax.swing.Timer timer = new javax.swing.Timer(waitMillis, action);
timer.setRepeats(false);
timer.start();
}
} catch (Exception err) {
throw new ClientletException(err);
}
}
/**
* Extract charset.
*
* @param responseURL
* the response url
* @param contentType
* the content type
* @return the string
*/
private String extractCharset(URL responseURL, String contentType) {
StringTokenizer tok = new StringTokenizer(contentType, ";");
if (tok.hasMoreTokens()) {
tok.nextToken();
while (tok.hasMoreTokens()) {
String assignment = tok.nextToken().trim();
int eqIdx = assignment.indexOf('=');
if (eqIdx != -1) {
String varName = assignment.substring(0, eqIdx).trim();
if ("charset".equalsIgnoreCase(varName)) {
String varValue = assignment.substring(eqIdx + 1);
return Strings.unquote(varValue.trim());
}
}
}
}
return null;
}
/**
* Extract locales.
*
* @param contentLanguage
* the content language
* @return the sets the
*/
private Set<Locale> extractLocales(String contentLanguage) {
Set<Locale> locales = new HashSet<Locale>(3);
StringTokenizer tok = new StringTokenizer(contentLanguage, ",");
while (tok.hasMoreTokens()) {
String lang = tok.nextToken().trim();
locales.add(new Locale(lang));
}
return locales;
}
/**
* Extract refresh.
*
* @param refresh
* the refresh
* @return the refresh info
*/
private final RefreshInfo extractRefresh(String refresh) {
String delayText = null;
String urlText = null;
StringTokenizer tok = new StringTokenizer(refresh, ";");
if (tok.hasMoreTokens()) {
delayText = tok.nextToken().trim();
while (tok.hasMoreTokens()) {
String assignment = tok.nextToken().trim();
int eqIdx = assignment.indexOf('=');
if (eqIdx != -1) {
String varName = assignment.substring(0, eqIdx).trim();
if ("url".equalsIgnoreCase(varName)) {
String varValue = assignment.substring(eqIdx + 1);
urlText = Strings.unquote(varValue.trim());
}
} else {
urlText = Strings.unquote(assignment);
}
}
}
int delay;
try {
delay = Integer.parseInt(delayText);
} catch (NumberFormatException nfe) {
logger.warn("extractRefresh(): Bad META refresh delay: " + delayText + ".");
delay = 0;
}
return new RefreshInfo(delay, urlText);
}
/**
* The listener interface for receiving localDocumentNotification events.
* The class that is interested in processing a localDocumentNotification
* event implements this interface, and the object created with that class
* is registered with a component using the component's
* <code>addLocalDocumentNotificationListener</code> method. When the
* localDocumentNotification event occurs, that object's appropriate method
* is invoked.
*
* @see LocalDocumentNotificationEvent
*/
private static class LocalDocumentNotificationListener implements DocumentNotificationListener {
/** The Constant MAX_WAIT. */
private static final int MAX_WAIT = 7000;
/** The document. */
private final HTMLDocumentImpl document;
/** The html panel. */
private final HtmlPanel htmlPanel;
/** The start timestamp. */
private final long startTimestamp;
/** The rcontext. */
private final HtmlRendererContext rcontext;
/** The ccontext. */
private final ClientletContext ccontext;
/** The content. */
private final HtmlContent content;
/** The detect http equiv. */
private final boolean detectHttpEquiv;
/** The has visible elements. */
private boolean hasVisibleElements = false;
/** The has switched to rendering. */
private boolean hasSwitchedToRendering = false;
/** The http equiv elements. */
private Collection<HTMLElement> httpEquivElements;
/**
* Instantiates a new local document notification listener.
*
* @param doc
* the doc
* @param panel
* the panel
* @param rcontext
* the rcontext
* @param cc
* the cc
* @param content
* the content
* @param detectHttpEquiv
* the detect http equiv
*/
public LocalDocumentNotificationListener(HTMLDocumentImpl doc, HtmlPanel panel, HtmlRendererContext rcontext,
ClientletContext cc, HtmlContent content, boolean detectHttpEquiv) {
this.document = doc;
this.startTimestamp = System.currentTimeMillis();
this.htmlPanel = panel;
this.rcontext = rcontext;
this.ccontext = cc;
this.content = content;
this.detectHttpEquiv = detectHttpEquiv;
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* allInvalidated()
*/
@Override
public void allInvalidated() {
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* externalScriptLoading (org.lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void externalScriptLoading(DOMNodeImpl node) {
// We can expect this to occur only in the parser thread.
if (this.hasVisibleElements) {
this.ensureSwitchedToRendering();
}
}
/*
* (non-Javadoc)
*
* @see
* org.lobobrowser.html.dombl.DocumentNotificationListener#invalidated(
* org. lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void invalidated(DOMNodeImpl node) {
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* lookInvalidated(org .lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void lookInvalidated(DOMNodeImpl node) {
}
/**
* Adds the http equiv element.
*
* @param element
* the element
*/
private void addHttpEquivElement(HTMLElement element) {
Collection<HTMLElement> httpEquivElements = this.httpEquivElements;
if (httpEquivElements == null) {
httpEquivElements = new LinkedList<HTMLElement>();
this.httpEquivElements = httpEquivElements;
}
httpEquivElements.add(element);
}
/*
* (non-Javadoc)
*
* @see
* org.lobobrowser.html.dombl.DocumentNotificationListener#nodeLoaded(
* org. lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void nodeLoaded(DOMNodeImpl node) {
// We can expect this to occur only in the parser thread.
if (this.detectHttpEquiv) {
if (node instanceof HTMLElement) {
HTMLElement element = (HTMLElement) node;
String tagName = element.getTagName();
if ("meta".equalsIgnoreCase(tagName)) {
String httpEquiv = element.getAttribute("http-equiv");
if (httpEquiv != null) {
this.addHttpEquivElement(element);
}
}
if ("head".equalsIgnoreCase(tagName) || "script".equalsIgnoreCase(tagName)
|| "html".equalsIgnoreCase(tagName)) {
// Note: SCRIPT is checked as an optimization. We do not
// want
// scripts to be processed twice. HTML is checked
// because
// sometimes sites don't put http-equiv in HEAD, e.g.
// http://baidu.com.
Map<String, String> httpEquiv = this.getHttpEquivData();
if ((httpEquiv != null) && (httpEquiv.size() > 0)) {
throw new HttpEquivRetryException(httpEquiv);
}
}
}
}
if (!this.hasVisibleElements) {
if (this.mayBeVisibleElement(node)) {
this.hasVisibleElements = true;
}
}
if (this.hasVisibleElements && ((System.currentTimeMillis() - this.startTimestamp) > MAX_WAIT)) {
this.ensureSwitchedToRendering();
}
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* positionInvalidated (org.lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void positionInvalidated(DOMNodeImpl node) {
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* sizeInvalidated(org .lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void sizeInvalidated(DOMNodeImpl node) {
}
/*
* (non-Javadoc)
*
* @see org.lobobrowser.html.dombl.DocumentNotificationListener#
* structureInvalidated (org.lobobrowser.html.domimpl.DOMNodeImpl)
*/
@Override
public void structureInvalidated(DOMNodeImpl node) {
}
/**
* May be visible element.
*
* @param node
* the node
* @return true, if successful
*/
private final boolean mayBeVisibleElement(DOMNodeImpl node) {
if (node instanceof HTMLElement) {
HTMLElement element = (HTMLElement) node;
boolean visible = !NON_VISIBLE_ELEMENTS.contains(element.getTagName().toLowerCase());
if (visible && logger.isInfoEnabled()) {
logger.info("mayBeVisibleElement(): Found possibly visible element: " + element.getTagName());
}
return visible;
} else {
return false;
}
}
/**
* Ensure switched to rendering.
*/
public void ensureSwitchedToRendering() {
synchronized (this) {
if (this.hasSwitchedToRendering) {
return;
}
this.hasSwitchedToRendering = true;
}
final HTMLDocumentImpl document = this.document;
document.removeDocumentNotificationListener(this);
if (SwingUtilities.isEventDispatchThread()) {
// Should have nicer effect (less flicker) in GUI thread.
htmlPanel.setDocument(document, rcontext);
ccontext.setResultingContent(content);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// Should have nicer effect (less flicker) in GUI
// thread.
htmlPanel.setDocument(document, rcontext);
ccontext.setResultingContent(content);
}
});
}
}
/**
* Gets the http equiv data.
*
* @return the http equiv data
*/
private Map<String, String> getHttpEquivData() {
Collection<HTMLElement> httpEquivElements = this.httpEquivElements;
if (httpEquivElements == null) {
return null;
}
Map<String, String> httpEquivData = new HashMap<String, String>(0);
for (Element element : httpEquivElements) {
String httpEquiv = element.getAttribute("http-equiv");
if (httpEquiv != null) {
String content = element.getAttribute("content");
httpEquivData.put(httpEquiv, content);
}
}
return httpEquivData;
}
}
/**
* The Class HttpEquivRetryException.
*/
private static class HttpEquivRetryException extends RuntimeException {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 1L;
/** The http equiv data. */
private final Map<String, String> httpEquivData;
/**
* Instantiates a new http equiv retry exception.
*
* @param httpEquiv
* the http equiv
*/
public HttpEquivRetryException(final Map<String, String> httpEquiv) {
super();
this.httpEquivData = httpEquiv;
}
/**
* Gets the http equiv data.
*
* @return the http equiv data
*/
public Map<String, String> getHttpEquivData() {
return httpEquivData;
}
}
}