package au.com.vaadinutils.js; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.base.Stopwatch; import com.vaadin.ui.JavaScript; import com.vaadin.ui.JavaScriptFunction; import au.com.vaadinutils.dao.JpaEntityHelper; import au.com.vaadinutils.errorHandling.ErrorWindow; import elemental.json.JsonArray; public class JSCallWithReturnValue { // Logger logger = LogManager.getLogger(); private static final int EXPECTED_RESPONSE_TIME_MS = 1500; // this is a particularly large value as this time may include considerable // server side processing time protected static final int RESPONSE_TIMEOUT_MS = 15000; private String hookName; private String jsToExecute; private String errorHookName; Logger logger = LogManager.getLogger(); private Exception trace; // setting the pool size to 1, we will hopefully never execute any events private final static ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); /** * * @param jsToExecute * - the actual JS you want to execute and get a return value for * like... new Date(); <br> * <br> * JavaScriptCallback <Boolean> callback = new JavaScriptCallback * <Boolean>()<br> * {<br> * * public void callback(Boolean value)<br> * { <br> * // do something here with value<br> * }<br> * };<br> * * new * JSCallWithReturnValue("myMethod(true)").callBoolean(callback); * */ public JSCallWithReturnValue(final String jsToExecute) { this.hookName = "callback" + JpaEntityHelper.getGuid().replace("-", "_"); this.errorHookName = "error" + JpaEntityHelper.getGuid().replace("-", "_"); this.jsToExecute = jsToExecute; } /** * * @param jsToExecute * - the actual JS you want to execute and get a return value for * like... new Date(); <br> * <br> * JavaScriptCallback <Boolean> callback = new JavaScriptCallback * <Boolean>()<br> * {<br> * * public void callback(Boolean value)<br> * { <br> * // do something here with value<br> * }<br> * };<br> * * new * JSCallWithReturnValue(JavaScriptFunctionCall("myMethod",true,1)).callBoolean(callback); * */ public JSCallWithReturnValue(final JavaScriptFunctionCall call) { this(call.getCall()); } public void callBoolean(final JavaScriptCallback<Boolean> callback) { call(new JavaScriptCallback<JsonArray>() { @Override public void callback(JsonArray arguments) { callback.callback(arguments.getBoolean(0)); } }); } public void callString(final JavaScriptCallback<String> callback) { call(new JavaScriptCallback<JsonArray>() { @Override public void callback(JsonArray arguments) { callback.callback(arguments.getString(0)); } }); } public void callVoid(final JavaScriptCallback<Void> callback) { call(new JavaScriptCallback<JsonArray>() { @Override public void callback(JsonArray value) { callback.callback(null); } }); } void call(final JavaScriptCallback<JsonArray> callback) { final Stopwatch timer = Stopwatch.createStarted(); final ScheduledFuture<?> future = createTimeoutHook(); JavaScript.getCurrent().addFunction(hookName, new JavaScriptFunction() { private static final long serialVersionUID = 1L; @Override public void call(JsonArray arguments) { try { if (timer.elapsed(TimeUnit.MILLISECONDS) > EXPECTED_RESPONSE_TIME_MS) { logger.warn("Responded after {}ms", timer.elapsed(TimeUnit.MILLISECONDS)); } logger.debug("Handling response for " + hookName); callback.callback(arguments); } catch (Exception e) { logger.error(e, e); logger.error(trace, trace); } finally { future.cancel(false); removeHooks(hookName, errorHookName); } } }); final String wrappedJs = wrapJSInTryCatch(jsToExecute); setupErrorHook(future); JavaScript.getCurrent().execute(wrappedJs); } void callBlind(final JavaScriptCallback<Void> javaScriptCallback) { final Stopwatch timer = Stopwatch.createStarted(); final ScheduledFuture<?> future = createTimeoutHook(); JavaScript.getCurrent().addFunction(hookName, new JavaScriptFunction() { private static final long serialVersionUID = 1L; @Override public void call(JsonArray arguments) { logger.debug("Handling response for " + hookName); javaScriptCallback.callback(null); future.cancel(false); removeHooks(hookName, errorHookName); if (timer.elapsed(TimeUnit.MILLISECONDS) > EXPECTED_RESPONSE_TIME_MS) { logger.warn("Responded after {}ms", timer.elapsed(TimeUnit.MILLISECONDS)); } } }); setupErrorHook(future); JavaScript.getCurrent().execute(wrapJSInTryCatchBlind(jsToExecute)); } void removeHooks(final String hook1, final String hook2) { final JavaScript js = JavaScript.getCurrent(); js.removeFunction(hook1); js.removeFunction(hook2); } private void setupErrorHook(final ScheduledFuture<?> future) { trace = new JavaScriptException("Java Script Invoked From Here, JS:" + jsToExecute); JavaScript.getCurrent().addFunction(errorHookName, new JavaScriptFunction() { private static final long serialVersionUID = 1L; @Override public void call(JsonArray arguments) { try { String value = arguments.getString(0); logger.error(jsToExecute + " -> resulted in the error: " + value, trace); Exception ex = new JavaScriptException(trace.getMessage() + " , JS Cause: " + value, trace); ErrorWindow.showErrorWindow(ex); } catch (Exception e) { ErrorWindow.showErrorWindow(trace); } finally { future.cancel(false); JavaScript.getCurrent().removeFunction(hookName); JavaScript.getCurrent().removeFunction(errorHookName); } } }); } ScheduledFuture<?> createTimeoutHook() { final Date requestedAt = new Date(); Runnable runner = new Runnable() { @Override public void run() { logger.error(jsToExecute + " -> Timeout " + RESPONSE_TIMEOUT_MS + " requested at " + requestedAt + "ms", trace); } }; ScheduledFuture<?> future = pool.schedule(runner, RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); return future; } private String wrapJSInTryCatch(String js) { js = js.trim(); if (js.endsWith(";")) { js = js.substring(0, js.length() - 1); } final String wrapped = "try{" + hookName + "(" + js + ");}" + "catch(err)" + "{debugger;console.error(err);" + errorHookName + "(err.message+' '+err.stack);};"; logger.debug(wrapped); return wrapped; } private String wrapJSInTryCatchBlind(String js) { js = js.trim(); if (js.endsWith(";")) { js = js.substring(0, js.length() - 1); } final String wrapped = "try{" + js + ";" + hookName + "();}" + "catch(err)" + "{console.error(err);" + errorHookName + "(err.message+' '+err.stack);};"; // logger.error(wrapped); return wrapped; } }