/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.sling.api.servlets; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper; /** * Helper base class for read-only Servlets used in Sling. This base class is * actually just a better implementation of the Servlet API <em>HttpServlet</em> * class which accounts for extensibility. So extensions of this class have * great control over what methods to overwrite. * <p> * If any of the default HTTP methods is to be implemented just overwrite the * respective doXXX method. If additional methods should be supported implement * appropriate doXXX methods and overwrite the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method * to dispatch to the doXXX methods as appropriate and overwrite the * {@link #getAllowedRequestMethods(Map)} to add the new method names. * <p> * Please note, that this base class is intended for applications where data is * only read. As such, this servlet by itself does not support the <em>POST</em>, * <em>PUT</em> and <em>DELETE</em> methods. Extensions of this class should * either overwrite any of the doXXX methods of this class or add support for * other read-only methods only. Applications wishing to support data * modification should rather use or extend the {@link SlingAllMethodsServlet} * which also contains support for the <em>POST</em>, <em>PUT</em> and * <em>DELETE</em> methods. This latter class should also be overwritten to * add support for HTTP methods modifying data. * <p> * Implementors note: The methods in this class are all declared to throw the * exceptions according to the intentions of the Servlet API rather than * throwing their Sling RuntimeException counter parts. This is done to ease the * integration with traditional servlets. * * @see SlingAllMethodsServlet */ public class SlingSafeMethodsServlet extends GenericServlet { private static final long serialVersionUID = 3620512288346703072L; /** * Handles the <em>HEAD</em> method. * <p> * This base implementation just calls the * {@link #doGet(SlingHttpServletRequest, SlingHttpServletResponse)} method dropping * the output. Implementations of this class may overwrite this method if * they have a more performing implementation. Otherwise, they may just keep * this base implementation. * * @param request The HTTP request * @param response The HTTP response which only gets the headers set * @throws ServletException Forwarded from the * {@link #doGet(SlingHttpServletRequest, SlingHttpServletResponse)} * method called by this implementation. * @throws IOException Forwarded from the * {@link #doGet(SlingHttpServletRequest, SlingHttpServletResponse)} * method called by this implementation. */ protected void doHead(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { // the null-output wrapper NoBodyResponse wrappedResponse = new NoBodyResponse(response); // do a normal get request, dropping the output doGet(request, wrappedResponse); // ensure the content length is set as gathered by the null-output wrappedResponse.setContentLength(); } /** * Called by the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method to * handle an HTTP <em>GET</em> request. * <p> * This default implementation reports back to the client that the method is * not supported. * <p> * Implementations of this class should overwrite this method with their * implementation for the HTTP <em>GET</em> method support. * * @param request The HTTP request * @param response The HTTP response * @throws ServletException Not thrown by this implementation. * @throws IOException If the error status cannot be reported back to the * client. */ protected void doGet(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { handleMethodNotImplemented(request, response); } /** * Handles the <em>OPTIONS</em> method by setting the HTTP * <code>Allow</code> header on the response depending on the methods * declared in this class. * <p> * Extensions of this class should generally not overwrite this method but * rather the {@link #getAllowedRequestMethods(Map)} method. This method * gathers all declared public and protected methods for the concrete class * (upto but not including this class) and calls the * {@link #getAllowedRequestMethods(Map)} method with the methods gathered. * The returned value is then used as the value of the <code>Allow</code> * header set. * * @param request The HTTP request object. Not used. * @param response The HTTP response object on which the header is set. * @throws ServletException Not thrown by this implementation. * @throws IOException Not thrown by this implementation. */ protected void doOptions(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { Map<String, Method> methods = getAllDeclaredMethods(getClass()); StringBuffer allowBuf = getAllowedRequestMethods(methods); response.setHeader("Allow", allowBuf.toString()); } /** * Handles the <em>TRACE</em> method by just returning the list of all * header values in the response body. * <p> * Extensions of this class do not generally need to overwrite this method * as it contains all there is to be done to the <em>TRACE</em> method. * * @param request The HTTP request whose headers are returned. * @param response The HTTP response into which the request headers are * written. * @throws ServletException Not thrown by this implementation. * @throws IOException May be thrown if there is an problem sending back the * request headers in the response stream. */ protected void doTrace(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { String CRLF = "\r\n"; StringBuffer responseString = new StringBuffer(); responseString.append("TRACE ").append(request.getRequestURI()); responseString.append(' ').append(request.getProtocol()); Enumeration<?> reqHeaderEnum = request.getHeaderNames(); while (reqHeaderEnum.hasMoreElements()) { String headerName = (String) reqHeaderEnum.nextElement(); Enumeration<?> reqHeaderValEnum = request.getHeaders(headerName); while (reqHeaderValEnum.hasMoreElements()) { responseString.append(CRLF); responseString.append(headerName).append(": "); responseString.append(reqHeaderValEnum.nextElement()); } } responseString.append(CRLF); String charset = "UTF-8"; byte[] rawResponse = responseString.toString().getBytes(charset); int responseLength = rawResponse.length; response.setContentType("message/http"); response.setCharacterEncoding(charset); response.setContentLength(responseLength); ServletOutputStream out = response.getOutputStream(); out.write(rawResponse); } /** * Called by the {@link #service(SlingHttpServletRequest, SlingHttpServletResponse)} * method to handle a request for an HTTP method, which is not known and * handled by this class or its extension. * <p> * This default implementation reports back to the client that the method is * not supported. * <p> * This method should be overwritten with great care. It is better to * overwrite the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method and * add support for any extension HTTP methods through an additional doXXX * method. * * @param request The HTTP request * @param response The HTTP response * @throws ServletException Not thrown by this implementation. * @throws IOException If the error status cannot be reported back to the * client. */ protected void doGeneric(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { handleMethodNotImplemented(request, response); } /** * Tries to handle the request by calling a Java method implemented for the * respective HTTP request method. * <p> * This base class implentation dispatches the <em>HEAD</em>, * <em>GET</em>, <em>OPTIONS</em> and <em>TRACE</em> to the * respective <em>doXXX</em> methods and returns <code>true</code> if * any of these methods is requested. Otherwise <code>false</code> is just * returned. * <p> * Implementations of this class may overwrite this method but should first * call this base implementation and in case <code>false</code> is * returned add handling for any other method and of course return whether * the requested method was known or not. * * @param request The HTTP request * @param response The HTTP response * @return <code>true</code> if the requested method (<code>request.getMethod()</code>) * is known. Otherwise <code>false</code> is returned. * @throws ServletException Forwarded from any of the dispatched methods * @throws IOException Forwarded from any of the dispatched methods */ protected boolean mayService(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { // assume the method is known for now boolean methodKnown = true; String method = request.getMethod(); if (HttpConstants.METHOD_HEAD.equals(method)) { doHead(request, response); } else if (HttpConstants.METHOD_GET.equals(method)) { doGet(request, response); } else if (HttpConstants.METHOD_OPTIONS.equals(method)) { doOptions(request, response); } else if (HttpConstants.METHOD_TRACE.equals(method)) { doTrace(request, response); } else { // actually we do not know the method methodKnown = false; } // return whether we actually knew the request method or not return methodKnown; } /** * Helper method which causes an appropriate HTTP response to be sent for an * unhandled HTTP request method. In case of HTTP/1.1 a 405 status code * (Method Not Allowed) is returned, otherwise a 400 status (Bad Request) is * returned. * * @param request The HTTP request from which the method and protocol values * are extracted to build the appropriate message. * @param response The HTTP response to which the error status is sent. * @throws IOException Thrown if the status cannot be sent to the client. */ protected void handleMethodNotImplemented(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws IOException { String protocol = request.getProtocol(); String msg = "Method " + request.getMethod() + " not supported"; if (protocol.endsWith("1.1")) { // for HTTP/1.1 use 405 Method Not Allowed response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); } else { // otherwise use 400 Bad Request response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); } } /** * Called by the {@link #service(ServletRequest, ServletResponse)} method to * handle the HTTP request. This implementation calls the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method and * depedending on its return value call the * {@link #doGeneric(SlingHttpServletRequest, SlingHttpServletResponse)} method. If * the {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method * can handle the request, the * {@link #doGeneric(SlingHttpServletRequest, SlingHttpServletResponse)} method is not * called otherwise it is called. * <p> * Implementations of this class should not generally overwrite this method. * Rather the {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} * method should be overwritten to add support for more HTTP methods. * * @param request The HTTP request * @param response The HTTP response * @throws ServletException Forwarded from the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} * or * {@link #doGeneric(SlingHttpServletRequest, SlingHttpServletResponse)} * methods. * @throws IOException Forwarded from the * {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} * or * {@link #doGeneric(SlingHttpServletRequest, SlingHttpServletResponse)} * methods. */ protected void service(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpServletResponse response) throws ServletException, IOException { // first try to handle the request by the known methods boolean methodKnown = mayService(request, response); // otherwise try to handle it through generic means if (!methodKnown) { doGeneric(request, response); } } /** * Forwards the request to the * {@link #service(SlingHttpServletRequest, SlingHttpServletResponse)} * method if the request is a HTTP request. * <p> * Implementations of this class will not generally overwrite this method. * * @param req The Servlet request * @param res The Servlet response * @throws ServletException If the request is not a HTTP request or * forwarded from the * {@link #service(SlingHttpServletRequest, SlingHttpServletResponse)} * called. * @throws IOException Forwarded from the * {@link #service(SlingHttpServletRequest, SlingHttpServletResponse)} * called. */ @Override public void service(@Nonnull ServletRequest req, @Nonnull ServletResponse res) throws ServletException, IOException { if ((req instanceof SlingHttpServletRequest) && (res instanceof SlingHttpServletResponse)) { service((SlingHttpServletRequest) req, (SlingHttpServletResponse) res); } else { throw new ServletException("Not a Sling HTTP request/response"); } } /** * Returns the simple class name of this servlet class. Extensions of this * class may overwrite to return more specific information. */ @Override public @Nonnull String getServletInfo() { return getClass().getSimpleName(); } /** * Helper method called by * {@link #doOptions(SlingHttpServletRequest, SlingHttpServletResponse)} to calculate * the value of the <em>Allow</em> header sent as the response to the HTTP * <em>OPTIONS</em> request. * <p> * This base class implementation checks whether any doXXX methods exist for * <em>GET</em> and <em>HEAD</em> and returns the list of methods * supported found. The list returned always includes the HTTP * <em>OPTIONS</em> and <em>TRACE</em> methods. * <p> * Implementations of this class may overwrite this method check for more * methods supported by the extension (generally the same list as used in * the {@link #mayService(SlingHttpServletRequest, SlingHttpServletResponse)} method). * This base class implementation should always be called to make sure the * default HTTP methods are included in the list. * * @param declaredMethods The public and protected methods declared in the * extension of this class. * @return A <code>StringBuffer</code> containing the list of HTTP methods * supported. */ protected StringBuffer getAllowedRequestMethods( Map<String, Method> declaredMethods) { StringBuffer allowBuf = new StringBuffer(); // OPTIONS and TRACE are always supported by this servlet allowBuf.append(HttpConstants.METHOD_OPTIONS); allowBuf.append(", ").append(HttpConstants.METHOD_TRACE); // add more method names depending on the methods found if (declaredMethods.containsKey("doHead") && !declaredMethods.containsKey("doGet")) { allowBuf.append(", ").append(HttpConstants.METHOD_HEAD); } else if (declaredMethods.containsKey("doGet")) { allowBuf.append(", ").append(HttpConstants.METHOD_GET); allowBuf.append(", ").append(HttpConstants.METHOD_HEAD); } return allowBuf; } /** * Returns a map of methods declared by the class indexed by method name. * This method is called by the * {@link #doOptions(SlingHttpServletRequest, SlingHttpServletResponse)} method to * find the methods to be checked by the * {@link #getAllowedRequestMethods(Map)} method. Note, that only extension * classes of this class are considered to be sure to not account for the * default implementations of the doXXX methods in this class. * * @param c The <code>Class</code> to get the declared methods from * @return The Map of methods considered for support checking. */ private Map<String, Method> getAllDeclaredMethods(Class<?> c) { // stop (and do not include) the AbstractSlingServletClass if (c == null || c.getName().equals(SlingSafeMethodsServlet.class.getName())) { return new HashMap<String, Method>(); } // get the declared methods from the base class Map<String, Method> methodSet = getAllDeclaredMethods(c.getSuperclass()); // add declared methods of c (maybe overwrite base class methods) Method[] declaredMethods = c.getDeclaredMethods(); for (Method method : declaredMethods) { // only consider public and protected methods if (Modifier.isProtected(method.getModifiers()) || Modifier.isPublic(method.getModifiers())) { methodSet.put(method.getName(), method); } } return methodSet; } /** * A response that includes no body, for use in (dumb) "HEAD" support. This * just swallows that body, counting the bytes in order to set the content * length appropriately. */ private class NoBodyResponse extends SlingHttpServletResponseWrapper { /** The byte sink and counter */ private NoBodyOutputStream noBody; /** Optional writer around the byte sink */ private PrintWriter writer; /** Whether the request processor set the content length itself or not. */ private boolean didSetContentLength; NoBodyResponse(SlingHttpServletResponse wrappedResponse) { super(wrappedResponse); noBody = new NoBodyOutputStream(); } /** * Called at the end of request processing to ensure the content length * is set. If the processor already set the length, this method does not * do anything. Otherwise the number of bytes written through the * null-output is set on the response. */ void setContentLength() { if (!didSetContentLength) { setContentLength(noBody.getContentLength()); } } /** * Overwrite this to prevent setting the content length at the end of * the request through {@link #setContentLength()} */ @Override public void setContentLength(int len) { super.setContentLength(len); didSetContentLength = true; } /** * Just return the null output stream and don't check whether a writer * has already been acquired. */ @Override public ServletOutputStream getOutputStream() { return noBody; } /** * Just return the writer to the null output stream and don't check * whether an output stram has already been acquired. */ @Override public PrintWriter getWriter() throws UnsupportedEncodingException { if (writer == null) { OutputStreamWriter w; w = new OutputStreamWriter(noBody, getCharacterEncoding()); writer = new PrintWriter(w); } return writer; } } /** * Simple ServletOutputStream which just does not write but counts the bytes * written through it. This class is used by the NoBodyResponse. */ private class NoBodyOutputStream extends ServletOutputStream { private int contentLength = 0; /** * @return the number of bytes "written" through this stream */ int getContentLength() { return contentLength; } @Override public void write(int b) { contentLength++; } @Override public void write(byte buf[], int offset, int len) { if (len >= 0) { contentLength += len; } else { throw new IndexOutOfBoundsException(); } } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener writeListener) { // nothing to do } } }