All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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 handlers = new ConcurrentHashMap<>();
	        handlers.put("about", new com.sun.webkit.network.about.Handler());
	        handlers.put("data", new com.sun.webkit.network.data.Handler());
	    	setFinalStatic(URLs.class.getDeclaredField("handlerMap"), handlers);
		} catch (NoSuchFieldException | SecurityException |
								IllegalArgumentException e) {
			throw new Ui4jException(e);
		}
	}

    private static void setFinalStatic(Field field, Object newValue) {
		try {
			field.setAccessible(true);
			Field modifiersField = Field.class.getDeclaredField("modifiers");
			modifiersField.setAccessible(true);
			modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
			field.set(null, newValue);
		} catch (NoSuchFieldException | SecurityException |
								IllegalArgumentException | IllegalAccessException e) {
			throw new Ui4jException(e);
		}
     }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy