summaryrefslogtreecommitdiffstats
path: root/compatibility-server-gae/src/main/java/com/vaadin
diff options
context:
space:
mode:
authorMirjan Merruko <mirjan@vaadin.com>2017-03-28 21:25:21 +0300
committerHenri Sara <henri.sara@gmail.com>2017-04-25 15:01:06 +0300
commite1b923f90623d7dcdbe355225a4049323dabacf7 (patch)
tree4e6eba97a44ab1c079ebca79a205cd0ef53d219d /compatibility-server-gae/src/main/java/com/vaadin
parent9a6ebeb5a80bdb1d97d47422c62a973b3f2d2cf9 (diff)
downloadvaadin-framework-e1b923f90623d7dcdbe355225a4049323dabacf7.tar.gz
vaadin-framework-e1b923f90623d7dcdbe355225a4049323dabacf7.zip
Move GAEVaadinServlet to its own module
Diffstat (limited to 'compatibility-server-gae/src/main/java/com/vaadin')
-rw-r--r--compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java469
1 files changed, 469 insertions, 0 deletions
diff --git a/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java b/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java
new file mode 100644
index 0000000000..5b25a48ba9
--- /dev/null
+++ b/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2000-2016 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;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import com.google.appengine.api.datastore.Blob;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.FetchOptions.Builder;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.memcache.Expiration;
+import com.google.appengine.api.memcache.MemcacheService;
+import com.google.appengine.api.memcache.MemcacheServiceFactory;
+import com.google.apphosting.api.DeadlineExceededException;
+
+/**
+ * ApplicationServlet to be used when deploying to Google App Engine, in
+ * web.xml:
+ *
+ * <pre>
+ * &lt;servlet&gt;
+ * &lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;
+ * &lt;servlet-class&gt;com.vaadin.server.GAEApplicationServlet&lt;/servlet-class&gt;
+ * &lt;init-param&gt;
+ * &lt;param-name&gt;UI&lt;/param-name&gt;
+ * &lt;param-value&gt;com.vaadin.demo.HelloWorld&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * &lt;/servlet&gt;
+ * </pre>
+ *
+ * Session support must be enabled in appengine-web.xml:
+ *
+ * <pre>
+ * &lt;sessions-enabled&gt;true&lt;/sessions-enabled&gt;
+ * </pre>
+ *
+ * Appengine datastore cleanup can be invoked by calling one of the applications
+ * with an additional path "/CLEAN". This can be set up as a cron-job in
+ * cron.xml (see appengine documentation for more information):
+ *
+ * <pre>
+ * &lt;cronentries&gt;
+ * &lt;cron&gt;
+ * &lt;url&gt;/HelloWorld/CLEAN&lt;/url&gt;
+ * &lt;description&gt;Clean up sessions&lt;/description&gt;
+ * &lt;schedule&gt;every 2 hours&lt;/schedule&gt;
+ * &lt;/cron&gt;
+ * &lt;/cronentries&gt;
+ * </pre>
+ *
+ * It is recommended (but not mandatory) to extract themes and widgetsets and
+ * have App Engine server these statically. Extract VAADIN folder (and it's
+ * contents) 'next to' the WEB-INF folder, and add the following to
+ * appengine-web.xml:
+ *
+ * <pre>
+ * &lt;static-files&gt;
+ * &lt;include path=&quot;/VAADIN/**&quot; /&gt;
+ * &lt;/static-files&gt;
+ * </pre>
+ *
+ * Additional limitations:
+ * <ul>
+ * <li/>Do not change application state when serving an ApplicationResource.
+ * <li/>Avoid changing application state in transaction handlers, unless you're
+ * confident you fully understand the synchronization issues in App Engine.
+ * <li/>The application remains locked while uploading - no progressbar is
+ * possible.
+ * </ul>
+ *
+ * @deprecated No longer supported with Vaadin 8.0
+ */
+@Deprecated
+public class GAEVaadinServlet extends VaadinServlet {
+
+ // memcache mutex is MUTEX_BASE + sessio id
+ private static final String MUTEX_BASE = "_vmutex";
+
+ // used identify ApplicationContext in memcache and datastore
+ private static final String AC_BASE = "_vac";
+
+ // UIDL requests will attempt to gain access for this long before telling
+ // the client to retry
+ private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000;
+
+ // Tell client to retry after this delay.
+ // Note: currently interpreting Retry-After as ms, not sec
+ private static final int RETRY_AFTER_MILLISECONDS = 100;
+
+ // Properties used in the datastore
+ private static final String PROPERTY_EXPIRES = "expires";
+ private static final String PROPERTY_DATA = "data";
+
+ // path used for cleanup
+ private static final String CLEANUP_PATH = "/CLEAN";
+ // max entities to clean at once
+ private static final int CLEANUP_LIMIT = 200;
+ // appengine session kind
+ private static final String APPENGINE_SESSION_KIND = "_ah_SESSION";
+ // appengine session expires-parameter
+ private static final String PROPERTY_APPENGINE_EXPIRES = "_expires";
+
+ // sessions with undefined (-1) expiration are limited to this, but explicit
+ // longer timeouts can be used
+ private static final int DEFAULT_MAX_INACTIVE_INTERVAL = 24 * 3600;
+
+ protected void sendDeadlineExceededNotification(
+ VaadinServletRequest request, VaadinServletResponse response)
+ throws IOException {
+ criticalNotification(request, response, "Deadline Exceeded",
+ "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...",
+ "", null);
+ }
+
+ protected void sendNotSerializableNotification(VaadinServletRequest request,
+ VaadinServletResponse response) throws IOException {
+ criticalNotification(request, response, "NotSerializableException",
+ "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...",
+ "",
+ getApplicationUrl(request).toString() + "?restartApplication");
+ }
+
+ protected void sendCriticalErrorNotification(VaadinServletRequest request,
+ VaadinServletResponse response) throws IOException {
+ criticalNotification(request, response, "Critical error",
+ "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...",
+ "",
+ getApplicationUrl(request).toString() + "?restartApplication");
+ }
+
+ @Override
+ protected void service(HttpServletRequest unwrappedRequest,
+ HttpServletResponse unwrappedResponse)
+ throws ServletException, IOException {
+ VaadinServletRequest request = new VaadinServletRequest(
+ unwrappedRequest, getService());
+ VaadinServletResponse response = new VaadinServletResponse(
+ unwrappedResponse, getService());
+
+ if (isCleanupRequest(request)) {
+ cleanDatastore();
+ return;
+ }
+
+ if (isStaticResourceRequest(request)) {
+ // no locking needed, let superclass handle
+ super.service(request, response);
+ cleanSession(request);
+ return;
+ }
+
+ if (ServletPortletHelper.isAppRequest(request)) {
+ // no locking needed, let superclass handle
+ getApplicationContext(request,
+ MemcacheServiceFactory.getMemcacheService());
+ super.service(request, response);
+ cleanSession(request);
+ return;
+ }
+
+ final HttpSession session = request
+ .getSession(getService().requestCanCreateSession(request));
+ if (session == null) {
+ try {
+ getService().handleSessionExpired(request, response);
+ } catch (ServiceException e) {
+ throw new ServletException(e);
+ }
+ cleanSession(request);
+ return;
+ }
+
+ boolean locked = false;
+ MemcacheService memcache = null;
+ String mutex = MUTEX_BASE + session.getId();
+ memcache = MemcacheServiceFactory.getMemcacheService();
+ try {
+ // try to get lock
+ long started = System.currentTimeMillis();
+ while (System.currentTimeMillis()
+ - started < MAX_UIDL_WAIT_MILLISECONDS) {
+ locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40),
+ MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
+ if (locked || ServletPortletHelper.isUIDLRequest(request)) {
+ /*
+ * Done if we got a lock. Will also avoid retrying if
+ * there's a UIDL request because those are retried from the
+ * client without keeping the server thread stalled.
+ */
+ break;
+ }
+ try {
+ Thread.sleep(RETRY_AFTER_MILLISECONDS);
+ } catch (InterruptedException e) {
+ getLogger()
+ .finer("Thread.sleep() interrupted while waiting for lock. Trying again. "
+ + e);
+ }
+ }
+
+ if (!locked) {
+ // Not locked; only UIDL can get trough here unlocked: tell
+ // client to retry
+ response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ // Note: currently interpreting Retry-After as ms, not sec
+ response.setHeader("Retry-After",
+ "" + RETRY_AFTER_MILLISECONDS);
+ return;
+ }
+
+ // de-serialize or create application context, store in session
+ VaadinSession ctx = getApplicationContext(request, memcache);
+
+ super.service(request, response);
+
+ // serialize
+ started = new Date().getTime();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(ctx);
+ oos.flush();
+ byte[] bytes = baos.toByteArray();
+
+ started = new Date().getTime();
+
+ String id = AC_BASE + session.getId();
+ Date expire = new Date(
+ started + getMaxInactiveIntervalSeconds(session) * 1000);
+ Expiration expires = Expiration.onDate(expire);
+
+ memcache.put(id, bytes, expires);
+
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ Entity entity = new Entity(AC_BASE, id);
+ entity.setProperty(PROPERTY_EXPIRES, expire.getTime());
+ entity.setProperty(PROPERTY_DATA, new Blob(bytes));
+ ds.put(entity);
+
+ } catch (DeadlineExceededException e) {
+ getLogger().log(Level.WARNING, "DeadlineExceeded for {0}",
+ session.getId());
+ sendDeadlineExceededNotification(request, response);
+ } catch (NotSerializableException e) {
+ getLogger().log(Level.SEVERE, "Not serializable!", e);
+
+ // TODO this notification is usually not shown - should we redirect
+ // in some other way - can we?
+ sendNotSerializableNotification(request, response);
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING,
+ "An exception occurred while servicing request.", e);
+
+ sendCriticalErrorNotification(request, response);
+ } finally {
+ // "Next, please!"
+ if (locked) {
+ memcache.delete(mutex);
+ }
+ cleanSession(request);
+ }
+ }
+
+ /**
+ * Returns the maximum inactive time for a session. This is used for
+ * handling the expiration of session related information in caches etc.
+ *
+ * @param session
+ * @return inactive timeout in seconds, greater than zero
+ */
+ protected int getMaxInactiveIntervalSeconds(final HttpSession session) {
+ int interval = session.getMaxInactiveInterval();
+ if (interval <= 0) {
+ getLogger().log(Level.FINE,
+ "Undefined session expiration time, using default value instead.");
+ return DEFAULT_MAX_INACTIVE_INTERVAL;
+ }
+ return interval;
+ }
+
+ protected VaadinSession getApplicationContext(HttpServletRequest request,
+ MemcacheService memcache) throws ServletException {
+ HttpSession session = request.getSession();
+ String id = AC_BASE + session.getId();
+ byte[] serializedAC = (byte[]) memcache.get(id);
+ if (serializedAC == null) {
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ Key key = KeyFactory.createKey(AC_BASE, id);
+ Entity entity = null;
+ try {
+ entity = ds.get(key);
+ } catch (EntityNotFoundException e) {
+ // Ok, we were a bit optimistic; we'll create a new one later
+ }
+ if (entity != null) {
+ Blob blob = (Blob) entity.getProperty(PROPERTY_DATA);
+ serializedAC = blob.getBytes();
+ // bring it to memcache
+ memcache.put(AC_BASE + session.getId(), serializedAC,
+ Expiration.byDeltaSeconds(
+ getMaxInactiveIntervalSeconds(session)),
+ MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
+ }
+ }
+ if (serializedAC != null) {
+ ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC);
+ ObjectInputStream ois;
+ try {
+ ois = new ObjectInputStream(bais);
+ VaadinSession vaadinSession = (VaadinSession) ois.readObject();
+ getService().storeSession(vaadinSession,
+ new WrappedHttpSession(session));
+ } catch (IOException e) {
+ getLogger().log(Level.WARNING,
+ "Could not de-serialize ApplicationContext for "
+ + session.getId()
+ + " A new one will be created. ",
+ e);
+ } catch (ClassNotFoundException e) {
+ getLogger().log(Level.WARNING,
+ "Could not de-serialize ApplicationContext for "
+ + session.getId()
+ + " A new one will be created. ",
+ e);
+ }
+ }
+
+ // will create new context if the above did not
+ try {
+ return getService().findVaadinSession(createVaadinRequest(request));
+ } catch (Exception e) {
+ throw new ServletException(e);
+ }
+ }
+
+ private boolean isCleanupRequest(HttpServletRequest request) {
+ String path = request.getPathInfo();
+ if (path != null && path.equals(CLEANUP_PATH)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Removes the ApplicationContext from the session in order to minimize the
+ * data serialized to datastore and memcache.
+ *
+ * @param request
+ */
+ private void cleanSession(VaadinServletRequest request) {
+ // Should really be replaced with a session storage API...
+ WrappedSession wrappedSession = request.getWrappedSession(false);
+ if (wrappedSession == null) {
+ return;
+ }
+ VaadinSession serviceSession = getService().loadSession(wrappedSession);
+ if (serviceSession == null) {
+ return;
+ }
+
+ /*
+ * Inform VaadinSession.valueUnbound that it should not kill the session
+ * even though it gets unbound.
+ */
+ serviceSession.setAttribute(
+ VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, Boolean.TRUE);
+ getService().removeSession(serviceSession.getSession());
+
+ // Remove preservation marker
+ serviceSession.setAttribute(
+ VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, null);
+ }
+
+ /**
+ * This will look at the timestamp and delete expired persisted Vaadin and
+ * appengine sessions from the datastore.
+ *
+ * TODO Possible improvements include: 1. Use transactions (requires entity
+ * groups - overkill?) 2. Delete one-at-a-time, catch possible exception,
+ * continue w/ next.
+ */
+ private void cleanDatastore() {
+ long expire = new Date().getTime();
+ try {
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ // Vaadin stuff first
+ {
+ Query q = new Query(AC_BASE);
+ q.setKeysOnly();
+
+ q.addFilter(PROPERTY_EXPIRES, FilterOperator.LESS_THAN_OR_EQUAL,
+ expire);
+ PreparedQuery pq = ds.prepare(q);
+ List<Entity> entities = pq
+ .asList(Builder.withLimit(CLEANUP_LIMIT));
+ if (entities != null) {
+ getLogger().log(Level.INFO,
+ "Vaadin cleanup deleting {0} expired Vaadin sessions.",
+ entities.size());
+ List<Key> keys = new ArrayList<Key>();
+ for (Entity e : entities) {
+ keys.add(e.getKey());
+ }
+ ds.delete(keys);
+ }
+ }
+ // Also cleanup GAE sessions
+ {
+ Query q = new Query(APPENGINE_SESSION_KIND);
+ q.setKeysOnly();
+ q.addFilter(PROPERTY_APPENGINE_EXPIRES,
+ FilterOperator.LESS_THAN_OR_EQUAL, expire);
+ PreparedQuery pq = ds.prepare(q);
+ List<Entity> entities = pq
+ .asList(Builder.withLimit(CLEANUP_LIMIT));
+ if (entities != null) {
+ getLogger().log(Level.INFO,
+ "Vaadin cleanup deleting {0} expired appengine sessions.",
+ entities.size());
+ List<Key> keys = new ArrayList<Key>();
+ for (Entity e : entities) {
+ keys.add(e.getKey());
+ }
+ ds.delete(keys);
+ }
+ }
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "Exception while cleaning.", e);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(GAEVaadinServlet.class.getName());
+ }
+}