summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJohannes Dahlström <johannesd@vaadin.com>2013-02-27 14:33:04 +0200
committerVaadin Code Review <review@vaadin.com>2013-04-04 12:46:42 +0000
commit69def694d5d98f518ad08c039195fd2ac8781d2f (patch)
tree8ec221cf013607180bf08b65ea189d44cd9dda49 /server
parent008d51dba378c2feb57bd5d30550561567f3f91a (diff)
downloadvaadin-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')
-rw-r--r--server/ivy.xml4
-rw-r--r--server/src/com/vaadin/server/BootstrapHandler.java20
-rw-r--r--server/src/com/vaadin/server/Constants.java8
-rw-r--r--server/src/com/vaadin/server/DefaultDeploymentConfiguration.java42
-rw-r--r--server/src/com/vaadin/server/DeploymentConfiguration.java10
-rw-r--r--server/src/com/vaadin/server/ServletPortletHelper.java7
-rw-r--r--server/src/com/vaadin/server/VaadinService.java1
-rw-r--r--server/src/com/vaadin/server/VaadinServletService.java5
-rw-r--r--server/src/com/vaadin/server/VaadinSession.java57
-rw-r--r--server/src/com/vaadin/server/communication/MetadataWriter.java15
-rw-r--r--server/src/com/vaadin/server/communication/PushConnection.java140
-rw-r--r--server/src/com/vaadin/server/communication/PushHandler.java184
-rw-r--r--server/src/com/vaadin/server/communication/PushRequestHandler.java95
-rw-r--r--server/src/com/vaadin/server/communication/UIInitHandler.java7
-rw-r--r--server/src/com/vaadin/server/communication/UidlRequestHandler.java2
-rw-r--r--server/src/com/vaadin/server/communication/UidlWriter.java12
-rw-r--r--server/src/com/vaadin/ui/UI.java48
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;
+ }
}