/** * Copyright 2011 Google Inc. All Rights Reserved. * * 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.google.apphosting.utils.servlet; import com.google.appengine.api.taskqueue.DeferredTask; import com.google.appengine.api.taskqueue.DeferredTaskContext; import com.google.apphosting.api.ApiProxy; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.net.HttpURLConnection; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Implementation of {@link HttpServlet} to dispatch tasks with a * {@link DeferredTask} payload; see {@link TaskOptions#payload(DeferredTask)}. * * <p>This servlet is mapped to {@link DeferredTaskContext#DEFAULT_DEFERRED_URL} by * default. Below is a snippet of the web.xml configuration.<br> * <pre> * <servlet> * <servlet-name>/_ah/queue/__deferred__</servlet-name> * <servlet-class * >com.google.apphosting.utils.servlet.DeferredTaskServlet</servlet-class> * </servlet> * * <servlet-mapping> * <servlet-name>_ah_queue_deferred</servlet-name> * <url-pattern>/_ah/queue/__deferred__</url-pattern> * </servlet-mapping> * </pre> * */ public class DeferredTaskServlet extends HttpServlet { // Keep this in sync with X_APPENGINE_QUEUENAME and static final String X_APPENGINE_QUEUENAME = "X-AppEngine-QueueName"; static final String DEFERRED_TASK_SERVLET_KEY = DeferredTaskContext.class.getName() + ".httpServlet"; static final String DEFERRED_TASK_REQUEST_KEY = DeferredTaskContext.class.getName() + ".httpServletRequest"; static final String DEFERRED_TASK_RESPONSE_KEY = DeferredTaskContext.class.getName() + ".httpServletResponse"; static final String DEFERRED_DO_NOT_RETRY_KEY = DeferredTaskContext.class.getName() + ".doNotRetry"; static final String DEFERRED_MARK_RETRY_KEY = DeferredTaskContext.class.getName() + ".markRetry"; /** * Thrown by readRequest when an error occurred during deserialization. */ protected static class DeferredTaskException extends Exception { public DeferredTaskException(Exception e) { super(e); } } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // header set. Non admin users cannot set this header so it's a signal that // this came from task queue or an admin smart enough to set the header. if (req.getHeader(X_APPENGINE_QUEUENAME) == null) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Not a taskqueue request."); return; } String method = req.getMethod(); if (!method.equals("POST")) { String protocol = req.getProtocol(); String msg = "DeferredTaskServlet does not support method: " + method; if (protocol.endsWith("1.1")) { resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); } else { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); } return; } // Place the current servlet, request and response in the environment for // situations where the task may need to get to it. Map<String, Object> attributes = ApiProxy.getCurrentEnvironment().getAttributes(); attributes.put(DEFERRED_TASK_SERVLET_KEY, this); attributes.put(DEFERRED_TASK_REQUEST_KEY, req); attributes.put(DEFERRED_TASK_RESPONSE_KEY, resp); attributes.put(DEFERRED_MARK_RETRY_KEY, false); try { performRequest(req, resp); if ((Boolean) attributes.get(DEFERRED_MARK_RETRY_KEY)) { resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR); } else { resp.setStatus(HttpURLConnection.HTTP_OK); } } catch (DeferredTaskException e) { resp.setStatus(HttpURLConnection.HTTP_UNSUPPORTED_TYPE); log("Deferred task failed exception: " + e); return; } catch (RuntimeException e) { Boolean doNotRetry = (Boolean) attributes.get(DEFERRED_DO_NOT_RETRY_KEY); if (doNotRetry == null || !doNotRetry) { throw new ServletException(e); } else if (doNotRetry) { resp.setStatus(HttpURLConnection.HTTP_NOT_AUTHORITATIVE); // Alternate success code. log(DeferredTaskServlet.class.getName() + " - Deferred task failed but doNotRetry specified. Exception: " + e); } } finally { // Clean out the attributes. attributes.remove(DEFERRED_TASK_SERVLET_KEY); attributes.remove(DEFERRED_TASK_REQUEST_KEY); attributes.remove(DEFERRED_TASK_RESPONSE_KEY); attributes.remove(DEFERRED_DO_NOT_RETRY_KEY); } } /** * Performs a task enqueued with {@link TaskOptions#payload(DeferredTask)} by * deserializing the input stream of the {@link HttpServletRequest}. * * @param req The HTTP request. * @param resp The HTTP response. * @throws DeferredTaskException If an error occurred while deserializing * the task. * <p>Note that other exceptions may be thrown by the * {@link DeferredTask#run()} method. */ protected void performRequest( HttpServletRequest req, HttpServletResponse resp) throws DeferredTaskException { readRequest(req, resp).run(); } /** * De-serializes the {@link DeferredTask} object from the input stream. * @throws DeferredTaskException With the chained exception being one of the following: * <li>{@link IllegalArgumentException}: Indicates a content-type header mismatch. * <li>{@link ClassNotFoundException}: Deserialization failure. * <li>{@link InvalidClassException}: Deserialization failure. * <li>{@link StreamCorruptedException}: Deserialization failure. * <li>{@link OptionalDataException}: Deserialization failure. * <li>{@link IOException}: Deserialization failure. * <li>{@link ClassCastException}: Deserialization failure. * */ protected DeferredTask readRequest( HttpServletRequest req, HttpServletResponse resp) throws DeferredTaskException { String contentType = req.getHeader("content-type"); if (contentType == null || !contentType.equals(DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE)) { throw new DeferredTaskException(new IllegalArgumentException( "Invalid content-type header." + " received: '" + (contentType == null ? "null" : contentType) + "' expected: '" + DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE + "'")); } DeferredTask deferredTask; try { ServletInputStream stream = req.getInputStream(); ObjectInputStream objectStream = new ObjectInputStream(stream) { @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String name = desc.getName(); try { return Class.forName(name, false, classLoader); } catch (ClassNotFoundException ex) { // This one should also handle primitive types return super.resolveClass(desc); } } @Override protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { // Note(user) This logic was copied from ObjectInputStream.java in the // JDK, and then modified to use the thread context class loader instead of the // "latest" loader that is used there. ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); ClassLoader nonPublicLoader = null; boolean hasNonPublicInterface = false; // define proxy in class loader of non-public interface(s), if any Class[] classObjs = new Class[interfaces.length]; for (int i = 0; i < interfaces.length; i++) { Class cl = Class.forName(interfaces[i], false, classLoader); if ((cl.getModifiers() & Modifier.PUBLIC) == 0) { if (hasNonPublicInterface) { if (nonPublicLoader != cl.getClassLoader()) { throw new IllegalAccessError("conflicting non-public interface class loaders"); } } else { nonPublicLoader = cl.getClassLoader(); hasNonPublicInterface = true; } } classObjs[i] = cl; } try { return Proxy.getProxyClass( hasNonPublicInterface ? nonPublicLoader : classLoader, classObjs); } catch (IllegalArgumentException e) { throw new ClassNotFoundException(null, e); } } }; deferredTask = (DeferredTask) objectStream.readObject(); } catch (ClassNotFoundException e) { throw new DeferredTaskException(e); } catch (IOException e) { throw new DeferredTaskException(e); } catch (ClassCastException e) { throw new DeferredTaskException(e); } return deferredTask; } }