/* @VaadinApache2LicenseForJavaFiles@ */ package com.vaadin.terminal.gwt.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; import com.vaadin.service.ApplicationContext; /** * ApplicationServlet to be used when deploying to Google App Engine, in * web.xml: * *
 *      <servlet>
 *              <servlet-name>HelloWorld</servlet-name>
 *              <servlet-class>com.vaadin.terminal.gwt.server.GAEApplicationServlet</servlet-class>
 *              <init-param>
 *                      <param-name>application</param-name>
 *                      <param-value>com.vaadin.demo.HelloWorld</param-value>
 *              </init-param>
 *      </servlet>
 * 
* * Session support must be enabled in appengine-web.xml: * *
 *      <sessions-enabled>true</sessions-enabled>
 * 
* * 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): * *
 * <cronentries>
 *   <cron>
 *     <url>/HelloWorld/CLEAN</url>
 *     <description>Clean up sessions</description>
 *     <schedule>every 2 hours</schedule>
 *   </cron>
 * </cronentries>
 * 
* * 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: * *
 *      <static-files>
 *           <include path="/VAADIN/**" />
 *      </static-files>
 * 
* * Additional limitations: * */ public class GAEApplicationServlet extends ApplicationServlet { private static final Logger logger = Logger .getLogger(GAEApplicationServlet.class.getName()); // 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"; protected void sendDeadlineExceededNotification(HttpServletRequest request, HttpServletResponse 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(HttpServletRequest request, HttpServletResponse 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(HttpServletRequest request, HttpServletResponse 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 request, HttpServletResponse response) throws ServletException, IOException { if (isCleanupRequest(request)) { cleanDatastore(); return; } RequestType requestType = getRequestType(request); if (requestType == RequestType.STATIC_FILE) { // no locking needed, let superclass handle super.service(request, response); cleanSession(request); return; } if (requestType == RequestType.APPLICATION_RESOURCE) { // no locking needed, let superclass handle getApplicationContext(request, MemcacheServiceFactory.getMemcacheService()); super.service(request, response); cleanSession(request); return; } final HttpSession session = request .getSession(requestCanCreateApplication(request, requestType)); if (session == null) { handleServiceSessionExpired(request, response); 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 = new Date().getTime(); // non-UIDL requests will try indefinitely while (requestType != RequestType.UIDL || new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); if (locked) { break; } try { Thread.sleep(RETRY_AFTER_MILLISECONDS); } catch (InterruptedException e) { logger.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 ApplicationContext 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 + (session.getMaxInactiveInterval() * 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) { logger.warning("DeadlineExceeded for " + session.getId()); sendDeadlineExceededNotification(request, response); } catch (NotSerializableException e) { logger.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) { logger.log(Level.WARNING, "An exception occurred while servicing request.", e); sendCriticalErrorNotification(request, response); } finally { // "Next, please!" if (locked) { memcache.delete(mutex); } cleanSession(request); } } protected ApplicationContext getApplicationContext( HttpServletRequest request, MemcacheService memcache) { 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(session .getMaxInactiveInterval()), MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); } } if (serializedAC != null) { ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); ObjectInputStream ois; try { ois = new ObjectInputStream(bais); ApplicationContext applicationContext = (ApplicationContext) ois .readObject(); session.setAttribute(WebApplicationContext.class.getName(), applicationContext); } catch (IOException e) { logger.log(Level.WARNING, "Could not de-serialize ApplicationContext for " + session.getId() + " A new one will be created. ", e); } catch (ClassNotFoundException e) { logger.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 return getApplicationContext(session); } private boolean isCleanupRequest(HttpServletRequest request) { String path = getRequestPathInfo(request); 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(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { session.removeAttribute(WebApplicationContext.class.getName()); } } /** * 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 entities = pq.asList(Builder .withLimit(CLEANUP_LIMIT)); if (entities != null) { logger.info("Vaadin cleanup deleting " + entities.size() + " expired Vaadin sessions."); List keys = new ArrayList(); 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 entities = pq.asList(Builder .withLimit(CLEANUP_LIMIT)); if (entities != null) { logger.info("Vaadin cleanup deleting " + entities.size() + " expired appengine sessions."); List keys = new ArrayList(); for (Entity e : entities) { keys.add(e.getKey()); } ds.delete(keys); } } } catch (Exception e) { logger.log(Level.WARNING, "Exception while cleaning.", e); } } }