/*
* 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.
*
* Copyright 2012 The Netty Project
*
* Contents have been modified
*/
package com.addthis.hydra.query.web;
import javax.activation.MimetypesFileTypeMap;
import java.io.IOException;
import java.io.Writer;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import static io.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.DATE;
import static io.netty.handler.codec.http.HttpHeaders.Names.EXPIRES;
import static io.netty.handler.codec.http.HttpHeaders.Names.LAST_MODIFIED;
import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.util.CharsetUtil;
/**
* A simple handler that serves incoming HTTP requests to send their respective
* HTTP responses. It also implements {@code 'If-Modified-Since'} header to
* take advantage of browser cache, as described in
* <a href="http://tools.ietf.org/html/rfc2616#section-14.25">RFC 2616</a>.
* <p/>
* <h3>How Browser Caching Works</h3>
* <p/>
* Web browser caching works with HTTP headers as illustrated by the following
* sample:
* <ol>
* <li>Request #1 returns the content of {@code /file1.txt}.</li>
* <li>Contents of {@code /file1.txt} is cached by the browser.</li>
* <li>Request #2 for {@code /file1.txt} does return the contents of the
* file again. Rather, a 304 Not Modified is returned. This tells the
* browser to use the contents stored in its cache.</li>
* <li>The server knows the file has not been modified because the
* {@code If-Modified-Since} date is the same as the file's last
* modified date.</li>
* </ol>
* <p/>
* <pre>
* Request #1 Headers
* ===================
* GET /file1.txt HTTP/1.1
*
* Response #1 Headers
* ===================
* HTTP/1.1 200 OK
* Date: Tue, 01 Mar 2011 22:44:26 GMT
* Last-Modified: Wed, 30 Jun 2010 21:36:48 GMT
* Expires: Tue, 01 Mar 2012 22:44:26 GMT
* Cache-Control: private, max-age=31536000
*
* Request #2 Headers
* ===================
* GET /file1.txt HTTP/1.1
* If-Modified-Since: Wed, 30 Jun 2010 21:36:48 GMT
*
* Response #2 Headers
* ===================
* HTTP/1.1 304 Not Modified
* Date: Tue, 01 Mar 2011 22:44:28 GMT
*
* </pre>
*/
public class HttpUtils {
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
static final int HTTP_CACHE_SECONDS = 60;
private HttpUtils() {
}
static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
log.trace("issuing redirect to {}", newUri);
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
// Close the connection as soon as the redirect message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
public static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(
HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
log.trace("issuing error of {}", status);
// Close the connection as soon as the error message is sent.
ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* When file timestamp is the same as what the browser is sending up, send a "304 Not Modified"
*
* @param ctx Context
*/
static void sendNotModified(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
setDateHeader(response);
// Close the connection as soon as the error message is sent.
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
/**
* Sets the Date header for the HTTP response
*
* @param response HTTP response
*/
static void setDateHeader(FullHttpResponse response) {
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
Calendar time = new GregorianCalendar();
response.headers().set(DATE, dateFormatter.format(time.getTime()));
}
/**
* Sets the Date and Cache headers for the HTTP Response
*
* @param response HTTP response
* @param fileToCache file to extract content type
*/
static void setDateAndCacheHeaders(HttpResponse response, Path fileToCache) throws IOException {
SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE));
// Date header
Calendar time = new GregorianCalendar();
response.headers().set(DATE, dateFormatter.format(time.getTime()));
// Add cache headers
time.add(Calendar.SECOND, HTTP_CACHE_SECONDS);
response.headers().set(EXPIRES, dateFormatter.format(time.getTime()));
response.headers().set(CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS);
response.headers().set(
LAST_MODIFIED, dateFormatter.format(new Date(Files.getLastModifiedTime(fileToCache).toMillis())));
}
/**
* Sets the content type header for the HTTP Response
*
* @param response HTTP response
* @param file file to extract content type
*/
static void setContentTypeHeader(HttpResponse response, Path file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE, mimeTypesMap.getContentType(file.toString()));
}
static void setContentTypeHeader(HttpResponse response, String type) {
response.headers().set(CONTENT_TYPE, type);
}
static HttpResponse startResponse(Writer writer) throws IOException {
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK);
setContentTypeHeader(response, "application/json; charset=utf-8");
return response;
}
}