com.ui4j.webkit.WebKitBrowser Maven / Gradle / Ivy
The newest version!
package com.ui4j.webkit;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;
import org.w3c.dom.Node;
import com.sun.webkit.dom.DocumentImpl;
import com.sun.webkit.network.URLs;
import com.ui4j.api.browser.BrowserEngine;
import com.ui4j.api.browser.BrowserType;
import com.ui4j.api.browser.Page;
import com.ui4j.api.browser.PageConfiguration;
import com.ui4j.api.dom.Document;
import com.ui4j.api.dom.Window;
import com.ui4j.api.event.DocumentListener;
import com.ui4j.api.event.DocumentLoadEvent;
import com.ui4j.api.interceptor.Interceptor;
import com.ui4j.api.interceptor.Response;
import com.ui4j.api.util.Logger;
import com.ui4j.api.util.LoggerFactory;
import com.ui4j.api.util.Ui4jException;
import com.ui4j.spi.PageContext;
import com.ui4j.spi.ShutdownListener;
import com.ui4j.spi.Ui4jExecutionTimeoutException;
import com.ui4j.webkit.browser.Ui4jHandler;
import com.ui4j.webkit.browser.WebKitPage;
import com.ui4j.webkit.browser.WebKitPageContext;
import com.ui4j.webkit.browser.WebKitWindow;
import com.ui4j.webkit.dom.WebKitDocument;
import com.ui4j.webkit.dom.WebKitElement;
import com.ui4j.webkit.proxy.WebKitProxy;
import com.ui4j.webkit.spi.WebKitJavaScriptEngine;
class WebKitBrowser implements BrowserEngine {
private static CountDownLatch startupLatch = new CountDownLatch(1);
private static AtomicBoolean launchedJFX = new AtomicBoolean(false);
private ShutdownListener shutdownListener;
private AtomicInteger pageCounter = new AtomicInteger(0);
private static final Logger LOG = LoggerFactory.getLogger(WebKitBrowser.class);
private WebKitProxy elementFactory = new WebKitProxy(WebKitElement.class, new Class[] {
Node.class, Document.class,
PageContext.class, WebKitJavaScriptEngine.class
});
private WebKitProxy documentFactory = new WebKitProxy(WebKitDocument.class, new Class[] {
PageContext.class, DocumentImpl.class,
WebKitJavaScriptEngine.class
});
private WebKitProxy windowFactory = new WebKitProxy(WebKitWindow.class, new Class[] {
Document.class
});
private WebKitProxy pageFactory = new WebKitProxy(WebKitPage.class, new Class[] {
WebView.class, WebKitJavaScriptEngine.class,
Window.class, Document.class, int.class
});
WebKitBrowser(ShutdownListener shutdownListener) {
this.shutdownListener = shutdownListener;
if (!Platform.isFxApplicationThread()) {
start();
}
}
public static class ApplicationImpl extends Application {
@Override
public void start(Stage stage) {
startupLatch.countDown();
}
}
public static class SyncDocumentListener implements DocumentListener {
private CountDownLatch latch;
private Window window;
private Document document;
public SyncDocumentListener(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void onLoad(DocumentLoadEvent event) {
this.window = event.getWindow();
this.document = event.getDocument();
latch.countDown();
}
public Document getDocument() {
return document;
}
public Window getWindow() {
return window;
}
}
public static class LauncherThread extends Thread {
private boolean headless;
public LauncherThread(boolean headless) {
this.headless = headless;
}
@Override
public void run() {
new ApplicationLauncher().launch(ApplicationImpl.class, headless);
}
}
public static class ExitRunner implements Runnable {
private CountDownLatch latch;
public ExitRunner(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
if (Platform.isFxApplicationThread()) {
Platform.exit();
}
latch.countDown();
}
}
@SuppressWarnings("rawtypes")
public static class EmptyObservableValue implements ObservableValue {
@Override
public void addListener(InvalidationListener listener) { }
@Override
public void removeListener(InvalidationListener listener) { }
@Override
public void addListener(ChangeListener listener) { }
@Override
public Object getValue() { return null; }
@Override
public void removeListener(ChangeListener listener) { }
}
public static class WorkerLoadListener implements ChangeListener {
private WebKitPageContext configuration;
private DocumentListener documentListener;
private WebKitJavaScriptEngine engine;
private Ui4jHandler handler;
public WorkerLoadListener(WebKitJavaScriptEngine engine, PageContext context, DocumentListener documentListener, Ui4jHandler handler) {
this.engine = engine;
this.configuration = (WebKitPageContext) context;
this.documentListener = documentListener;
this.handler = handler;
}
@Override
public void changed(ObservableValue extends Worker.State> ov, Worker.State oldState, Worker.State newState) {
if (newState == Worker.State.SUCCEEDED) {
Document document = configuration.createDocument(engine);
configuration.onLoad(document);
Window window = configuration.createWindow(document);
DocumentLoadEvent event = new DocumentLoadEvent(window);
documentListener.onLoad(event);
if (configuration.getConfiguration().getInterceptor() != null && handler != null) {
URLConnection connection = handler.getConnection();
Map> headers = connection.getHeaderFields();
Response response = new Response(window.getLocation(), Collections.unmodifiableMap(new HashMap<>(headers)));
configuration.getConfiguration().getInterceptor().afterLoad(response);
}
}
}
}
public static class ProgressListener implements ChangeListener {
private WebEngine engine;
public ProgressListener(WebEngine engine) {
this.engine = engine;
}
@Override
public void changed(ObservableValue extends Number> observable, Number oldValue, Number newValue) {
double progress = Math.floor((double) newValue * 100);
if (progress % 5 == 0 || progress % 10 == 0) {
WebKitBrowser.LOG.info(String.format("Loading %s [%d%%]", engine.getLocation(), (int) progress));
}
}
}
public static class WebViewCreator implements Runnable {
private WebView webView;
private String url;
private CountDownLatch latch;
private PageContext context;
private DocumentListener listener;
private WebKitJavaScriptEngine engine;
private PageConfiguration configuration;
private Ui4jHandler handler;
public WebViewCreator(String url,
PageContext context, DocumentListener listener, PageConfiguration configuration, Ui4jHandler handler) {
this(url, context, listener, null, configuration, handler);
}
public WebViewCreator(String url,
PageContext context, DocumentListener listener, CountDownLatch latch, PageConfiguration configuration, Ui4jHandler handler) {
this.url = url;
this.latch = latch;
this.context = context;
this.listener = listener;
this.configuration = configuration;
this.handler = handler;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
webView = new WebView();
engine = new WebKitJavaScriptEngine(webView.getEngine());
if (configuration.getUserAgent() != null) {
engine.getEngine().setUserAgent(configuration.getUserAgent());
}
engine.getEngine().load(url);
WorkerLoadListener loadListener = new WorkerLoadListener(engine, context, listener, handler);
webView.getEngine().getLoadWorker(). progressProperty().addListener(new ProgressListener(webView.getEngine()));
// load blank pages immediately
if (url == null || url.trim().equals("about:blank") || url.trim().equals("")) {
loadListener.changed(new EmptyObservableValue(), Worker.State.SCHEDULED, Worker.State.SUCCEEDED);
} else {
engine.getEngine().getLoadWorker().stateProperty().addListener(loadListener);
}
installErrorHandler();
if (latch != null) {
latch.countDown();
}
}
protected void installErrorHandler() {
JSObject objWindow = (JSObject) engine.getEngine().executeScript("window");
objWindow.setMember("Ui4jErrorHandler", new WebKitErrorHandler());
engine.getEngine().executeScript("window.onerror = function(message, url, lineNumber) { Ui4jErrorHandler.onError(message, url, lineNumber); return false; }");
}
public WebView getWebView() {
return webView;
}
public WebKitJavaScriptEngine getEngine() {
return engine;
}
}
@Override
public synchronized Page navigate(String url) {
return navigate(url, new PageConfiguration());
}
@Override
@SuppressWarnings("unchecked")
public Page navigate(String url, PageConfiguration configuration) {
WebKitPageContext context = new WebKitPageContext(configuration,
elementFactory, documentFactory,
windowFactory, pageFactory);
int pageId = pageCounter.incrementAndGet();
Interceptor interceptor = configuration.getInterceptor();
String ui4jUrl = url;
Ui4jHandler handler = null;
if (interceptor != null) {
String ui4jProtocol = "ui4j-" + pageId;
ui4jUrl = ui4jProtocol + ":" + url;
handler = new Ui4jHandler(interceptor);
try {
// HACK #26
Field handlerMap = URLs.class.getDeclaredField("handlerMap");
handlerMap.setAccessible(true);
Map handlers = (Map) handlerMap.get(null);
handlers.put(ui4jProtocol, handler);
// HACK #26
} catch (IllegalArgumentException | IllegalAccessException
| NoSuchFieldException | SecurityException e) {
throw new Ui4jException(e);
}
}
CountDownLatch documentReadyLatch = new CountDownLatch(1);
SyncDocumentListener adapter = new SyncDocumentListener(documentReadyLatch);
WebViewCreator creator = null;
if (Platform.isFxApplicationThread()) {
creator = new WebViewCreator(ui4jUrl, context, adapter, configuration, handler);
creator.run();
} else {
CountDownLatch webViewLatch = new CountDownLatch(1);
creator = new WebViewCreator(ui4jUrl, context, adapter, webViewLatch, configuration, handler);
Platform.runLater(creator);
try {
webViewLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new Ui4jExecutionTimeoutException(e, 10, TimeUnit.SECONDS);
}
}
try {
documentReadyLatch.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new Ui4jExecutionTimeoutException(e, 60, TimeUnit.SECONDS);
}
WebView webView = creator.getWebView();
WebKitPage page = ((WebKitPageContext) context).newPage(webView, creator.getEngine(), adapter.getWindow(), adapter.getDocument(), pageId);
return page;
}
public synchronized void start() {
if (launchedJFX.compareAndSet(false, true) &&
!Platform.isFxApplicationThread()) {
applyURLsHack();
boolean headless = System.getProperty("ui4j.headless") != null ? true : false;
new LauncherThread(headless).start();
try {
startupLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new Ui4jExecutionTimeoutException(e, 10, TimeUnit.SECONDS);
}
}
}
@Override
public synchronized void shutdown() {
if (launchedJFX.get()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(new ExitRunner(latch));
shutdownListener.onShutdown(this);
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new Ui4jExecutionTimeoutException(e, 10, TimeUnit.SECONDS);
}
}
}
@Override
public BrowserType getBrowserType() {
return BrowserType.WebKit;
}
// Hack #26
//
// https://github.com/ui4j/ui4j/issues/26
//
// WebView api doesnt let to intercept HTTP request.
// we need to apply our modifiable handlers hack until public api supports interceptors.
//
// We register custom URLStreamHandler per web page.
// Each page has its own handler so that we could intercept the request.
// @see Ui4jHandler class for implementation details.
//
// Hack #26
private void applyURLsHack() {
try {
ConcurrentHashMap