From: Henri Sara Date: Wed, 29 Feb 2012 15:16:00 +0000 (+0200) Subject: Implement server to client RPC (#8426). X-Git-Tag: 7.0.0.alpha2~412 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=121b1dae346d60896205c4db5bc0c0db0c504a41;p=vaadin-framework.git Implement server to client RPC (#8426). --- diff --git a/src/com/vaadin/data/Buffered.java b/src/com/vaadin/data/Buffered.java index f6398f585b..552bd16d04 100644 --- a/src/com/vaadin/data/Buffered.java +++ b/src/com/vaadin/data/Buffered.java @@ -5,6 +5,8 @@ package com.vaadin.data; import java.io.Serializable; +import java.util.Collections; +import java.util.List; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.terminal.ErrorMessage; @@ -12,6 +14,7 @@ import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.SystemError; import com.vaadin.terminal.gwt.client.communication.SharedState; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** *

@@ -338,6 +341,10 @@ public interface Buffered extends Serializable { return null; } + public List retrievePendingRpcCalls() { + return Collections.emptyList(); + } + /* Documented in super interface */ public void addListener(RepaintRequestListener listener) { } diff --git a/src/com/vaadin/data/Validator.java b/src/com/vaadin/data/Validator.java index bc64594edb..f07e957395 100644 --- a/src/com/vaadin/data/Validator.java +++ b/src/com/vaadin/data/Validator.java @@ -5,12 +5,15 @@ package com.vaadin.data; import java.io.Serializable; +import java.util.Collections; +import java.util.List; import com.vaadin.terminal.ErrorMessage; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** * Interface that implements a method for validating if an {@link Object} is @@ -173,6 +176,10 @@ public interface Validator extends Serializable { return null; } + public List retrievePendingRpcCalls() { + return Collections.emptyList(); + } + /** * Returns the message of the error in HTML. * diff --git a/src/com/vaadin/terminal/CompositeErrorMessage.java b/src/com/vaadin/terminal/CompositeErrorMessage.java index 2ec1fc949e..efe29196d8 100644 --- a/src/com/vaadin/terminal/CompositeErrorMessage.java +++ b/src/com/vaadin/terminal/CompositeErrorMessage.java @@ -7,10 +7,12 @@ package com.vaadin.terminal; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import com.vaadin.terminal.gwt.client.communication.SharedState; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** * Class for combining multiple error messages together. @@ -138,6 +140,10 @@ public class CompositeErrorMessage implements ErrorMessage, Serializable { return null; } + public List retrievePendingRpcCalls() { + return Collections.emptyList(); + } + /* Documented in super interface */ public void addListener(RepaintRequestListener listener) { } diff --git a/src/com/vaadin/terminal/Paintable.java b/src/com/vaadin/terminal/Paintable.java index 9026a45ca3..435b00a0e4 100644 --- a/src/com/vaadin/terminal/Paintable.java +++ b/src/com/vaadin/terminal/Paintable.java @@ -6,8 +6,10 @@ package com.vaadin.terminal; import java.io.Serializable; import java.util.EventObject; +import java.util.List; import com.vaadin.terminal.gwt.client.communication.SharedState; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** * Interface implemented by all classes that can be painted. Classes @@ -54,6 +56,17 @@ public interface Paintable extends java.util.EventListener, Serializable { */ public SharedState getState(); + /** + * Returns the list of pending server to client RPC calls and clears the + * list. + * + * @return unmodifiable ordered list of pending server to client method + * calls (not null) + * + * @since 7.0 + */ + public List retrievePendingRpcCalls(); + /** * Requests that the paintable should be repainted as soon as possible. */ diff --git a/src/com/vaadin/terminal/SystemError.java b/src/com/vaadin/terminal/SystemError.java index 55e62abb0a..462450422b 100644 --- a/src/com/vaadin/terminal/SystemError.java +++ b/src/com/vaadin/terminal/SystemError.java @@ -6,9 +6,12 @@ package com.vaadin.terminal; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Collections; +import java.util.List; import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** * SystemError is a runtime exception caused by error in system. @@ -96,6 +99,10 @@ public class SystemError extends RuntimeException implements ErrorMessage { return null; } + public List retrievePendingRpcCalls() { + return Collections.emptyList(); + } + /** * Returns the message of the error in HTML. * diff --git a/src/com/vaadin/terminal/UserError.java b/src/com/vaadin/terminal/UserError.java index 50cf32ab0c..15473e47e1 100644 --- a/src/com/vaadin/terminal/UserError.java +++ b/src/com/vaadin/terminal/UserError.java @@ -4,8 +4,12 @@ package com.vaadin.terminal; +import java.util.Collections; +import java.util.List; + import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; /** * UserError is a controlled error occurred in application. User @@ -149,6 +153,10 @@ public class UserError implements ErrorMessage { return null; } + public List retrievePendingRpcCalls() { + return Collections.emptyList(); + } + /* Documented in interface */ public void requestRepaintRequests() { } diff --git a/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml index 04f291d599..6a8a5df707 100644 --- a/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml +++ b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml @@ -47,6 +47,12 @@ --> + + + + --> + diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index ec1ca7e585..869c387a2f 100644 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -39,6 +39,7 @@ import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage; import com.vaadin.terminal.gwt.client.communication.JsonDecoder; import com.vaadin.terminal.gwt.client.communication.JsonEncoder; import com.vaadin.terminal.gwt.client.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.communication.RpcManager; import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.client.ui.RootConnector; import com.vaadin.terminal.gwt.client.ui.VContextMenu; @@ -54,8 +55,8 @@ import com.vaadin.terminal.gwt.server.AbstractCommunicationManager; * * Client-side widgets receive updates from the corresponding server-side * components as calls to - * {@link VPaintableWidget#updateFromUIDL(UIDL, ApplicationConnection)} (not to - * be confused with the server side interface + * {@link ComponentConnector#updateFromUIDL(UIDL, ApplicationConnection)} (not + * to be confused with the server side interface * {@link com.vaadin.terminal.Paintable} ). Any client-side changes (typically * resulting from user actions) are sent back to the server as variable changes * (see {@link #updateVariable()}). @@ -167,6 +168,8 @@ public class ApplicationConnection { private final LayoutManager layoutManager = new LayoutManager(this); + private final RpcManager rpcManager = GWT.create(RpcManager.class); + public ApplicationConnection() { view = GWT.create(RootConnector.class); } @@ -808,7 +811,7 @@ public class ApplicationConnection { */ private void cleanVariableBurst(ArrayList variableBurst) { for (int i = 1; i < variableBurst.size(); i++) { - String id = variableBurst.get(i).getPaintableId(); + String id = variableBurst.get(i).getConnectorId(); if (!getConnectorMap().hasConnector(id) && !getConnectorMap().isDragAndDropPaintable(id)) { // variable owner does not exist anymore @@ -1088,6 +1091,27 @@ public class ApplicationConnection { } } + if (json.containsKey("rpc")) { + VConsole.log(" * Performing server to client RPC calls"); + + JSONArray rpcCalls = new JSONArray( + json.getJavaScriptObject("rpc")); + + int rpcLength = rpcCalls.size(); + for (int i = 0; i < rpcLength; i++) { + try { + JSONArray rpcCall = (JSONArray) rpcCalls.get(i); + MethodInvocation invocation = parseMethodInvocation(rpcCall); + VConsole.log("Server to client RPC call: " + + invocation); + rpcManager.applyInvocation(invocation, + getConnectorMap()); + } catch (final Throwable e) { + VConsole.error(e); + } + } + } + if (json.containsKey("dd")) { // response contains data for drag and drop service VDragAndDropManager.get().handleServerResponse( @@ -1163,10 +1187,25 @@ public class ApplicationConnection { endRequest(); } + }; ApplicationConfiguration.runWhenWidgetsLoaded(c); } + private MethodInvocation parseMethodInvocation(JSONArray rpcCall) { + String connectorId = ((JSONString) rpcCall.get(0)).stringValue(); + String interfaceName = ((JSONString) rpcCall.get(1)).stringValue(); + String methodName = ((JSONString) rpcCall.get(2)).stringValue(); + JSONArray parametersJson = (JSONArray) rpcCall.get(3); + Object[] parameters = new Object[parametersJson.size()]; + for (int j = 0; j < parametersJson.size(); ++j) { + parameters[j] = JsonDecoder.convertValue( + (JSONArray) parametersJson.get(j), getConnectorMap()); + } + return new MethodInvocation(connectorId, interfaceName, methodName, + parameters); + } + // Redirect browser, null reloads current page private static native void redirect(String url) /*-{ @@ -1177,11 +1216,11 @@ public class ApplicationConnection { } }-*/; - private void addVariableToQueue(String paintableId, String variableName, + private void addVariableToQueue(String connectorId, String variableName, Object value, boolean immediate) { // note that type is now deduced from value // TODO could eliminate invocations of same shared variable setter - addMethodInvocationToQueue(new MethodInvocation(paintableId, + addMethodInvocationToQueue(new MethodInvocation(connectorId, UPDATE_VARIABLE_INTERFACE, UPDATE_VARIABLE_METHOD, new Object[] { variableName, value }), immediate); } @@ -1273,7 +1312,7 @@ public class ApplicationConnection { for (MethodInvocation invocation : pendingInvocations) { JSONArray invocationJson = new JSONArray(); invocationJson.set(0, - new JSONString(invocation.getPaintableId())); + new JSONString(invocation.getConnectorId())); invocationJson.set(1, new JSONString(invocation.getInterfaceName())); invocationJson.set(2, diff --git a/src/com/vaadin/terminal/gwt/client/Connector.java b/src/com/vaadin/terminal/gwt/client/Connector.java index eee525865b..67202ce02b 100644 --- a/src/com/vaadin/terminal/gwt/client/Connector.java +++ b/src/com/vaadin/terminal/gwt/client/Connector.java @@ -3,6 +3,9 @@ */ package com.vaadin.terminal.gwt.client; +import java.util.Collection; + +import com.vaadin.terminal.gwt.client.communication.ClientRpc; import com.vaadin.terminal.gwt.client.communication.SharedState; /** @@ -73,4 +76,18 @@ public interface Connector { */ public void doInit(String connectorId, ApplicationConnection connection); + /** + * For internal use by the framework: returns the registered RPC + * implementations for an RPC interface identifier. + * + * TODO interface identifier type or format may change + * + * @param rpcInterfaceId + * RPC interface identifier: fully qualified interface type name + * @return RPC interface implementations registered for an RPC interface, + * not null + */ + public Collection getRpcImplementations( + String rpcInterfaceId); + } diff --git a/src/com/vaadin/terminal/gwt/client/Util.java b/src/com/vaadin/terminal/gwt/client/Util.java index 5ed30eaff1..572a9bebb2 100644 --- a/src/com/vaadin/terminal/gwt/client/Util.java +++ b/src/com/vaadin/terminal/gwt/client/Util.java @@ -858,7 +858,7 @@ public class Util { String curId = null; ArrayList invocations = new ArrayList(); for (int i = 0; i < loggedBurst.size(); i++) { - String id = loggedBurst.get(i).getPaintableId(); + String id = loggedBurst.get(i).getConnectorId(); if (curId == null) { curId = id; diff --git a/src/com/vaadin/terminal/gwt/client/communication/ClientRpc.java b/src/com/vaadin/terminal/gwt/client/communication/ClientRpc.java new file mode 100644 index 0000000000..45dbe69454 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/communication/ClientRpc.java @@ -0,0 +1,23 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.communication; + +import java.io.Serializable; + +/** + * Interface to be extended by all server to client RPC interfaces. + * + * On the server side, proxies of the interface can be obtained from + * AbstractComponent. On the client, RPC implementations can be registered with + * AbstractConnector.registerRpc(). + * + * Note: Currently, each RPC interface may not contain multiple methods with the + * same name, even if their parameter lists would differ. + * + * @since 7.0 + */ +public interface ClientRpc extends Serializable { + +} diff --git a/src/com/vaadin/terminal/gwt/client/communication/MethodInvocation.java b/src/com/vaadin/terminal/gwt/client/communication/MethodInvocation.java index 17e1ce30e3..f4305ffcaa 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/MethodInvocation.java +++ b/src/com/vaadin/terminal/gwt/client/communication/MethodInvocation.java @@ -4,6 +4,8 @@ package com.vaadin.terminal.gwt.client.communication; +import java.util.Arrays; + /** * Information needed by the framework to send an RPC method invocation from the * client to the server or vice versa. @@ -12,21 +14,21 @@ package com.vaadin.terminal.gwt.client.communication; */ public class MethodInvocation { - private final String paintableId; + private final String connectorId; private final String interfaceName; private final String methodName; private final Object[] parameters; - public MethodInvocation(String paintableId, String interfaceName, + public MethodInvocation(String connectorId, String interfaceName, String methodName, Object[] parameters) { - this.paintableId = paintableId; + this.connectorId = connectorId; this.interfaceName = interfaceName; this.methodName = methodName; this.parameters = parameters; } - public String getPaintableId() { - return paintableId; + public String getConnectorId() { + return connectorId; } public String getInterfaceName() { @@ -40,4 +42,10 @@ public class MethodInvocation { public Object[] getParameters() { return parameters; } + + @Override + public String toString() { + return connectorId + ":" + interfaceName + "." + methodName + "(" + + Arrays.toString(parameters) + ")"; + } } \ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java b/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java new file mode 100644 index 0000000000..302e6eaa55 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/communication/RpcManager.java @@ -0,0 +1,32 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.communication; + +import java.io.Serializable; + +import com.vaadin.terminal.gwt.client.ConnectorMap; + +/** + * Client side RPC manager that can invoke methods based on RPC calls received + * from the server. + * + * A GWT generator is used to create an implementation of this class at + * run-time. + * + * @since 7.0 + */ +public interface RpcManager extends Serializable { + /** + * Perform server to client RPC invocation. + * + * @param invocation + * method to invoke + * @param connectorMap + * mapper used to find Connector for the method call and any + * connectors referenced in parameters + */ + public void applyInvocation(MethodInvocation invocation, + ConnectorMap connectorMap); +} diff --git a/src/com/vaadin/terminal/gwt/client/communication/ServerRpc.java b/src/com/vaadin/terminal/gwt/client/communication/ServerRpc.java index c08008e5c1..36f23a0cc1 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/ServerRpc.java +++ b/src/com/vaadin/terminal/gwt/client/communication/ServerRpc.java @@ -11,8 +11,8 @@ import com.vaadin.terminal.gwt.client.ApplicationConnection; /** * Interface to be extended by all client to server RPC interfaces. * - * The nested interface InitializableClientToServerRpc The - * {@link #initRpc(String, ApplicationConnection)} method is created + * The nested interface InitializableClientToServerRpc has an + * {@link #initRpc(String, ApplicationConnection)} method, which is created * automatically by a GWT generator and must be called on the client side before * generated implementations of the interface are used to perform RPC calls. * diff --git a/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java b/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java index 2b5bccd66f..7232f4ed7e 100755 --- a/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/AbstractConnector.java @@ -1,13 +1,22 @@ package com.vaadin.terminal.gwt.client.ui; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.Connector; +import com.vaadin.terminal.gwt.client.communication.ClientRpc; public abstract class AbstractConnector implements Connector { private ApplicationConnection connection; private String id; + private Map> rpcImplementations; + /* * (non-Javadoc) * @@ -61,4 +70,52 @@ public abstract class AbstractConnector implements Connector { } + /** + * Registers an implementation for a server to client RPC interface. + * + * Multiple registrations can be made for a single interface, in which case + * all of them receive corresponding RPC calls. + * + * @param rpcInterface + * RPC interface + * @param implementation + * implementation that should receive RPC calls + */ + protected void registerRpc(Class rpcInterface, + T implementation) { + String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", "."); + if (null == rpcImplementations) { + rpcImplementations = new HashMap>(); + } + if (null == rpcImplementations.get(rpcInterfaceId)) { + rpcImplementations.put(rpcInterfaceId, new ArrayList()); + } + rpcImplementations.get(rpcInterfaceId).add(implementation); + } + + /** + * Unregisters an implementation for a server to client RPC interface. + * + * @param rpcInterface + * RPC interface + * @param implementation + * implementation to unregister + */ + protected void unregisterRpc(Class rpcInterface, + T implementation) { + String rpcInterfaceId = rpcInterface.getName().replaceAll("\\$", "."); + if (null != rpcImplementations + && null != rpcImplementations.get(rpcInterfaceId)) { + rpcImplementations.get(rpcInterfaceId).remove(implementation); + } + } + + public Collection getRpcImplementations( + String rpcInterfaceId) { + if (null == rpcImplementations) { + return Collections.emptyList(); + } + return (Collection) rpcImplementations.get(rpcInterfaceId); + } + } diff --git a/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java b/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java index 1ba3fd6930..bd27b8244d 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java +++ b/src/com/vaadin/terminal/gwt/client/ui/MediaBaseConnector.java @@ -6,13 +6,12 @@ package com.vaadin.terminal.gwt.client.ui; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.UIDL; import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.communication.ClientRpc; public abstract class MediaBaseConnector extends AbstractComponentConnector { public static final String TAG_SOURCE = "src"; - public static final String ATTR_PAUSE = "pause"; - public static final String ATTR_PLAY = "play"; public static final String ATTR_MUTED = "muted"; public static final String ATTR_CONTROLS = "ctrl"; public static final String ATTR_AUTOPLAY = "auto"; @@ -21,6 +20,38 @@ public abstract class MediaBaseConnector extends AbstractComponentConnector { public static final String ATTR_HTML = "html"; public static final String ATTR_ALT_TEXT = "alt"; + /** + * Server to client RPC interface for controlling playback of the media. + * + * @since 7.0 + */ + public static interface MediaControl extends ClientRpc { + /** + * Start playing the media. + */ + public void play(); + + /** + * Pause playback of the media. + */ + public void pause(); + } + + @Override + protected void init() { + super.init(); + + registerRpc(MediaControl.class, new MediaControl() { + public void play() { + getWidget().play(); + } + + public void pause() { + getWidget().pause(); + } + }); + } + @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { super.updateFromUIDL(uidl, client); @@ -41,9 +72,6 @@ public abstract class MediaBaseConnector extends AbstractComponentConnector { } } setAltText(uidl); - - evalPauseCommand(uidl); - evalPlayCommand(uidl); } protected boolean shouldShowControls(UIDL uidl) { @@ -62,18 +90,6 @@ public abstract class MediaBaseConnector extends AbstractComponentConnector { return uidl.getBooleanAttribute(ATTR_HTML); } - private void evalPlayCommand(UIDL uidl) { - if (uidl.hasAttribute(ATTR_PLAY)) { - getWidget().play(); - } - } - - private void evalPauseCommand(UIDL uidl) { - if (uidl.hasAttribute(ATTR_PAUSE)) { - getWidget().pause(); - } - } - @Override public VMediaBase getWidget() { return (VMediaBase) super.getWidget(); diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index c52b6fad6a..9143d61aca 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -807,6 +807,7 @@ public abstract class AbstractCommunicationManager implements } LinkedList stateQueue = new LinkedList(); + LinkedList rpcPendingQueue = new LinkedList(); if (paintables != null) { @@ -815,9 +816,11 @@ public abstract class AbstractCommunicationManager implements paintQueue.addAll(paintables); while (!paintQueue.isEmpty()) { - final Paintable p = paintQueue.pop(); + final Paintable p = paintQueue.removeFirst(); // for now, all painted components may need a state refresh stateQueue.push(p); + // ... and RPC calls to be sent + rpcPendingQueue.push(p); // // TODO CLEAN // if (p instanceof Root) { @@ -898,10 +901,50 @@ public abstract class AbstractCommunicationManager implements } outWriter.print("\"state\":"); outWriter.append(sharedStates.toString()); - outWriter.print(", "); // close changes + outWriter.print(", "); // close states } - // TODO send server to client RPC calls in call order here + // send server to client RPC calls for components in the root, in call + // order + if (!rpcPendingQueue.isEmpty()) { + // collect RPC calls from components in the root in the order in + // which they were performed, remove the calls from components + List pendingInvocations = collectPendingRpcCalls(rpcPendingQueue); + + JSONArray rpcCalls = new JSONArray(); + for (ClientMethodInvocation invocation : pendingInvocations) { + // add invocation to rpcCalls + try { + JSONArray invocationJson = new JSONArray(); + invocationJson + .put(getPaintableId(invocation.getPaintable())); + invocationJson.put(invocation.getInterfaceName()); + invocationJson.put(invocation.getMethodName()); + JSONArray paramJson = new JSONArray(); + for (int i = 0; i < invocation.getParameters().length; ++i) { + paramJson.put(JsonCodec.encode( + invocation.getParameters()[i], this)); + } + invocationJson.put(paramJson); + rpcCalls.put(invocationJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize RPC method call parameters for paintable " + + getPaintableId(invocation.getPaintable()) + + " method " + + invocation.getInterfaceName() + "." + + invocation.getMethodName() + ": " + + e.getMessage()); + } + + } + + if (rpcCalls.length() > 0) { + outWriter.print("\"rpc\" : "); + outWriter.append(rpcCalls.toString()); + outWriter.print(", "); // close rpc + } + } outWriter.print("\"meta\" : {"); boolean metaOpen = false; @@ -1050,6 +1093,45 @@ public abstract class AbstractCommunicationManager implements } } + /** + * Collects all pending RPC calls from listed {@link Paintable}s and clears + * their RPC queues. + * + * @param rpcPendingQueue + * list of {@link Paintable} of interest + * @return ordered list of pending RPC calls + */ + private List collectPendingRpcCalls( + LinkedList rpcPendingQueue) { + List pendingInvocations = new ArrayList(); + for (Paintable paintable : rpcPendingQueue) { + List paintablePendingRpc = paintable + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList( + totalCalls); + + // merge two ordered comparable lists + for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { + if (paintableIndex >= paintablePendingRpc.size() + || (oldIndex < oldPendingRpc.size() && ((Comparable) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } + protected abstract InputStream getThemeResourceAsStream(Root root, String themeName, String resource); @@ -1279,7 +1361,7 @@ public abstract class AbstractCommunicationManager implements } final VariableOwner owner = getVariableOwner(invocation - .getPaintableId()); + .getConnectorId()); if (owner != null && owner.isEnabled()) { VariableChange change = new VariableChange(invocation); @@ -1290,8 +1372,8 @@ public abstract class AbstractCommunicationManager implements Map m = new HashMap(); m.put(change.getName(), change.getValue()); while (nextInvocation != null - && invocation.getPaintableId().equals( - nextInvocation.getPaintableId()) + && invocation.getConnectorId().equals( + nextInvocation.getConnectorId()) && ApplicationConnection.UPDATE_VARIABLE_METHOD .equals(nextInvocation.getMethodName())) { i++; @@ -1343,7 +1425,7 @@ public abstract class AbstractCommunicationManager implements } } else { msg += "non-existent component, VAR_PID=" - + invocation.getPaintableId(); + + invocation.getConnectorId(); // TODO should this cause the message to be ignored? success = false; } @@ -1369,13 +1451,13 @@ public abstract class AbstractCommunicationManager implements * @param invocation */ protected void applyInvocation(MethodInvocation invocation) { - final RpcTarget target = getRpcTarget(invocation.getPaintableId()); + final RpcTarget target = getRpcTarget(invocation.getConnectorId()); if (null != target) { ServerRpcManager.applyInvocation(target, invocation); } else { // TODO better exception? throw new RuntimeException("No RPC target for paintable " - + invocation.getPaintableId()); + + invocation.getConnectorId()); } } @@ -1397,7 +1479,7 @@ public abstract class AbstractCommunicationManager implements // parse JSON to MethodInvocations for (int i = 0; i < invocationsJson.length(); ++i) { JSONArray invocationJson = invocationsJson.getJSONArray(i); - String paintableId = invocationJson.getString(0); + String connectorId = invocationJson.getString(0); String interfaceName = invocationJson.getString(1); String methodName = invocationJson.getString(2); JSONArray parametersJson = invocationJson.getJSONArray(3); @@ -1406,7 +1488,7 @@ public abstract class AbstractCommunicationManager implements parameters[j] = JsonCodec.convertVariableValue( parametersJson.getJSONArray(j), this); } - MethodInvocation invocation = new MethodInvocation(paintableId, + MethodInvocation invocation = new MethodInvocation(connectorId, interfaceName, methodName, parameters); invocations.add(invocation); } diff --git a/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java b/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java new file mode 100644 index 0000000000..8133a617ca --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java @@ -0,0 +1,64 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.Serializable; + +import com.vaadin.terminal.Paintable; + +/** + * Internal class for keeping track of pending server to client method + * invocations for a Paintable. + * + * @since 7.0 + */ +public class ClientMethodInvocation implements Serializable, + Comparable { + private final Paintable paintable; + private final String interfaceName; + private final String methodName; + private final Object[] parameters; + + // used for sorting calls between different Paintables in the same Root + private final long sequenceNumber; + // TODO may cause problems when clustering etc. + private static long counter = 0; + + public ClientMethodInvocation(Paintable paintable, String interfaceName, + String methodName, Object[] parameters) { + this.paintable = paintable; + this.interfaceName = interfaceName; + this.methodName = methodName; + this.parameters = (null != parameters) ? parameters : new Object[0]; + sequenceNumber = ++counter; + } + + public Paintable getPaintable() { + return paintable; + } + + public String getInterfaceName() { + return interfaceName; + } + + public String getMethodName() { + return methodName; + } + + public Object[] getParameters() { + return parameters; + } + + protected long getSequenceNumber() { + return sequenceNumber; + } + + public int compareTo(ClientMethodInvocation o) { + if (null == o) { + return 0; + } + return Long.signum(getSequenceNumber() - o.getSequenceNumber()); + } +} \ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/widgetsetutils/RpcManagerGenerator.java b/src/com/vaadin/terminal/gwt/widgetsetutils/RpcManagerGenerator.java new file mode 100644 index 0000000000..844b602b78 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/widgetsetutils/RpcManagerGenerator.java @@ -0,0 +1,197 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.widgetsetutils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.google.gwt.core.ext.Generator; +import com.google.gwt.core.ext.GeneratorContext; +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JClassType; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.core.ext.typeinfo.JType; +import com.google.gwt.core.ext.typeinfo.TypeOracle; +import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; +import com.google.gwt.user.rebind.SourceWriter; +import com.vaadin.terminal.gwt.client.Connector; +import com.vaadin.terminal.gwt.client.ConnectorMap; +import com.vaadin.terminal.gwt.client.communication.ClientRpc; +import com.vaadin.terminal.gwt.client.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.communication.RpcManager; + +/** + * GWT generator that creates an implementation for {@link RpcManager} on the + * client side classes for executing RPC calls received from the the server. + * + * @since 7.0 + */ +public class RpcManagerGenerator extends Generator { + + @Override + public String generate(TreeLogger logger, GeneratorContext context, + String typeName) throws UnableToCompleteException { + + String packageName = null; + String className = null; + try { + TypeOracle typeOracle = context.getTypeOracle(); + + // get classType and save instance variables + JClassType classType = typeOracle.getType(typeName); + packageName = classType.getPackage().getName(); + className = classType.getSimpleSourceName() + "Impl"; + // Generate class source code for SerializerMapImpl + generateClass(logger, context, packageName, className); + } catch (Exception e) { + logger.log(TreeLogger.ERROR, + "SerializerMapGenerator creation failed", e); + } + // return the fully qualifed name of the class generated + return packageName + "." + className; + } + + /** + * Generate source code for RpcManagerImpl + * + * @param logger + * Logger object + * @param context + * Generator context + * @param packageName + * package name for the class to generate + * @param className + * class name for the class to generate + */ + private void generateClass(TreeLogger logger, GeneratorContext context, + String packageName, String className) { + // get print writer that receives the source code + PrintWriter printWriter = null; + printWriter = context.tryCreate(logger, packageName, className); + // print writer if null, source code has ALREADY been generated + if (printWriter == null) { + return; + } + logger.log(Type.INFO, + "Detecting server to client RPC interface types..."); + Date date = new Date(); + TypeOracle typeOracle = context.getTypeOracle(); + JClassType serverToClientRpcType = typeOracle.findType(ClientRpc.class + .getName()); + JClassType[] rpcInterfaceSubtypes = serverToClientRpcType.getSubtypes(); + + // init composer, set class properties, create source writer + ClassSourceFileComposerFactory composer = null; + composer = new ClassSourceFileComposerFactory(packageName, className); + composer.addImport("com.google.gwt.core.client.GWT"); + composer.addImplementedInterface(RpcManager.class.getName()); + SourceWriter sourceWriter = composer.createSourceWriter(context, + printWriter); + sourceWriter.indent(); + + List rpcInterfaces = new ArrayList(); + + // iterate over RPC interfaces and create helper methods for each + // interface + for (JClassType type : rpcInterfaceSubtypes) { + if (null == type.isInterface()) { + // only interested in interfaces here, not implementations + continue; + } + rpcInterfaces.add(type); + // generate method to call methods of an RPC interface + sourceWriter.println("private void " + getInvokeMethodName(type) + + "(" + MethodInvocation.class.getName() + " invocation, " + + ConnectorMap.class.getName() + " connectorMap) {"); + sourceWriter.indent(); + + // loop over the methods of the interface and its superinterfaces + // methods + for (JClassType currentType : type.getFlattenedSupertypeHierarchy()) { + for (JMethod method : currentType.getMethods()) { + sourceWriter.println("if (\"" + method.getName() + + "\".equals(invocation.getMethodName())) {"); + sourceWriter.indent(); + // construct parameter string with appropriate casts + String paramString = ""; + JType[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; ++i) { + paramString = paramString + "(" + + parameterTypes[i].getQualifiedSourceName() + + ") invocation.getParameters()[i]"; + if (i < parameterTypes.length - 1) { + paramString = paramString + ", "; + } + } + sourceWriter + .println(Connector.class.getName() + + " connector = connectorMap.getConnector(invocation.getConnectorId());"); + sourceWriter + .println("for (" + + ClientRpc.class.getName() + + " rpcImplementation : connector.getRpcImplementations(\"" + + type.getQualifiedSourceName() + "\")) {"); + sourceWriter.indent(); + sourceWriter.println("((" + type.getQualifiedSourceName() + + ") rpcImplementation)." + method.getName() + "(" + + paramString + ");"); + sourceWriter.outdent(); + sourceWriter.println("}"); + sourceWriter.println("return;"); + sourceWriter.outdent(); + sourceWriter.println("}"); + } + } + + sourceWriter.outdent(); + sourceWriter.println("}"); + + logger.log(Type.DEBUG, + "Constructed helper method for server to client RPC for " + + type.getName()); + } + + // generate top-level "switch-case" method to select the correct + // previously generated method based on the RPC interface + sourceWriter.println("public void applyInvocation(" + + MethodInvocation.class.getName() + " invocation, " + + ConnectorMap.class.getName() + " connectorMap) {"); + sourceWriter.indent(); + + for (JClassType type : rpcInterfaces) { + sourceWriter.println("if (\"" + type.getQualifiedSourceName() + + "\".equals(invocation.getInterfaceName())) {"); + sourceWriter.indent(); + sourceWriter.println(getInvokeMethodName(type) + + "(invocation, connectorMap);"); + sourceWriter.println("return;"); + sourceWriter.outdent(); + sourceWriter.println("}"); + + logger.log(Type.INFO, + "Configured server to client RPC for " + type.getName()); + } + sourceWriter.outdent(); + sourceWriter.println("}"); + + // close generated class + sourceWriter.outdent(); + sourceWriter.println("}"); + // commit generated class + context.commit(logger, printWriter); + logger.log(Type.INFO, + "Done. (" + (new Date().getTime() - date.getTime()) / 1000 + + "seconds)"); + + } + + private String getInvokeMethodName(JClassType type) { + return "invoke" + type.getQualifiedSourceName().replaceAll("\\.", "_"); + } +} diff --git a/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java b/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java index 2d5e2b704f..778ced783b 100644 --- a/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java +++ b/src/com/vaadin/terminal/gwt/widgetsetutils/SerializerMapGenerator.java @@ -66,7 +66,7 @@ public class SerializerMapGenerator extends Generator { } /** - * Generate source code for WidgetMapImpl + * Generate source code for SerializerMapImpl * * @param typesNeedingSerializers * @@ -80,8 +80,7 @@ public class SerializerMapGenerator extends Generator { // get print writer that receives the source code PrintWriter printWriter = null; printWriter = context.tryCreate(logger, packageName, className); - // print writer if null, source code has ALREADY been generated, - // return (WidgetMap is equal to all permutations atm) + // print writer if null, source code has ALREADY been generated if (printWriter == null) { return; } diff --git a/src/com/vaadin/ui/AbstractComponent.java b/src/com/vaadin/ui/AbstractComponent.java index 1757c6a7d9..40e4771aa4 100644 --- a/src/com/vaadin/ui/AbstractComponent.java +++ b/src/com/vaadin/ui/AbstractComponent.java @@ -5,7 +5,9 @@ package com.vaadin.ui; import java.io.Serializable; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -13,6 +15,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -31,7 +34,9 @@ import com.vaadin.terminal.PaintTarget.PaintStatus; import com.vaadin.terminal.Resource; import com.vaadin.terminal.Terminal; import com.vaadin.terminal.gwt.client.ComponentState; +import com.vaadin.terminal.gwt.client.communication.ClientRpc; import com.vaadin.terminal.gwt.client.ui.AbstractComponentConnector; +import com.vaadin.terminal.gwt.server.ClientMethodInvocation; import com.vaadin.terminal.gwt.server.ComponentSizeValidator; import com.vaadin.terminal.gwt.server.RpcManager; import com.vaadin.terminal.gwt.server.RpcTarget; @@ -161,17 +166,28 @@ public abstract class AbstractComponent implements Component, MethodEventSource private ActionManager actionManager; /** - * A map from RPC interface class to the RPC call manager that handles - * incoming RPC calls for that interface. + * A map from client to server RPC interface class to the RPC call manager + * that handles incoming RPC calls for that interface. */ private Map, RpcManager> rpcManagerMap = new HashMap, RpcManager>(); + /** + * A map from server to client RPC interface class to the RPC proxy that + * sends ourgoing RPC calls for that interface. + */ + private Map, ClientRpc> rpcProxyMap = new HashMap, ClientRpc>(); + /** * Shared state object to be communicated from the server to the client when * modified. */ private ComponentState sharedState; + /** + * Pending RPC method invocations to be sent. + */ + private ArrayList pendingInvocations = new ArrayList(); + /* Constructor */ /** @@ -1631,8 +1647,86 @@ public abstract class AbstractComponent implements Component, MethodEventSource } } + /** + * Returns an RPC proxy for a given server to client RPC interface for this + * component. + * + * TODO more javadoc, subclasses, ... + * + * @param rpcInterface + * RPC interface type + * + * @since 7.0 + */ + public T getRpcProxy(final Class rpcInterface) { + // create, initialize and return a dynamic proxy for RPC + try { + if (!rpcProxyMap.containsKey(rpcInterface)) { + InvocationHandler handler = new InvocationHandler() { + public Object invoke(Object proxy, Method method, + Object[] args) throws Throwable { + addMethodInvocationToQueue(rpcInterface.getName() + .replaceAll("\\$", "."), method.getName(), args); + // TODO no need to do full repaint if only RPC calls + requestRepaint(); + return null; + } + }; + Class proxyClass = Proxy.getProxyClass( + rpcInterface.getClassLoader(), + new Class[] { rpcInterface }); + T rpcProxy = (T) proxyClass.getConstructor( + new Class[] { InvocationHandler.class }).newInstance( + new Object[] { handler }); + // cache the proxy + rpcProxyMap.put(rpcInterface, rpcProxy); + } + return (T) rpcProxyMap.get(rpcInterface); + } catch (Exception e) { + // TODO exception handling? + throw new RuntimeException(e); + } + } + + /** + * For internal use: adds a method invocation to the pending RPC call queue. + * + * @param interfaceName + * RPC interface name + * @param methodName + * RPC method name + * @param parameters + * RPC vall parameters + * + * @since 7.0 + */ + protected void addMethodInvocationToQueue(String interfaceName, + String methodName, Object[] parameters) { + // add to queue + pendingInvocations.add(new ClientMethodInvocation(this, interfaceName, + methodName, parameters)); + } + + /** + * @see RpcTarget#getRpcManager(Class) + * + * @param rpcInterface + * RPC interface for which a call was made + * @return RPC Manager handling calls for the interface + * + * @since 7.0 + */ public RpcManager getRpcManager(Class rpcInterface) { return rpcManagerMap.get(rpcInterface); } + public List retrievePendingRpcCalls() { + if (pendingInvocations.isEmpty()) { + return Collections.emptyList(); + } else { + List result = pendingInvocations; + pendingInvocations = new ArrayList(); + return Collections.unmodifiableList(result); + } + } } diff --git a/src/com/vaadin/ui/AbstractMedia.java b/src/com/vaadin/ui/AbstractMedia.java index 3c02d73858..e164b087f3 100644 --- a/src/com/vaadin/ui/AbstractMedia.java +++ b/src/com/vaadin/ui/AbstractMedia.java @@ -13,6 +13,7 @@ import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Resource; import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector; +import com.vaadin.terminal.gwt.client.ui.MediaBaseConnector.MediaControl; /** * Abstract base class for the HTML5 media components. @@ -33,10 +34,6 @@ public class AbstractMedia extends AbstractComponent { private boolean muted; - private boolean pause; - - private boolean play; - /** * Sets a single media file as the source of the media component. * @@ -182,22 +179,14 @@ public class AbstractMedia extends AbstractComponent { * Pauses the media. */ public void pause() { - // cancel any possible play command - play = false; - - pause = true; - requestRepaint(); + getRpcProxy(MediaControl.class).pause(); } /** * Starts playback of the media. */ public void play() { - // cancel any possible pause command. - pause = false; - - play = true; - requestRepaint(); + getRpcProxy(MediaControl.class).play(); } @Override @@ -218,13 +207,5 @@ public class AbstractMedia extends AbstractComponent { target.endTag(MediaBaseConnector.TAG_SOURCE); } target.addAttribute(MediaBaseConnector.ATTR_MUTED, isMuted()); - if (play) { - target.addAttribute(MediaBaseConnector.ATTR_PLAY, true); - play = false; - } - if (pause) { - target.addAttribute(MediaBaseConnector.ATTR_PAUSE, true); - pause = false; - } } }