diff --git a/bundles/org.eclipse.rap.rwt/js/rwt/client/JavaScriptExecutor.js b/bundles/org.eclipse.rap.rwt/js/rwt/client/JavaScriptExecutor.js index 204f3c0253..3cf6a505b2 100644 --- a/bundles/org.eclipse.rap.rwt/js/rwt/client/JavaScriptExecutor.js +++ b/bundles/org.eclipse.rap.rwt/js/rwt/client/JavaScriptExecutor.js @@ -7,6 +7,7 @@ * * Contributors: * EclipseSource - initial API and implementation + * Kyle Smith - Add evaluate method ******************************************************************************/ namespace( "rwt.client" ); @@ -17,6 +18,15 @@ rwt.client.JavaScriptExecutor = function() { eval( code ); }; + this.evaluate = function( futureId, code ) { + const remote = rwt.remote.Connection.getInstance().getRemoteObject( this ); + const retval = eval( code ); + remote.call( "complete", { + futureId : futureId, + retval: retval + } ); + }; + }; rwt.client.JavaScriptExecutor.getInstance = function() { diff --git a/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/JavaScriptExecutorHandler.js b/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/JavaScriptExecutorHandler.js index bb17e5e847..9f5cee588e 100644 --- a/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/JavaScriptExecutorHandler.js +++ b/bundles/org.eclipse.rap.rwt/js/rwt/remote/handler/JavaScriptExecutorHandler.js @@ -7,6 +7,7 @@ * * Contributors: * EclipseSource - initial API and implementation + * Kyle Smith - Add evaluate method ******************************************************************************/ rwt.remote.HandlerRegistry.add( "rwt.client.JavaScriptExecutor", { @@ -20,12 +21,16 @@ rwt.remote.HandlerRegistry.add( "rwt.client.JavaScriptExecutor", { destructor : rwt.util.Functions.returnTrue, methods : [ - "execute" + "execute", + "evaluate" ], methodHandler : { "execute" : function( object, args ) { object.execute( args.content ); + }, + "evaluate" : function( object, args ) { + object.evaluate( args.futureId, args.content ); } } diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java index df282b9a81..093fe93474 100644 --- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java +++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java @@ -7,9 +7,12 @@ * * Contributors: * EclipseSource - initial API and implementation + * Kyle Smith - Add evalute method ******************************************************************************/ package org.eclipse.rap.rwt.client.service; +import java.util.concurrent.CompletableFuture; + /** * The JavaScriptExecuter service allows executing JavaScript code on the client. @@ -20,13 +23,35 @@ public interface JavaScriptExecutor extends ClientService { /** - * Evaluate the JavaScript code on the client - * - * If the code throws an error, it will crash the web client. - * Accessing internals of the web client is strongly discouraged. + * Executes the JavaScript code on the client. + *

+ * If the code throws an error, it will crash the web client. Accessing internals of the web + * client is strongly discouraged. * * @param code the JavaScript code to evaluate */ public void execute( String code ); + /** + * Evaluates the JavaScript code on the client. Unlike {@link #execute(String)}, this method + * returns a value (asynchronously) if the specified JavaScript code returns a value. The returned + * {@link CompletableFuture} is completed when the client object sends the return value, if any, + * in the next remote call, which may never happen. If the JavaScript code does not return a + * value, the future is completed with {@code null}. + *

+ * Important: Do not wait for the future to complete in the UI thread or it will cause a + * deadlock. The code to execute is only sent to the client once the UI thread is available, which + * will not happen if the future is blocking it. Instead, use another thread or + * {@link java.util.concurrent.ExecutorService ExecutorService}. + *

+ * If the code throws an error, it will crash the web client. Accessing internals of the web + * client is strongly discouraged. + * + * @param code the JavaScript code to evaluate + * @return a future that will be completed with the result of the JavaScript code, or a future + * that will be completed with {@code null} if the given JavaScript code does not return a + * value (i.e., returns {@code undefined}) + * @since 4.3 + */ + public CompletableFuture evaluate( String code ); } diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl.java index bee6d8ed61..e45ad109bc 100644 --- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl.java +++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl.java @@ -7,24 +7,35 @@ * * Contributors: * EclipseSource - initial API and implementation + * Kyle Smith - Add evalute method ******************************************************************************/ package org.eclipse.rap.rwt.internal.client; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + import org.eclipse.rap.json.JsonObject; +import org.eclipse.rap.json.JsonValue; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.rap.rwt.internal.remote.ConnectionImpl; +import org.eclipse.rap.rwt.remote.AbstractOperationHandler; import org.eclipse.rap.rwt.remote.RemoteObject; +import org.eclipse.swt.internal.widgets.IdGenerator; public final class JavaScriptExecutorImpl implements JavaScriptExecutor { private static final String REMOTE_ID = "rwt.client.JavaScriptExecutor"; private final RemoteObject remoteObject; + private final Map> futures; public JavaScriptExecutorImpl() { ConnectionImpl connection = ( ConnectionImpl )RWT.getUISession().getConnection(); + futures = new HashMap<>(); remoteObject = connection.createServiceObject( REMOTE_ID ); + remoteObject.setHandler( new EvaluateHandler() ); } @Override @@ -32,4 +43,35 @@ public void execute( String code ) { remoteObject.call( "execute", new JsonObject().add( "content", code.trim() ) ); } + @Override + public CompletableFuture evaluate( String code ) { + CompletableFuture future = new CompletableFuture<>(); + String id = IdGenerator.getInstance( RWT.getUISession() ).createId( future ); + futures.put( id, future ); + remoteObject.call( "evaluate", + new JsonObject().add( "futureId", id ).add( "content", code.trim() ) ); + return future; + } + + private final class EvaluateHandler extends AbstractOperationHandler { + + @Override + public void handleCall( String method, JsonObject properties ) { + if( "complete".equals( method ) ) { + JsonValue idJson = properties.get( "futureId" ); + JsonValue retvalJson = properties.get( "retval" ); + if( idJson != null && idJson.isString() ) { + CompletableFuture future = futures.get( idJson.asString() ); + if( future != null ) { + if( retvalJson != null ) { + future.complete( retvalJson.toString() ); + } else { + future.complete( null ); + } + futures.remove( idJson.asString() ); + } + } + } + } + } } diff --git a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl_Test.java b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl_Test.java index 77d60e94b0..1d6d289236 100644 --- a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl_Test.java +++ b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/client/JavaScriptExecutorImpl_Test.java @@ -18,6 +18,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.util.concurrent.CompletableFuture; + import org.eclipse.rap.json.JsonObject; import org.eclipse.rap.rwt.internal.remote.ConnectionImpl; import org.eclipse.rap.rwt.remote.RemoteObject; @@ -75,6 +77,33 @@ public void testExecute_createsSeparateOperationForEveryCall() { verify( remoteObject ).call( eq( "execute" ), eq( new JsonObject().add( "content", "code 2" ) ) ); verifyNoMoreInteractions( remoteObject ); } + + @Test + public void testEvaluate_createsCallOperation() { + RemoteObject remoteObject = mock( RemoteObject.class ); + fakeConnection( remoteObject ); + JavaScriptExecutorImpl executor = new JavaScriptExecutorImpl(); + + CompletableFuture future = executor.evaluate( "5 + 6" ); + + verify( remoteObject ).call( eq( "evaluate" ), eq( new JsonObject().add( "content", "5+6" ) ) ); + verifyNoMoreInteractions( remoteObject ); + } + + @Test + public void testEvaluate_createsSeparateOperationForEveryCall() { + RemoteObject remoteObject = mock( RemoteObject.class ); + fakeConnection( remoteObject ); + JavaScriptExecutorImpl executor = new JavaScriptExecutorImpl(); + + CompletableFuture future = executor.evaluate( "5 + 6" ); + CompletableFuture future2 = executor.evaluate( "Date.now()" ); + + verify( remoteObject ).call( eq( "evaluate" ), eq( new JsonObject().add( "content", "5+6" ) ) ); + verify( remoteObject ).call( eq( "evaluate" ), eq( new JsonObject().add( "content", "Date.now()" ) ) ); + + verifyNoMoreInteractions( remoteObject ); + } private static ConnectionImpl fakeConnection( RemoteObject remoteObject ) { ConnectionImpl connection = mock( ConnectionImpl.class ); @@ -82,5 +111,4 @@ private static ConnectionImpl fakeConnection( RemoteObject remoteObject ) { Fixture.fakeConnection( connection ); return connection; } - }