diff options
author | Johannes Dahlström <johannesd@vaadin.com> | 2013-02-27 14:33:04 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2013-04-04 12:46:42 +0000 |
commit | 69def694d5d98f518ad08c039195fd2ac8781d2f (patch) | |
tree | 8ec221cf013607180bf08b65ea189d44cd9dda49 /server | |
parent | 008d51dba378c2feb57bd5d30550561567f3f91a (diff) | |
download | vaadin-framework-69def694d5d98f518ad08c039195fd2ac8781d2f.tar.gz vaadin-framework-69def694d5d98f518ad08c039195fd2ac8781d2f.zip |
Server push (#111)
* Asynchronous bidirectional communication
* Use Atmosphere as a backend
* Use websockets if available, fallback to HTTP streaming
* Push mode (disabled, manual, automatic)
* Configurable via servlet parameter pushMode
* Disabled: The default; regular AJAX communication
* Manual: Need explicit UI.push() call
* Automatic: push all UIs in session when lock released
* UI.push()
* Push pending state and RPC to client asynchronously
* Must hold session lock when invoking
Change-Id: Idb5978ac81f7ff1e66665df4e3f96e29e4c419d4
Diffstat (limited to 'server')
17 files changed, 634 insertions, 23 deletions
diff --git a/server/ivy.xml b/server/ivy.xml index d757e3a3cd..09e34fc075 100644 --- a/server/ivy.xml +++ b/server/ivy.xml @@ -49,6 +49,10 @@ <!-- Jsoup for BootstrapHandler --> <dependency org="org.jsoup" name="jsoup" rev="1.6.3" conf="build,ide,test -> default" /> + + <!-- Atmosphere --> + <dependency org="org.atmosphere" name="atmosphere-runtime" rev="1.0.12" + conf="build,ide,test -> default" /> <!-- TESTING DEPENDENCIES --> diff --git a/server/src/com/vaadin/server/BootstrapHandler.java b/server/src/com/vaadin/server/BootstrapHandler.java index 671279219e..d4f1ad308a 100644 --- a/server/src/com/vaadin/server/BootstrapHandler.java +++ b/server/src/com/vaadin/server/BootstrapHandler.java @@ -41,6 +41,7 @@ import org.jsoup.parser.Tag; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Version; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; /** @@ -337,8 +338,8 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { VaadinRequest request = context.getRequest(); VaadinService vaadinService = request.getService(); - String staticFileLocation = vaadinService - .getStaticFileLocation(request); + String vaadinLocation = vaadinService.getStaticFileLocation(request) + + "/VAADIN/"; fragmentNodes .add(new Element(Tag.valueOf("iframe"), "") @@ -348,8 +349,17 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { "position:absolute;width:0;height:0;border:0;overflow:hidden") .attr("src", "javascript:false")); - String bootstrapLocation = staticFileLocation - + "/VAADIN/vaadinBootstrap.js"; + if (context.getSession().getPushMode() != PushMode.DISABLED) { + // Load client-side dependencies for push support + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript").attr("src", + vaadinLocation + "portal.min.js")); + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript").attr("src", + vaadinLocation + "atmosphere.min.js")); + } + + String bootstrapLocation = vaadinLocation + "vaadinBootstrap.js"; fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type", "text/javascript").attr("src", bootstrapLocation)); Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr( @@ -477,6 +487,8 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { appConfig.put("heartbeatInterval", vaadinService .getDeploymentConfiguration().getHeartbeatInterval()); + appConfig.put("pushMode", session.getPushMode().toString()); + String serviceUrl = getServiceUrl(context); if (serviceUrl != null) { appConfig.put(ApplicationConstants.SERVICE_URL, serviceUrl); diff --git a/server/src/com/vaadin/server/Constants.java b/server/src/com/vaadin/server/Constants.java index a9bc3e5b9e..d0f8507c94 100644 --- a/server/src/com/vaadin/server/Constants.java +++ b/server/src/com/vaadin/server/Constants.java @@ -47,6 +47,13 @@ public interface Constants { + "in web.xml. The default of 5min will be used.\n" + "==========================================================="; + static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = "\n" + + "===========================================================\n" + + "WARNING: pushMode has been set to an unrecognized value\n" + + "in web.xml. The permitted values are \"disabled\", \"manual\",\n" + + "and \"automatic\". The default of \"disabled\" will be used.\n" + + "==========================================================="; + static final String WIDGETSET_MISMATCH_INFO = "\n" + "=================================================================\n" + "The widgetset in use does not seem to be built for the Vaadin\n" @@ -63,6 +70,7 @@ public interface Constants { static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime"; static final String SERVLET_PARAMETER_HEARTBEAT_INTERVAL = "heartbeatInterval"; static final String SERVLET_PARAMETER_CLOSE_IDLE_SESSIONS = "closeIdleSessions"; + static final String SERVLET_PARAMETER_PUSH_MODE = "pushMode"; static final String SERVLET_PARAMETER_UI_PROVIDER = "UIProvider"; // Configurable parameter names diff --git a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java index 5b0c3fe8d1..d11bd69997 100644 --- a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java @@ -19,6 +19,8 @@ package com.vaadin.server; import java.util.Properties; import java.util.logging.Logger; +import com.vaadin.shared.communication.PushMode; + /** * The default implementation of {@link DeploymentConfiguration} based on a base * class for resolving system properties and a set of init parameters. @@ -33,6 +35,7 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { private int resourceCacheTime; private int heartbeatInterval; private boolean closeIdleSessions; + private PushMode pushMode; private final Class<?> systemPropertyBaseClass; /** @@ -55,6 +58,7 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { checkResourceCacheTime(); checkHeartbeatInterval(); checkCloseIdleSessions(); + checkPushMode(); } @Override @@ -167,12 +171,32 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { return heartbeatInterval; } + /** + * {@inheritDoc} + * <p> + * The default value is false. + */ @Override public boolean isCloseIdleSessions() { return closeIdleSessions; } /** + * {@inheritDoc} + * <p> + * The default mode is {@link PushMode#DISABLED}. + */ + @Override + public PushMode getPushMode() { + return pushMode; + } + + @Override + public Properties getInitParameters() { + return initParameters; + } + + /** * Log a warning if Vaadin is not running in production mode. */ private void checkProductionMode() { @@ -231,13 +255,19 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { .equals("true"); } - private Logger getLogger() { - return Logger.getLogger(getClass().getName()); + private void checkPushMode() { + String mode = getApplicationOrSystemProperty( + Constants.SERVLET_PARAMETER_PUSH_MODE, + PushMode.DISABLED.toString()); + try { + pushMode = Enum.valueOf(PushMode.class, mode.toUpperCase()); + } catch (IllegalArgumentException e) { + getLogger().warning(Constants.WARNING_PUSH_MODE_NOT_RECOGNIZED); + pushMode = PushMode.DISABLED; + } } - @Override - public Properties getInitParameters() { - return initParameters; + private Logger getLogger() { + return Logger.getLogger(getClass().getName()); } - } diff --git a/server/src/com/vaadin/server/DeploymentConfiguration.java b/server/src/com/vaadin/server/DeploymentConfiguration.java index bd4bc928f4..23edf8052a 100644 --- a/server/src/com/vaadin/server/DeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DeploymentConfiguration.java @@ -19,6 +19,8 @@ package com.vaadin.server; import java.io.Serializable; import java.util.Properties; +import com.vaadin.shared.communication.PushMode; + /** * A collection of properties configured at deploy time as well as a way of * accessing third party properties not explicitly supported by this class. @@ -78,6 +80,14 @@ public interface DeploymentConfiguration extends Serializable { public boolean isCloseIdleSessions(); /** + * Returns the mode of bidirectional ("push") client-server communication + * that should be used. + * + * @return The push mode in use. + */ + public PushMode getPushMode(); + + /** * Gets the properties configured for the deployment, e.g. as init * parameters to the servlet or portlet. * diff --git a/server/src/com/vaadin/server/ServletPortletHelper.java b/server/src/com/vaadin/server/ServletPortletHelper.java index baf697cae3..c14467a10e 100644 --- a/server/src/com/vaadin/server/ServletPortletHelper.java +++ b/server/src/com/vaadin/server/ServletPortletHelper.java @@ -123,6 +123,10 @@ public class ServletPortletHelper implements Serializable { return hasPathPrefix(request, ApplicationConstants.HEARTBEAT_PATH + '/'); } + public static boolean isPushRequest(VaadinRequest request) { + return hasPathPrefix(request, ApplicationConstants.PUSH_PATH + '/'); + } + public static void initDefaultUIProvider(VaadinSession session, VaadinService vaadinService) throws ServiceException { String uiProperty = vaadinService.getDeploymentConfiguration() @@ -191,7 +195,7 @@ public class ServletPortletHelper implements Serializable { * <li>{@link Locale#getDefault()}</li> * </ol> */ - static Locale findLocale(Component component, VaadinSession session, + public static Locale findLocale(Component component, VaadinSession session, VaadinRequest request) { if (component == null) { component = UI.getCurrent(); @@ -225,5 +229,4 @@ public class ServletPortletHelper implements Serializable { return Locale.getDefault(); } - } diff --git a/server/src/com/vaadin/server/VaadinService.java b/server/src/com/vaadin/server/VaadinService.java index 3b088294e3..ceabaaf729 100644 --- a/server/src/com/vaadin/server/VaadinService.java +++ b/server/src/com/vaadin/server/VaadinService.java @@ -650,6 +650,7 @@ public abstract class VaadinService implements Serializable, Callback { session.setLocale(locale); session.setConfiguration(getDeploymentConfiguration()); session.setCommunicationManager(new LegacyCommunicationManager(session)); + session.setPushMode(getDeploymentConfiguration().getPushMode()); ServletPortletHelper.initDefaultUIProvider(session, this); onVaadinSessionStarted(request, session); diff --git a/server/src/com/vaadin/server/VaadinServletService.java b/server/src/com/vaadin/server/VaadinServletService.java index a12e2b47e2..ba78efa9bb 100644 --- a/server/src/com/vaadin/server/VaadinServletService.java +++ b/server/src/com/vaadin/server/VaadinServletService.java @@ -28,8 +28,10 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.vaadin.server.communication.PushRequestHandler; import com.vaadin.server.communication.ServletBootstrapHandler; import com.vaadin.server.communication.ServletUIInitHandler; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; public class VaadinServletService extends VaadinService { @@ -73,6 +75,9 @@ public class VaadinServletService extends VaadinService { List<RequestHandler> handlers = super.createRequestHandlers(); handlers.add(0, new ServletBootstrapHandler()); handlers.add(new ServletUIInitHandler()); + if (getDeploymentConfiguration().getPushMode() != PushMode.DISABLED) { + handlers.add(new PushRequestHandler(this)); + } return handlers; } diff --git a/server/src/com/vaadin/server/VaadinSession.java b/server/src/com/vaadin/server/VaadinSession.java index 844b7ff674..029a384e70 100644 --- a/server/src/com/vaadin/server/VaadinSession.java +++ b/server/src/com/vaadin/server/VaadinSession.java @@ -39,6 +39,7 @@ import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterFactory; import com.vaadin.data.util.converter.DefaultConverterFactory; import com.vaadin.event.EventRouter; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.AbstractField; import com.vaadin.ui.Table; import com.vaadin.ui.UI; @@ -128,6 +129,8 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { private transient Lock lock; + private PushMode pushMode; + /** * Create a new service session tied to a Vaadin service * @@ -806,11 +809,28 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { /** * Unlocks this session. This method should always be used in a finally * block after {@link #lock()} to ensure that the lock is always released. + * <p> + * If {@link #getPushMode() the push mode} is {@link PushMode#AUTOMATIC + * automatic}, pushes the changes in all UIs in this session to their + * respective clients. * - * @see #unlock() + * @see #lock() + * @see UI#push() */ public void unlock() { - getLockInstance().unlock(); + assert hasLock(); + try { + if (getPushMode() == PushMode.AUTOMATIC + && ((ReentrantLock) getLockInstance()).getHoldCount() == 1) { + // Only push if the reentrant lock will actually be released by + // this unlock() invocation. + for (UI ui : getUIs()) { + ui.push(); + } + } + } finally { + getLockInstance().unlock(); + } } /** @@ -1005,6 +1025,39 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } /** + * Returns the mode of bidirectional ("push") communication that is used in + * this session. + * + * @return The push mode. + */ + public PushMode getPushMode() { + return pushMode; + } + + /** + * Sets the mode of bidirectional ("push") communication that should be used + * in this session. Set once on session creation and cannot be changed + * afterwards. + * + * @param pushMode + * The push mode to use. + * + * @throws IllegalArgumentException + * if the argument is null. + * @throws IllegalStateException + * if the mode is already set. + */ + public void setPushMode(PushMode pushMode) { + if (pushMode == null) { + throw new IllegalArgumentException("Push mode cannot be null"); + } + if (this.pushMode != null) { + throw new IllegalStateException("Push mode already set"); + } + this.pushMode = pushMode; + } + + /** * Sets this session to be closed and all UI state to be discarded at the * end of the current request, or at the end of the next request if there is * no ongoing one. diff --git a/server/src/com/vaadin/server/communication/MetadataWriter.java b/server/src/com/vaadin/server/communication/MetadataWriter.java index 7119e0ffeb..1a3f0e946a 100644 --- a/server/src/com/vaadin/server/communication/MetadataWriter.java +++ b/server/src/com/vaadin/server/communication/MetadataWriter.java @@ -51,6 +51,9 @@ public class MetadataWriter implements Serializable { * @param analyzeLayouts * Whether detected layout problems should be reported in client * and server console. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. * @param hilightedConnector * The connector that should be highlighted on the client or null * if none. @@ -62,8 +65,9 @@ public class MetadataWriter implements Serializable { * */ public void write(UI ui, Writer writer, boolean repaintAll, - boolean analyzeLayouts, ClientConnector hilightedConnector, - SystemMessages messages) throws IOException { + boolean analyzeLayouts, boolean async, + ClientConnector hilightedConnector, SystemMessages messages) + throws IOException { List<InvalidLayout> invalidComponentRelativeSizes = null; @@ -112,6 +116,13 @@ public class MetadataWriter implements Serializable { } } + if (async) { + if (metaOpen) { + writer.write(", "); + } + writer.write("\"async\":true"); + } + // meta instruction for client to enable auto-forward to // sessionExpiredURL after timer expires. if (messages != null && messages.getSessionExpiredMessage() == null diff --git a/server/src/com/vaadin/server/communication/PushConnection.java b/server/src/com/vaadin/server/communication/PushConnection.java new file mode 100644 index 0000000000..2db9d42763 --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushConnection.java @@ -0,0 +1,140 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; + +import org.atmosphere.cpr.AtmosphereResource; +import org.json.JSONException; + +import com.vaadin.ui.UI; + +/** + * Represents a bidirectional ("push") connection between a single UI and its + * client-side. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushConnection implements Serializable { + + private UI ui; + private boolean pending = true; + private AtmosphereResource resource; + + public PushConnection(UI ui) { + this.ui = ui; + } + + /** + * Pushes pending state changes and client RPC calls to the client. It is + * NOT safe to invoke this method if not holding the session lock. + * <p> + * This is internal API; please use {@link UI#push()} instead. + */ + public void push() { + if (!isConnected()) { + // Not currently connected; defer until connection established + setPending(true); + } else { + try { + push(true); + } catch (IOException e) { + // TODO Error handling + throw new RuntimeException("Push failed", e); + } + } + } + + /** + * Pushes pending state changes and client RPC calls to the client. + * + * @param async + * True if this push asynchronously originates from the server, + * false if it is a response to a client request. + * @throws IOException + */ + protected void push(boolean async) throws IOException { + Writer writer = new StringWriter(); + try { + new UidlWriter().write(getUI(), writer, false, false, async); + } catch (JSONException e) { + throw new IOException("Error writing UIDL", e); + } + // "Broadcast" the changes to the single client only + getResource().getBroadcaster().broadcast(writer.toString(), + getResource()); + } + + /** + * Associates this connection with the given AtmosphereResource. If there is + * a push pending, commits it. + * + * @param resource + * The AtmosphereResource representing the push channel. + * @throws IOException + */ + protected void connect(AtmosphereResource resource) throws IOException { + this.resource = resource; + if (isPending()) { + push(true); + setPending(false); + } + } + + /** + * Returns whether this connection is currently open. + */ + protected boolean isConnected() { + return resource != null + && resource.getBroadcaster().getAtmosphereResources() + .contains(resource); + } + + /** + * Marks that changes in the UI should be pushed as soon as a connection is + * established. + */ + protected void setPending(boolean pending) { + this.pending = pending; + } + + /** + * @return Whether the UI should be pushed as soon as a connection opens. + */ + protected boolean isPending() { + return pending; + } + + /** + * @return the UI associated with this connection. + */ + protected UI getUI() { + return ui; + } + + /** + * @return The AtmosphereResource associated with this connection or null if + * connection not open. + */ + protected AtmosphereResource getResource() { + return resource; + } +} diff --git a/server/src/com/vaadin/server/communication/PushHandler.java b/server/src/com/vaadin/server/communication/PushHandler.java new file mode 100644 index 0000000000..39481db46a --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -0,0 +1,184 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Writer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.atmosphere.cpr.AtmosphereHandler; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResource; +import org.atmosphere.cpr.AtmosphereResourceEvent; +import org.json.JSONException; + +import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ServiceException; +import com.vaadin.server.SessionExpiredException; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; + +/** + * Establishes bidirectional ("push") communication channels + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushHandler implements AtmosphereHandler { + + private VaadinService service; + + public PushHandler(VaadinService service) { + this.service = service; + } + + @Override + public void onRequest(AtmosphereResource resource) { + + AtmosphereRequest req = resource.getRequest(); + VaadinRequest vaadinRequest = getVaadinRequest(req); + + VaadinSession session; + try { + session = service.findVaadinSession(vaadinRequest); + } catch (ServiceException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return; + } catch (SessionExpiredException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return; + } + + session.lock(); + try { + UI ui = service.findUI(vaadinRequest); + if (ui == null) { + throw new RuntimeException("UI not found!"); + } + PushConnection connection = ui.getPushConnection(); + + if (req.getMethod().equalsIgnoreCase("GET")) { + /* + * We received a request to establish a push channel for a UI. + * Associate the AtmosphereResource with the UI and leave the + * connection open by calling resource.suspend(). If there is a + * pending push, send it now. + */ + getLogger().log(Level.FINER, + "New push connection with transport {}", + resource.transport()); + resource.suspend(); + + connection.connect(resource); + } else if (req.getMethod().equalsIgnoreCase("POST")) { + /* + * We received a UIDL request through Atmosphere. If the push + * channel is bidirectional (websockets), the request was sent + * via the same channel. Otherwise, the client used a separate + * AJAX request. Handle the request and send changed UI state + * via the push channel (we do not respond to the request + * directly.) + */ + new ServerRpcHandler().handleRpc(ui, req.getReader(), + vaadinRequest); + connection.push(false); + } + } catch (InvalidUIDLSecurityKeyException e) { + // TODO Error handling + e.printStackTrace(); + } catch (JSONException e) { + // TODO Error handling + e.printStackTrace(); + } catch (IOException e) { + // TODO Error handling + e.printStackTrace(); + } finally { + session.unlock(); + } + } + + @Override + public void onStateChange(AtmosphereResourceEvent event) throws IOException { + AtmosphereResource resource = event.getResource(); + + String id = resource.uuid(); + if (event.isCancelled()) { + // The client closed the connection. + // TODO Do some cleanup + getLogger().log(Level.FINER, "Connection closed for resource {}", + id); + } else if (event.isResuming()) { + // A connection that was suspended earlier was resumed (committed to + // the client.) Should only happen if the transport is JSONP or + // long-polling. + getLogger() + .log(Level.FINER, "Resuming request for resource {}", id); + } else { + // A message was broadcast to this resource and should be sent to + // the client. We don't do any actual broadcasting, in the sense of + // sending to multiple recipients; any UIDL message is specific to a + // single client. + getLogger().log(Level.FINER, "Writing message to resource {}", id); + + resource.getResponse().setContentType( + "application/json; charset=UTF-8"); + Writer writer = resource.getResponse().getWriter(); + writer.write("for(;;);[{" + event.getMessage() + "}]"); + + switch (resource.transport()) { + case SSE: + case WEBSOCKET: + break; + case STREAMING: + writer.flush(); + break; + case JSONP: + case LONG_POLLING: + resource.resume(); + break; + default: + getLogger().log(Level.SEVERE, "Unknown transport {}", + resource.transport()); + } + } + } + + @Override + public void destroy() { + } + + private VaadinRequest getVaadinRequest(AtmosphereRequest req) { + while (req.getRequest() instanceof AtmosphereRequest) { + req = (AtmosphereRequest) req.getRequest(); + } + if (req.getRequest() instanceof VaadinRequest) { + return (VaadinRequest) req.getRequest(); + } else { + throw new IllegalArgumentException( + "Request does not wrap VaadinRequest"); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(PushHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java new file mode 100644 index 0000000000..10ef16e11c --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.atmosphere.client.TrackMessageSizeInterceptor; +import org.atmosphere.cpr.AtmosphereFramework; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinServletRequest; +import com.vaadin.server.VaadinServletResponse; +import com.vaadin.server.VaadinSession; + +/** + * Handles requests to open a push (bidirectional) communication channel between + * the client and the server. After the initial request, communication through + * the push channel is managed by {@link PushHandler}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushRequestHandler implements RequestHandler { + + private AtmosphereFramework atmosphere; + private PushHandler pushHandler; + + public PushRequestHandler(VaadinService service) { + + atmosphere = new AtmosphereFramework(); + + pushHandler = new PushHandler(service); + atmosphere.addAtmosphereHandler("/*", pushHandler); + atmosphere + .addInitParameter("org.atmosphere.cpr.sessionSupport", "true"); + + // Required to ensure the client-side knows at which points to split the + // message stream into individual messages when using certain transports + atmosphere.interceptor(new TrackMessageSizeInterceptor()); + + atmosphere.init(); + } + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + if (!ServletPortletHelper.isPushRequest(request)) { + return false; + } + + if (request instanceof VaadinServletRequest) { + try { + atmosphere.doCometSupport(AtmosphereRequest + .wrap((VaadinServletRequest) request), + AtmosphereResponse + .wrap((VaadinServletResponse) response)); + } catch (ServletException e) { + // TODO PUSH decide how to handle + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException( + "Portlets not currently supported"); + } + + return true; + } + + public void destroy() { + atmosphere.destroy(); + } +} diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java index c3e7119d3f..8275ea3efd 100644 --- a/server/src/com/vaadin/server/communication/UIInitHandler.java +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -37,6 +37,7 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinSession; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.ui.UI; @@ -205,6 +206,10 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { // Set thread local here so it is available in init UI.setCurrent(ui); + if (session.getPushMode() != PushMode.DISABLED) { + ui.setPushConnection(new PushConnection(ui)); + } + ui.doInit(request, uiId.intValue()); session.addUI(ui); @@ -263,7 +268,7 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { writer.write(uI.getSession().getCommunicationManager() .getSecurityKeyUIDL(request)); } - new UidlWriter().write(uI, writer, true, false); + new UidlWriter().write(uI, writer, true, false, false); writer.write("}"); String initialUIDL = writer.toString(); diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java index 0de9029063..32f9df3eff 100644 --- a/server/src/com/vaadin/server/communication/UidlRequestHandler.java +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -169,7 +169,7 @@ public class UidlRequestHandler extends SynchronizedRequestHandler { .getSecurityKeyUIDL(request)); } - new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts); + new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts, false); closeJsonMessage(writer); } diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java index 81bbb91649..79ae8af07e 100644 --- a/server/src/com/vaadin/server/communication/UidlWriter.java +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -62,14 +62,18 @@ public class UidlWriter implements Serializable { * Whether the client should re-render the whole UI. * @param analyzeLayouts * Whether detected layout problems should be logged. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. + * * @throws IOException * If the writing fails. * @throws JSONException * If the JSON serialization fails. */ public void write(UI ui, Writer writer, boolean repaintAll, - boolean analyzeLayouts) throws IOException, JSONException { - + boolean analyzeLayouts, boolean async) throws IOException, + JSONException { ArrayList<ClientConnector> dirtyVisibleConnectors = ui .getConnectorTracker().getDirtyVisibleConnectors(); VaadinSession session = ui.getSession(); @@ -153,7 +157,7 @@ public class UidlWriter implements Serializable { .getSystemMessages(ui.getLocale(), null); // TODO hilightedConnector new MetadataWriter().write(ui, writer, repaintAll, analyzeLayouts, - null, messages); + async, null, messages); writer.write(", "); writer.write("\"resources\" : "); @@ -289,8 +293,6 @@ public class UidlWriter implements Serializable { assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; writePerformanceData(ui, writer); - } catch (IOException ex) { - throw new RuntimeException(ex); } finally { uiConnectorTracker.setWritingResponse(false); } diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index a20c2b2087..162d072222 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -37,8 +37,10 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinServlet; import com.vaadin.server.VaadinSession; +import com.vaadin.server.communication.PushConnection; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.ScrollClientRpc; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIServerRpc; @@ -470,6 +472,8 @@ public abstract class UI extends AbstractSingleComponentContainer implements private Navigator navigator; + private PushConnection pushConnection = new PushConnection(this); + /** * This method is used by Component.Focusable objects to request focus to * themselves. Focus renders must be handled at window level (instead of @@ -1118,4 +1122,48 @@ public abstract class UI extends AbstractSingleComponentContainer implements return loadingIndicator; } + /** + * Pushes the pending changes and client RPC invocations of this UI to the + * client-side. + * <p> + * As with all UI methods, it is not safe to call push() without holding the + * {@link VaadinSession#lock() session lock}. + * + * @throws IllegalStateException + * if push is disabled. + * @throws UIDetachedException + * if this UI is not attached to a session. + * + * @see VaadinSession#getPushMode() + * + * @since 7.1 + */ + public void push() { + VaadinSession session = getSession(); + if (session != null) { + if (session.getPushMode() == PushMode.DISABLED) { + throw new IllegalStateException("Push not enabled"); + } + assert pushConnection != null; + pushConnection.push(); + } else { + throw new UIDetachedException("Trying to push a detached UI"); + } + } + + /** + * Returns the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public PushConnection getPushConnection() { + return pushConnection; + } + + /** + * Sets the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public void setPushConnection(PushConnection connection) { + pushConnection = connection; + } } |