blob: 84d785bf402eed9055895ce4b71713671eedeb6f [file] [log] [blame]
/*
* 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
}
}
}