blob: 550dcc4e403fba62ae07b6ec0a66e5363158e3c5 [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.pivot.web.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.Iterator;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.pivot.annotations.UnsupportedOperation;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.serialization.Serializer;
import org.apache.pivot.util.Constants;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.web.Query;
import org.apache.pivot.web.QueryDictionary;
import org.apache.pivot.web.QueryException;
/**
* Abstract base class for query servlets.
*/
public abstract class QueryServlet extends HttpServlet {
/**
* Immutable string sequence representing a query path. The path is
* constructed by splitting the path info provided by the base servlet on
* the path separator character ("/").
*/
public static class Path implements Sequence<String>, Iterable<String> {
private ArrayList<String> elements;
public Path() {
this(new String[] {});
}
public Path(final String[] elements) {
this.elements = new ArrayList<>(elements);
}
@Override
@UnsupportedOperation
public int add(final String element) {
throw new UnsupportedOperationException();
}
@Override
@UnsupportedOperation
public void insert(String element, int index) {
throw new UnsupportedOperationException();
}
@Override
@UnsupportedOperation
public String update(final int index, final String element) {
throw new UnsupportedOperationException();
}
@Override
@UnsupportedOperation
public int remove(final String element) {
throw new UnsupportedOperationException();
}
@Override
public Sequence<String> remove(final int index, final int count) {
return elements.remove(index, count);
}
@Override
public String get(final int index) {
return elements.get(index);
}
@Override
public int indexOf(final String element) {
return elements.indexOf(element);
}
@Override
public int getLength() {
return elements.getLength();
}
@Override
public Iterator<String> iterator() {
return new ImmutableIterator<>(elements.iterator());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (String element : elements) {
sb.append("/");
sb.append(element);
}
return sb.toString();
}
}
private static final long serialVersionUID = 4881638232902478092L;
private boolean determineContentLength = false;
private transient ThreadLocal<String> hostname = new ThreadLocal<>();
private transient ThreadLocal<Integer> port = new ThreadLocal<>();
private transient ThreadLocal<String> contextPath = new ThreadLocal<>();
private transient ThreadLocal<String> servletPath = new ThreadLocal<>();
private transient ThreadLocal<Boolean> secure = new ThreadLocal<>();
private transient ThreadLocal<QueryDictionary> parameters = new ThreadLocal<>();
private transient ThreadLocal<QueryDictionary> requestHeaders = new ThreadLocal<>();
private transient ThreadLocal<QueryDictionary> responseHeaders = new ThreadLocal<>();
/**
* Gets the host name that was requested.
* @return The host name from the request.
*/
public String getHostname() {
return hostname.get();
}
/**
* Returns the Internet Protocol (IP) port number of the interface on which
* the request was received.
* @return The port number from the request.
*/
public int getPort() {
return port.get().intValue();
}
/**
* Returns the portion of the request URL representing the context path.
* @return The context path from the request URL.
*/
public String getContextPath() {
return contextPath.get();
}
/**
* Returns the portion of the request URL representing the servlet path.
* @return The servlet path parsed out of the request URL.
*/
public String getServletPath() {
return servletPath.get();
}
/**
* Tells whether the request has been encrypted over HTTPS.
* @return {@code true} if the request was sent over HTTPS, {@code false} otherwise
* (regular HTTP).
*/
public boolean isSecure() {
return secure.get().booleanValue();
}
/**
* Returns the name of the HTTP protocol that the request is using.
* @return The {@code "http"} or {@code "https"} protocol string depending
* on the {@link #isSecure} setting.
*/
public String getProtocol() {
return isSecure() ? Constants.HTTPS_PROTOCOL : Constants.HTTP_PROTOCOL;
}
/**
* Returns the location of this servlet.
* @return The {@link URL} associated with this servlet.
*/
public URL getLocation() {
URL location;
try {
location = new URL(isSecure() ? Constants.HTTPS_PROTOCOL : Constants.HTTP_PROTOCOL, getHostname(),
getPort(), getContextPath() + getServletPath() + "/");
} catch (MalformedURLException exception) {
throw new RuntimeException(exception);
}
return location;
}
/**
* Returns the servlet's parameter dictionary, which holds the values passed
* in the HTTP request query string.
* @return The set of parameters passed in this query.
*/
public QueryDictionary getParameters() {
return parameters.get();
}
/**
* Returns the servlet's request header dictionary, which holds the HTTP
* request headers.
* @return The set of request headers set in this query.
*/
public QueryDictionary getRequestHeaders() {
return requestHeaders.get();
}
/**
* Returns the servlet's response header dictionary, which holds the HTTP
* response headers that will be sent back to the client.
* @return The current set of response headers to return to the client.
*/
public QueryDictionary getResponseHeaders() {
return responseHeaders.get();
}
/**
* Prepares a servlet for request execution. This method is called
* immediately prior to the {@link #validate(Query.Method, Path)} method.
* <p> The default implementation is a no-op.
*
* @throws ServletException on any kind of error.
*/
protected void prepare() throws ServletException {
// No-op
}
/**
* Disposes any resources allocated in {@link #prepare()}. This method is
* guaranteed to be called even if the HTTP handler method throws. <p> The
* default implementation is a no-op.
*
* @throws ServletException on any kind of error.
*/
protected void dispose() throws ServletException {
// No-op
}
/**
* Validates a servlet for request execution. This method is called
* immediately prior to the HTTP handler method. <p> The default
* implementation is a no-op.
*
* @param method The type of query this is.
* @param path The path to the server resources.
* @throws QueryException if there is a problem, Houston.
*/
protected void validate(final Query.Method method, final Path path) throws QueryException {
// No-op
}
/**
* Handles an HTTP GET request. The default implementation throws an HTTP
* 405 query exception.
*
* @param path The request path.
* @return The result of the GET.
* @throws QueryException on any error.
*/
protected Object doGet(final Path path) throws QueryException {
throw new QueryException(Query.Status.METHOD_NOT_ALLOWED);
}
/**
* Handles an HTTP POST request. The default implementation throws an HTTP
* 405 query exception.
*
* @param path The path of this request.
* @param value The value parsed from the POST request data.
* @return A URL containing the location of the created resource, or
* <tt>null</tt> if operation did not result in the creation of a resource.
* @throws QueryException on errors.
*/
protected URL doPost(final Path path, final Object value) throws QueryException {
throw new QueryException(Query.Status.METHOD_NOT_ALLOWED);
}
/**
* Handles an HTTP GET request. The default implementation throws an HTTP
* 405 query exception.
*
* @param path The server path for this request.
* @param value The value parsed from the PUT request data.
* @return <tt>true</tt> if the operation resulted in the creation of a
* resource; <tt>false</tt>, otherwise.
* @throws QueryException on any error.
*/
protected boolean doPut(final Path path, final Object value) throws QueryException {
throw new QueryException(Query.Status.METHOD_NOT_ALLOWED);
}
/**
* Handles an HTTP DELETE request. The default implementation throws an HTTP
* 405 query exception.
*
* @param path The server path for this request.
* @throws QueryException if there was a problem.
*/
protected void doDelete(final Path path) throws QueryException {
throw new QueryException(Query.Status.METHOD_NOT_ALLOWED);
}
/**
* Creates a serializer that will be used to serialize the current request
* data.
*
* @param method Type of query this serializer will apply to.
* @param path The server path this is intended for.
* @return The newly created serializer for this request.
* @throws QueryException if there is a problem.
*/
protected abstract Serializer<?> createSerializer(Query.Method method, Path path)
throws QueryException;
@Override
protected void service(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
try {
try {
URL url = new URL(request.getRequestURL().toString());
hostname.set(url.getHost());
port.set(Integer.valueOf(request.getLocalPort()));
contextPath.set(request.getContextPath());
servletPath.set(request.getServletPath());
secure.set(Boolean.valueOf(url.getProtocol().equalsIgnoreCase(Constants.HTTPS_PROTOCOL)));
} catch (MalformedURLException exception) {
throw new ServletException(exception);
}
parameters.set(new QueryDictionary(true));
requestHeaders.set(new QueryDictionary(false));
responseHeaders.set(new QueryDictionary(false));
// Copy the query string into the arguments dictionary
String queryString = request.getQueryString();
if (queryString != null) {
QueryDictionary parametersDictionary = parameters.get();
String[] pairs = queryString.split("&");
for (int i = 0, n = pairs.length; i < n; i++) {
String[] pair = pairs[i].split("=");
String key = URLDecoder.decode(pair[0], Constants.URL_ENCODING);
String value = URLDecoder.decode((pair.length > 1) ? pair[1] : "", Constants.URL_ENCODING);
parametersDictionary.add(key, value);
}
}
// Copy the request headers into the request properties dictionary
QueryDictionary requestHeaderDictionary = requestHeaders.get();
@SuppressWarnings("unchecked")
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
requestHeaderDictionary.add(headerName, headerValue);
}
// Prepare the servlet for request processing
prepare();
// Process the request
super.service(request, response);
} catch (IOException exception) {
System.err.println(exception);
throw exception;
} catch (RuntimeException exception) {
System.err.println(exception);
throw exception;
} finally {
// Clean up thread local variables
hostname.remove();
port.remove();
contextPath.remove();
servletPath.remove();
secure.remove();
parameters.remove();
requestHeaders.remove();
responseHeaders.remove();
// Clean up any allocated resources
dispose();
}
}
@Override
@SuppressWarnings("unchecked")
protected final void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
Path path = getPath(request);
Object result = null;
Serializer<Object> serializer = null;
try {
validate(Query.Method.GET, path);
result = doGet(path);
serializer = (Serializer<Object>) createSerializer(Query.Method.GET, path);
} catch (QueryException exception) {
response.setStatus(exception.getStatus());
response.flushBuffer();
}
if (!response.isCommitted() && serializer != null) {
response.setStatus(Query.Status.OK);
setResponseHeaders(response);
response.setContentType(serializer.getMIMEType(result));
OutputStream responseOutputStream = response.getOutputStream();
if (determineContentLength) {
File tempFile = File.createTempFile(getClass().getName(), null);
// Serialize the result to an intermediary file
try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
serializer.writeObject(result, fileOutputStream);
} catch (SerializationException exception) {
throw new ServletException(exception);
}
// Set the content length header
response.setHeader(Constants.CONTENT_LENGTH_HEADER, String.valueOf(tempFile.length()));
// Write the contents of the file out to the response
try (FileInputStream fileInputStream = new FileInputStream(tempFile)) {
byte[] buffer = new byte[1024];
int nBytes;
do {
nBytes = fileInputStream.read(buffer);
if (nBytes > 0) {
responseOutputStream.write(buffer, 0, nBytes);
}
} while (nBytes != -1);
}
} else {
try {
serializer.writeObject(result, responseOutputStream);
} catch (SerializationException exception) {
throw new ServletException(exception);
}
}
response.flushBuffer();
}
}
@Override
protected final void doPost(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
Path path = getPath(request);
URL location = null;
try {
validate(Query.Method.POST, path);
Object value = null;
if (request.getContentLength() > 0) {
Serializer<?> serializer = createSerializer(Query.Method.POST, path);
value = serializer.readObject(request.getInputStream());
}
location = doPost(path, value);
} catch (SerializationException exception) {
throw new ServletException(exception);
} catch (QueryException exception) {
response.setStatus(exception.getStatus());
response.flushBuffer();
}
if (!response.isCommitted()) {
if (location == null) {
response.setStatus(Query.Status.NO_CONTENT);
} else {
response.setStatus(Query.Status.CREATED);
response.setHeader(Constants.LOCATION_HEADER, location.toString());
}
setResponseHeaders(response);
response.setContentLength(0);
}
}
@Override
protected final void doPut(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
Path path = getPath(request);
boolean created = false;
try {
validate(Query.Method.PUT, path);
Object value = null;
if (request.getContentLength() > 0) {
Serializer<?> serializer = createSerializer(Query.Method.PUT, path);
value = serializer.readObject(request.getInputStream());
}
created = doPut(path, value);
} catch (SerializationException exception) {
throw new ServletException(exception);
} catch (QueryException exception) {
response.setStatus(exception.getStatus());
response.flushBuffer();
}
if (!response.isCommitted()) {
response.setStatus(created ? Query.Status.CREATED : Query.Status.NO_CONTENT);
setResponseHeaders(response);
response.setContentLength(0);
response.flushBuffer();
}
}
@Override
protected final void doDelete(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
try {
Path path = getPath(request);
validate(Query.Method.DELETE, path);
doDelete(path);
} catch (QueryException exception) {
response.setStatus(exception.getStatus());
response.flushBuffer();
}
if (!response.isCommitted()) {
response.setStatus(204);
setResponseHeaders(response);
response.setContentLength(0);
response.flushBuffer();
}
}
@Override
protected final void doHead(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
response.flushBuffer();
}
@Override
protected final void doOptions(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
response.flushBuffer();
}
@Override
protected final void doTrace(final HttpServletRequest request, final HttpServletResponse response)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
response.flushBuffer();
}
private static Path getPath(final HttpServletRequest request) {
String pathInfo = request.getPathInfo();
Path path;
if (pathInfo == null || pathInfo.length() == 0) {
path = new Path();
} else {
path = new Path(pathInfo.substring(1).split("/"));
}
return path;
}
private void setResponseHeaders(final HttpServletResponse response) {
QueryDictionary responseHeaderDictionary = responseHeaders.get();
for (String key : responseHeaderDictionary) {
for (int i = 0, n = responseHeaderDictionary.getLength(key); i < n; i++) {
response.addHeader(key, responseHeaderDictionary.get(key, i));
}
}
}
}