| /* |
| * 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.AbstractList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.List; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.pivot.io.SerializationException; |
| import org.apache.pivot.io.Serializer; |
| import org.apache.pivot.web.Query; |
| import org.apache.pivot.web.QueryException; |
| import org.apache.pivot.web.QueryProperties; |
| |
| /** |
| * 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 extends AbstractList<String> { |
| private List<String> elements; |
| |
| public Path() { |
| this(new String[] {}); |
| } |
| |
| public Path(String[] elements) { |
| this.elements = Arrays.asList(elements); |
| } |
| |
| @Override |
| public String get(int index) { |
| return elements.get(index); |
| } |
| |
| @Override |
| public int size() { |
| return elements.size(); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| |
| int i = 0; |
| for (String element : elements) { |
| sb.append("/"); |
| sb.append(element); |
| i++; |
| } |
| |
| return sb.toString(); |
| } |
| } |
| |
| private static final long serialVersionUID = 0; |
| |
| private boolean determineContentLength = false; |
| |
| private transient ThreadLocal<String> hostname = new ThreadLocal<String>(); |
| private transient ThreadLocal<Integer> port = new ThreadLocal<Integer>(); |
| private transient ThreadLocal<String> contextPath = new ThreadLocal<String>(); |
| private transient ThreadLocal<String> servletPath = new ThreadLocal<String>(); |
| private transient ThreadLocal<Boolean> secure = new ThreadLocal<Boolean>(); |
| |
| private transient ThreadLocal<QueryProperties> parameters = new ThreadLocal<QueryProperties>(); |
| private transient ThreadLocal<QueryProperties> requestHeaders = new ThreadLocal<QueryProperties>(); |
| private transient ThreadLocal<QueryProperties> responseHeaders = new ThreadLocal<QueryProperties>(); |
| |
| public static final String HTTP_PROTOCOL = "http"; |
| public static final String HTTPS_PROTOCOL = "https"; |
| public static final String URL_ENCODING = "UTF-8"; |
| |
| public static final String CONTENT_TYPE_HEADER = "Content-Type"; |
| public static final String CONTENT_LENGTH_HEADER = "Content-Length"; |
| public static final String LOCATION_HEADER = "Location"; |
| |
| /** |
| * Gets the host name that was requested. |
| */ |
| public String getHostname() { |
| return hostname.get(); |
| } |
| |
| /** |
| * Returns the Internet Protocol (IP) port number of the interface on which |
| * the request was received. |
| */ |
| public int getPort() { |
| return port.get(); |
| } |
| |
| /** |
| * Returns the portion of the request URL representing the context path. |
| */ |
| public String getContextPath() { |
| return contextPath.get(); |
| } |
| |
| /** |
| * Returns the portion of the request URL representing the servlet path. |
| */ |
| public String getServletPath() { |
| return servletPath.get(); |
| } |
| |
| /** |
| * Tells whether the request has been ecrypted over HTTPS. |
| */ |
| public boolean isSecure() { |
| return secure.get(); |
| } |
| |
| /** |
| * Returns the name of the HTTP protocol that the request is using. |
| */ |
| public String getProtocol() { |
| return isSecure() ? HTTPS_PROTOCOL : HTTP_PROTOCOL; |
| } |
| |
| /** |
| * Returns the location of this servlet. |
| */ |
| public URL getLocation() { |
| URL location; |
| try { |
| location = new URL(isSecure() ? HTTPS_PROTOCOL : 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. |
| */ |
| public QueryProperties getParameters() { |
| return parameters.get(); |
| } |
| |
| /** |
| * Returns the servlet's request header dictionary, which holds the HTTP |
| * request headers. |
| */ |
| public QueryProperties 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. |
| */ |
| public QueryProperties 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 |
| */ |
| 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 |
| */ |
| 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 |
| * @param path |
| * |
| * @throws QueryException |
| */ |
| protected void validate(Query.Method method, Path path) throws QueryException { |
| // No-op |
| } |
| |
| /** |
| * Handles an HTTP GET request. The default implementation throws an HTTP |
| * 405 query exception. |
| * |
| * @param path |
| * |
| * @return |
| * The result of the GET. |
| * |
| * @throws QueryException |
| */ |
| protected Object doGet(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 |
| * @param value |
| * |
| * @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 |
| */ |
| protected URL doPost(Path path, 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 |
| * @param value |
| * |
| * @return |
| * <tt>true</tt> if the operation resulted in the creation of a resource; |
| * <tt>false</tt>, otherwise. |
| * |
| * @throws QueryException |
| */ |
| protected boolean doPut(Path path, 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 |
| * |
| * @throws QueryException |
| */ |
| protected void doDelete(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 |
| * @param path |
| * |
| * @throws ServletException |
| */ |
| protected abstract Serializer<?> createSerializer(Query.Method method, Path path) |
| throws QueryException; |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| protected void service(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| try { |
| try { |
| URL url = new URL(request.getRequestURL().toString()); |
| hostname.set(url.getHost()); |
| port.set(request.getLocalPort()); |
| contextPath.set(request.getContextPath()); |
| servletPath.set(request.getServletPath()); |
| secure.set(url.getProtocol().equalsIgnoreCase(HTTPS_PROTOCOL)); |
| } catch (MalformedURLException exception) { |
| throw new ServletException(exception); |
| } |
| |
| parameters.set(new QueryProperties(true)); |
| requestHeaders.set(new QueryProperties(false)); |
| responseHeaders.set(new QueryProperties(false)); |
| |
| // Copy the query string into the arguments dictionary |
| String queryString = request.getQueryString(); |
| if (queryString != null) { |
| QueryProperties 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], URL_ENCODING); |
| String value = URLDecoder.decode((pair.length > 1) ? pair[1] : "", URL_ENCODING); |
| |
| parametersDictionary.put(key, value); |
| } |
| } |
| |
| // Copy the request headers into the request properties dictionary |
| QueryProperties requestHeaderDictionary = requestHeaders.get(); |
| Enumeration<String> headerNames = request.getHeaderNames(); |
| while (headerNames.hasMoreElements()) { |
| String headerName = headerNames.nextElement(); |
| String headerValue = request.getHeader(headerName); |
| |
| requestHeaderDictionary.put(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(HttpServletRequest request, 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 |
| FileOutputStream fileOutputStream = new FileOutputStream(tempFile); |
| try { |
| serializer.writeObject(result, fileOutputStream); |
| } catch (SerializationException exception) { |
| throw new ServletException(exception); |
| } finally { |
| fileOutputStream.close(); |
| } |
| |
| // Set the content length header |
| response.setHeader(CONTENT_LENGTH_HEADER, String.valueOf(tempFile.length())); |
| |
| // Write the contents of the file out to the response |
| FileInputStream fileInputStream = new FileInputStream(tempFile); |
| try { |
| byte[] buffer = new byte[1024]; |
| int nBytes; |
| do { |
| nBytes = fileInputStream.read(buffer); |
| if (nBytes > 0) { |
| responseOutputStream.write(buffer, 0, nBytes); |
| } |
| } while (nBytes != -1); |
| } finally { |
| fileInputStream.close(); |
| } |
| } else { |
| try { |
| serializer.writeObject(result, responseOutputStream); |
| } catch (SerializationException exception) { |
| throw new ServletException(exception); |
| } |
| } |
| |
| response.flushBuffer(); |
| } |
| } |
| |
| @Override |
| protected final void doPost(HttpServletRequest request, 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(LOCATION_HEADER, location.toString()); |
| } |
| |
| setResponseHeaders(response); |
| response.setContentLength(0); |
| } |
| } |
| |
| @Override |
| protected final void doPut(HttpServletRequest request, 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(HttpServletRequest request, 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(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); |
| response.flushBuffer(); |
| } |
| |
| @Override |
| protected final void doOptions(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); |
| response.flushBuffer(); |
| } |
| |
| @Override |
| protected final void doTrace(HttpServletRequest request, HttpServletResponse response) |
| throws IOException, ServletException { |
| response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); |
| response.flushBuffer(); |
| } |
| |
| private Path getPath(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(HttpServletResponse response) { |
| QueryProperties responseHeaderProperties = responseHeaders.get(); |
| |
| for (String key : responseHeaderProperties.keySet()) { |
| for (int i = 0, n = responseHeaderProperties.size(key); i < n; i++) { |
| response.addHeader(key, responseHeaderProperties.get(key, i)); |
| } |
| } |
| } |
| } |