package com.ui4j.webkit.proxy; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.web.WebEngine; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.AllArguments; import net.bytebuddy.implementation.bind.annotation.Origin; import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.implementation.bind.annotation.SuperCall; import net.bytebuddy.implementation.bind.annotation.This; import net.bytebuddy.matcher.ElementMatchers; import com.ui4j.api.util.Ui4jException; import com.ui4j.spi.Ui4jExecutionTimeoutException; import com.ui4j.webkit.dom.WebKitDocument; public class WebKitProxy { public static class CallableExecutor implements Runnable { private CountDownLatch latch; private Callable<Object> callable; private Object result; public CallableExecutor(CountDownLatch latch, Callable<Object> callable) { this.latch = latch; this.callable = callable; } @Override public void run() { try { result = callable.call(); } catch (Exception e) { throw new Ui4jException(e); } finally { latch.countDown(); } } public Object getResult() { return result; } } private static class LoadListener implements ChangeListener<Boolean> { private CountDownLatch latch; public LoadListener(CountDownLatch latch) { this.latch = latch; } @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { if (newValue.equals(Boolean.FALSE)) { // finished loading latch.countDown(); } } }; private static class LoadingRunner implements Runnable { private WebEngine engine; private CountDownLatch latch; private boolean loading; public LoadingRunner(WebEngine engine, CountDownLatch latch) { this.engine = engine; this.latch = latch; } @Override public void run() { loading = engine.getLoadWorker().isRunning(); latch.countDown(); } public boolean isLoading() { return loading; } } public static class WebKitInterceptor { @RuntimeType public static Object execute(@SuperCall Callable<Object> callable, @This Object that, @Origin Method method, @AllArguments Object[] arguments) { if (that instanceof WebKitDocument) { WebKitDocument document = (WebKitDocument) that; boolean loading = false; if (Platform.isFxApplicationThread()) { loading = document.getEngine().getLoadWorker().isRunning(); } else { CountDownLatch loadingLatch = new CountDownLatch(1); LoadingRunner loadingRunner = new LoadingRunner(document.getEngine(), loadingLatch); Platform.runLater(loadingRunner); try { loadingLatch.await(60, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new Ui4jExecutionTimeoutException(e, 60, TimeUnit.SECONDS); } loading = loadingRunner.isLoading(); if (loading) { CountDownLatch listenerLatch = new CountDownLatch(1); LoadListener listener = new LoadListener(listenerLatch); Platform.runLater(() -> document.getEngine().getLoadWorker().runningProperty().addListener(listener)); try { listenerLatch.await(60, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new Ui4jExecutionTimeoutException(e, 60, TimeUnit.SECONDS); } Platform.runLater(() -> document.getEngine().getLoadWorker().runningProperty().removeListener(listener)); document.refreshDocument(); } } } Object ret = null; if (!Platform.isFxApplicationThread()) { CountDownLatch latch = new CountDownLatch(1); CallableExecutor executor = new CallableExecutor(latch, callable); Platform.runLater(executor); try { latch.await(60, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new Ui4jExecutionTimeoutException(e, 60, TimeUnit.SECONDS); } ret = executor.getResult(); } else { try { ret = callable.call(); } catch (Exception e) { throw new Ui4jException(e); } } return ret; } } private Constructor<?> constructor; private Class<?> proxyClass; public WebKitProxy(Class<?> klass, Class<?>[] constructorArguments) { Class<?> loaded = new ByteBuddy() .subclass(klass) .method(ElementMatchers.any() .and(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class)) .and(ElementMatchers.not(ElementMatchers.nameStartsWith("getEngine"))))) .intercept(MethodDelegation.to(WebKitInterceptor.class)) .make() .load(WebKitProxy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); this.proxyClass = loaded; try { constructor = loaded.getConstructor(constructorArguments); } catch (NoSuchMethodException | SecurityException e) { throw new Ui4jException(e); } } public Object newInstance(Object[] arguments) { Object instance = null; try { instance = constructor.newInstance(arguments); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new Ui4jException(e); } return instance; } public Class<?> getProxyClass() { return proxyClass; } }