| // 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.enterprise.adaptor; |
| |
| import com.sun.net.httpserver.HttpExchange; |
| import com.sun.net.httpserver.HttpHandler; |
| import com.sun.net.httpserver.HttpsServer; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.net.HttpURLConnection; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.charset.Charset; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.zip.GZIPOutputStream; |
| |
| abstract class AbstractHandler implements HttpHandler { |
| /** |
| * Attribute of {@link HttpExchange} that is {@code true} if the HTTP headers |
| * have already been sent for the exchange, and unset otherwise. |
| */ |
| public static final String ATTR_HEADERS_SENT |
| = AbstractHandler.class.getName() + ".headers-sent"; |
| |
| private static final Logger log |
| = Logger.getLogger(AbstractHandler.class.getName()); |
| // DateFormats are relatively expensive to create, and cannot be used from |
| // multiple threads |
| protected static ThreadLocal<DateFormat> dateFormat |
| = new ThreadLocal<DateFormat>() { |
| @Override |
| protected DateFormat initialValue() { |
| return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); |
| } |
| }; |
| /** |
| * When thread-local value is not {@code null}, signals that {@link #handle} |
| * should abort immediately with an error. This is a hack required because the |
| * HttpServer can't handle the Executor rejecting execution of a runnable. |
| */ |
| public static final ThreadLocal<Object> abortImmediately |
| = new ThreadLocal<Object>(); |
| |
| /** |
| * The hostname is sometimes needed to generate the correct DocId; in the case |
| * that it is needed and the host is an old HTTP/1.0 client, this value will |
| * be used. |
| */ |
| protected final String fallbackHostname; |
| /** |
| * Default encoding to encode simple response messages. |
| */ |
| protected final Charset defaultEncoding; |
| |
| /** |
| * @param fallbackHostname Fallback hostname in case we talk to an old HTTP |
| * client |
| * @param defaultEncoding Encoding to use when sending simple text responses |
| */ |
| protected AbstractHandler(String fallbackHostname, Charset defaultEncoding) { |
| this.fallbackHostname = fallbackHostname; |
| this.defaultEncoding = defaultEncoding; |
| } |
| |
| protected String getLoggableRequestHeaders(HttpExchange ex) { |
| StringBuilder sb = new StringBuilder(); |
| for (Map.Entry<String, List<String>> me |
| : ex.getRequestHeaders().entrySet()) { |
| for (String value : me.getValue()) { |
| sb.append(me.getKey()); |
| sb.append(": "); |
| sb.append(value); |
| sb.append(", "); |
| } |
| } |
| // Cut off trailing ", " |
| return sb.substring(0, sb.length() - 2); |
| } |
| |
| protected void logRequest(HttpExchange ex) { |
| if (log.isLoggable(Level.FINER)) { |
| log.log(Level.FINER, "Received {1} request to {0}. Headers: '{'{2}'}'", |
| new Object[] {ex.getRequestURI(), ex.getRequestMethod(), |
| getLoggableRequestHeaders(ex)}); |
| } |
| } |
| |
| /** |
| * Best-effort attempt to reform the identical URI the client used to |
| * contact the server. |
| */ |
| protected URI getRequestUri(HttpExchange ex) { |
| String host = ex.getRequestHeaders().getFirst("Host"); |
| if (host == null) { |
| // Client must be using HTTP/1.0 |
| host = fallbackHostname; |
| } |
| String protocol = (ex.getHttpContext().getServer() instanceof HttpsServer) |
| ? "https" : "http"; |
| URI base; |
| try { |
| base = new URI(protocol, host, "/", null, null); |
| } catch (URISyntaxException e) { |
| throw new IllegalStateException(e); |
| } |
| URI requestedUri = ex.getRequestURI(); |
| // If uri is already absolute (e.g., a proxy is involved), then this |
| // does nothing, otherwise it resolves the URI for us based on who we |
| // think we are |
| requestedUri = base.resolve(requestedUri); |
| log.log(Level.FINER, "Resolved original URI to: {0}", requestedUri); |
| return requestedUri; |
| } |
| |
| /** |
| * Sends response to GSA. Should only be used when the request method is |
| * HEAD. |
| */ |
| protected void respondToHead(HttpExchange ex, int code, String contentType) |
| throws IOException { |
| ex.getResponseHeaders().set("Transfer-Encoding", "chunked"); |
| respond(ex, code, contentType, (byte[]) null); |
| } |
| |
| /** |
| * Sends cheaply-generated response message to GSA. This is intended for use |
| * with pre-build, canned messages. It automatically handles not sending the |
| * actual content when the request method is HEAD. If the content requires |
| * a moderate amount of work to produce, then you should manually call |
| * {@link #respond} or {@link #respondToHead} depending on the situation. |
| */ |
| protected void cannedRespond(HttpExchange ex, int code, Translation response) |
| throws IOException { |
| // TODO(ejona): use exchange to decide on response language |
| cannedRespond(ex, code, "text/plain", response.toString()); |
| } |
| |
| /** |
| * Sends cheaply-generated response message to GSA. This is intended for use |
| * with pre-build, canned messages. It automatically handles not sending the |
| * actual content when the request method is HEAD. If the content requires |
| * a moderate amount of work to produce, then you should manually call |
| * {@link #respond} or {@link #respondToHead} depending on the situation. |
| */ |
| protected void cannedRespond(HttpExchange ex, int code, Translation response, |
| Object... params) throws IOException { |
| // TODO(ejona): use exchange to decide on response language |
| cannedRespond(ex, code, "text/plain", response.toString(params)); |
| } |
| |
| private void cannedRespond(HttpExchange ex, int code, String contentType, |
| String response) throws IOException { |
| if ("HEAD".equals(ex.getRequestMethod())) { |
| respondToHead(ex, code, contentType); |
| } else { |
| respond(ex, code, contentType, response.getBytes(defaultEncoding)); |
| } |
| } |
| |
| /** |
| * Sends headers and configures {@code ex} for (possibly) sending content. |
| * Completing the request is the caller's responsibility. |
| */ |
| protected void startResponse(HttpExchange ex, int code, String contentType, |
| boolean hasBody) throws IOException { |
| log.finest("Starting response"); |
| if (contentType != null) { |
| ex.getResponseHeaders().set("Content-Type", contentType); |
| } |
| ex.setAttribute(ATTR_HEADERS_SENT, true); |
| if (!hasBody) { |
| // No body. Required for HEAD requests |
| ex.sendResponseHeaders(code, -1); |
| } else { |
| // Chuncked encoding |
| ex.sendResponseHeaders(code, 0); |
| // Check to see if enableCompressionIfSupported was called |
| if ("gzip".equals(ex.getResponseHeaders().getFirst("Content-Encoding"))) { |
| // Creating the GZIPOutputStream must happen after sendResponseHeaders |
| // since the constructor writes data to the provided OutputStream |
| ex.setStreams(null, new GZIPOutputStream(ex.getResponseBody())); |
| } |
| } |
| } |
| |
| /** |
| * Sends response to GSA. Should not be used directly if the request method |
| * is HEAD. |
| */ |
| protected void respond(HttpExchange ex, int code, String contentType, |
| byte response[]) throws IOException { |
| startResponse(ex, code, contentType, response != null); |
| if (response != null) { |
| OutputStream responseBody = ex.getResponseBody(); |
| log.finest("before writing response"); |
| responseBody.write(response); |
| responseBody.flush(); |
| // This shouldn't be needed, but without it one developer had trouble |
| responseBody.close(); |
| log.finest("after writing response"); |
| } |
| ex.close(); |
| log.finest("after closing exchange"); |
| } |
| |
| /** |
| * Redirect client to {@code location}. The client should retrieve the |
| * referred location via GET, independent of the method of this request. |
| */ |
| public void sendRedirect(HttpExchange ex, URI location) |
| throws IOException { |
| ex.getResponseHeaders().set("Location", location.toString()); |
| respond(ex, HttpURLConnection.HTTP_SEE_OTHER, null, null); |
| } |
| |
| /** |
| * If the client supports it, set the correct headers and make {@link |
| * #respond} provide GZIPed response data to the client. |
| */ |
| protected void enableCompressionIfSupported(HttpExchange ex) |
| throws IOException { |
| String encodingList = ex.getRequestHeaders().getFirst("Accept-Encoding"); |
| if (encodingList == null) { |
| return; |
| } |
| Collection<String> encodings = Arrays.asList(encodingList.split(",")); |
| if (encodings.contains("gzip")) { |
| log.finer("Enabling gzip compression for response"); |
| ex.getResponseHeaders().set("Content-Encoding", "gzip"); |
| } |
| } |
| |
| /** |
| * Retrieves and parses the If-Modified-Since from the request, returning null |
| * if there was no such header or there was an error. |
| */ |
| protected static Date getIfModifiedSince(HttpExchange ex) { |
| String ifModifiedSince |
| = ex.getRequestHeaders().getFirst("If-Modified-Since"); |
| if (ifModifiedSince == null) { |
| return null; |
| } |
| try { |
| return dateFormat.get().parse(ifModifiedSince); |
| } catch (java.text.ParseException e) { |
| log.log(Level.WARNING, "Exception when parsing ifModifiedSince", e); |
| // Ignore and act like it wasn't present |
| return null; |
| } |
| } |
| |
| protected void setLastModified(HttpExchange ex, Date lastModified) { |
| ex.getResponseHeaders().set("Last-Modified", |
| dateFormat.get().format(lastModified)); |
| |
| } |
| |
| protected abstract void meteredHandle(HttpExchange ex) throws IOException; |
| |
| /** |
| * Performs entry logging, calls {@link #meteredHandle}, and performs exit |
| * logging. Also logs and handles exceptions. |
| */ |
| public void handle(HttpExchange ex) throws IOException { |
| // Checking abortImmediately is part of a hack to immediately reject clients |
| // when the work queue grows too long. |
| if (abortImmediately.get() != null) { |
| throw new IOException("Too many clients"); |
| } |
| try { |
| log.fine("beginning"); |
| logRequest(ex); |
| log.log(Level.FINE, "Processing request with {0}", |
| this.getClass().getName()); |
| ex.getResponseHeaders().set("Date", dateFormat.get().format(new Date())); |
| meteredHandle(ex); |
| } catch (Exception e) { |
| Boolean headersSent = (Boolean) ex.getAttribute(ATTR_HEADERS_SENT); |
| if (headersSent == null) { |
| headersSent = false; |
| } |
| log.log(Level.WARNING, "Unexpected exception during request", e); |
| if (headersSent) { |
| // The headers have already been sent, so all we can do is throw the |
| // exception up and allow the server to kill the connection. |
| throw new RuntimeException(e); |
| } else { |
| cannedRespond(ex, HttpURLConnection.HTTP_INTERNAL_ERROR, |
| Translation.HTTP_INTERNAL_ERROR); |
| } |
| } finally { |
| log.fine("ending"); |
| } |
| } |
| } |