Java tutorial
/** * * Copyright 2010, Nicolas Leroux. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * * User: nicolas * Date: Feb 25, 2010 * */ package play.modules.netty; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.*; import org.jboss.netty.handler.codec.http.*; import org.jboss.netty.handler.stream.ChunkedFile; import org.jboss.netty.handler.stream.ChunkedStream; import play.Invoker; import play.Logger; import play.Play; import play.PlayPlugin; import play.exceptions.PlayException; import play.exceptions.UnexpectedException; import play.i18n.Messages; import play.libs.MimeTypes; import play.mvc.ActionInvoker; import play.mvc.Http; import play.mvc.Http.Request; import play.mvc.Http.Response; import play.mvc.Router; import play.mvc.Scope; import play.mvc.results.NotFound; import play.mvc.results.RenderStatic; import play.templates.JavaExtensions; import play.templates.TemplateLoader; import play.utils.Utils; import play.vfs.VirtualFile; import java.io.*; import java.net.InetSocketAddress; import java.net.URLDecoder; import java.net.URLEncoder; import java.text.ParseException; import java.util.*; import play.data.validation.Validation; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; public class PlayHandler extends SimpleChannelUpstreamHandler { private final static String signature = "Play! Framework;" + Play.version + ";" + Play.mode.name().toLowerCase(); @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Logger.trace("messageReceived: begin"); final Object msg = e.getMessage(); if (msg instanceof HttpRequest) { final HttpRequest nettyRequest = (HttpRequest) msg; try { final Request request = parseRequest(ctx, nettyRequest); final Response response = new Response(); Http.Response.current.set(response); response.out = new ByteArrayOutputStream(); boolean raw = false; for (PlayPlugin plugin : Play.plugins) { if (plugin.rawInvocation(request, response)) { raw = true; break; } } if (raw) { copyResponse(ctx, request, response, nettyRequest); } else { Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e)); } } catch (Exception ex) { serve500(ex, ctx, nettyRequest); } } Logger.trace("messageReceived: end"); } private static Map<String, RenderStatic> staticPathsCache = new HashMap<String, RenderStatic>(); public class NettyInvocation extends Invoker.Invocation { private final ChannelHandlerContext ctx; private final Request request; private final Response response; private final HttpRequest nettyRequest; private final MessageEvent e; public NettyInvocation(Request request, Response response, ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent e) { this.ctx = ctx; this.request = request; this.response = response; this.nettyRequest = nettyRequest; this.e = e; } @Override public boolean init() { Logger.trace("init: begin"); Request.current.set(request); Response.current.set(response); // Patch favicon.ico if (!request.path.equals("/favicon.ico")) { super.init(); } if (Play.mode == Play.Mode.PROD && staticPathsCache.containsKey(request.path)) { RenderStatic rs = null; synchronized (staticPathsCache) { rs = staticPathsCache.get(request.path); } serveStatic(rs, ctx, request, response, nettyRequest, e); Logger.trace("init: end false"); return false; } try { Router.routeOnlyStatic(request); } catch (NotFound e) { serve404(e, ctx, request, nettyRequest); Logger.trace("init: end false"); return false; } catch (RenderStatic e) { if (Play.mode == Play.Mode.PROD) { synchronized (staticPathsCache) { staticPathsCache.put(request.path, e); } } serveStatic(e, ctx, request, response, nettyRequest, this.e); Logger.trace("init: end false"); return false; } Logger.trace("init: end true"); return true; } @Override public void run() { try { Logger.trace("run: begin"); super.run(); } catch (Exception e) { serve500(e, ctx, nettyRequest); } Logger.trace("run: end"); } @Override public void execute() throws Exception { if (!ctx.getChannel().isConnected()) { try { ctx.getChannel().close(); } catch (Throwable e) { // Ignore } return; } // Check the exceeded size before re rendering so we can render the error if the size is exceeded saveExceededSizeError(nettyRequest, request, response); ActionInvoker.invoke(request, response); copyResponse(ctx, request, response, nettyRequest); Logger.trace("execute: end"); } } void saveExceededSizeError(HttpRequest nettyRequest, Request request, Response response) { String warning = nettyRequest.getHeader(HttpHeaders.Names.WARNING); String length = nettyRequest.getHeader(HttpHeaders.Names.CONTENT_LENGTH); if (warning != null) { try { StringBuilder error = new StringBuilder(); error.append("\u0000"); // Cannot put warning which is play.module.netty.content.length.exceeded // as Key as it will result error when printing error error.append("play.module.netty.maxContentLength"); error.append(":"); String size = null; try { size = JavaExtensions.formatSize(Long.parseLong(length)); } catch (Exception e) { size = length + " bytes"; } error.append(Messages.get(warning, size)); error.append("\u0001"); error.append(size); error.append("\u0000"); if (request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS") != null && request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value != null) { error.append(request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value); } String errorData = URLEncoder.encode(error.toString(), "utf-8"); Http.Cookie c = new Http.Cookie(); c.value = errorData; c.name = Scope.COOKIE_PREFIX + "_ERRORS"; request.cookies.put(Scope.COOKIE_PREFIX + "_ERRORS", c); } catch (Exception e) { throw new UnexpectedException("Error serialization problem", e); } } } protected static void addToResponse(Response response, HttpResponse nettyResponse) { Map<String, Http.Header> headers = response.headers; for (Map.Entry<String, Http.Header> entry : headers.entrySet()) { Http.Header hd = entry.getValue(); for (String value : hd.values) { nettyResponse.setHeader(entry.getKey(), value); } } Map<String, Http.Cookie> cookies = response.cookies; for (Http.Cookie cookie : cookies.values()) { CookieEncoder encoder = new CookieEncoder(true); Cookie c = new DefaultCookie(cookie.name, cookie.value); c.setSecure(cookie.secure); c.setPath(cookie.path); if (cookie.domain != null) { c.setDomain(cookie.domain); } if (cookie.maxAge != null) { c.setMaxAge(cookie.maxAge); } encoder.addCookie(c); nettyResponse.addHeader(SET_COOKIE, encoder.encode()); } if (!response.headers.containsKey(CACHE_CONTROL)) { nettyResponse.setHeader(CACHE_CONTROL, "no-cache"); } } protected static void writeResponse(ChannelHandlerContext ctx, Response response, HttpResponse nettyResponse, HttpRequest nettyRequest) { Logger.trace("writeResponse: begin"); byte[] content = null; final boolean keepAlive = isKeepAlive(nettyRequest); if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) { content = new byte[0]; } else { content = response.out.toByteArray(); } ChannelBuffer buf = ChannelBuffers.copiedBuffer(content); nettyResponse.setContent(buf); if (keepAlive) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("writeResponse: content length [" + response.out.size() + "]"); setContentLength(nettyResponse, response.out.size()); } ChannelFuture f = ctx.getChannel().write(nettyResponse); // Decide whether to close the connection or not. if (!keepAlive) { // Close the connection when the whole content is written out. f.addListener(ChannelFutureListener.CLOSE); } Logger.trace("writeResponse: end"); } public static void copyResponse(ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest) throws Exception { Logger.trace("copyResponse: begin"); //response.out.flush(); // Decide whether to close the connection or not. HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status)); nettyResponse.setHeader(SERVER, signature); if (response.contentType != null) { nettyResponse.setHeader(CONTENT_TYPE, response.contentType + (response.contentType.startsWith("text/") && !response.contentType.contains("charset") ? "; charset=utf-8" : "")); } else { nettyResponse.setHeader(CONTENT_TYPE, "text/plain; charset=utf-8"); } addToResponse(response, nettyResponse); final Object obj = response.direct; File file = null; InputStream is = null; if (obj instanceof File) { file = (File) obj; } else if (obj instanceof InputStream) { is = (InputStream) obj; } final boolean keepAlive = isKeepAlive(nettyRequest); if (file != null && file.isFile()) { try { nettyResponse = addEtag(nettyRequest, nettyResponse, file); if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { Channel ch = ctx.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { nettyResponse.setHeader(CONTENT_TYPE, MimeTypes.getContentType(file.getName(), "text/plain")); RandomAccessFile raf = new RandomAccessFile(file, "r"); long fileLength = raf.length(); if (keepAlive) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("file length is [" + fileLength + "]"); setContentLength(nettyResponse, fileLength); } Channel ch = ctx.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); // Write the content. // If it is not a HEAD if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) { writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); } if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } } catch (Exception e) { throw e; } } else if (is != null) { ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) { writeFuture = ctx.getChannel().write(new ChunkedStream(is)); } if (!keepAlive) { writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { writeResponse(ctx, response, nettyResponse, nettyRequest); } Logger.trace("copyResponse: end"); } static String getRemoteIPAddress(ChannelHandlerContext ctx) { String fullAddress = ((InetSocketAddress) ctx.getChannel().getRemoteAddress()).getAddress() .getHostAddress(); // Address resolves to /x.x.x.x:zzzz we only want x.x.x.x if (fullAddress.startsWith("/")) { fullAddress = fullAddress.substring(1); } int i = fullAddress.indexOf(":"); if (i != -1) { fullAddress = fullAddress.substring(0, i); } return fullAddress; } public static Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) throws Exception { Logger.trace("parseRequest: begin"); Logger.trace("parseRequest: URI = " + nettyRequest.getUri()); int index = nettyRequest.getUri().indexOf("?"); String querystring = ""; String path = URLDecoder.decode(nettyRequest.getUri(), "UTF-8"); if (index != -1) { path = URLDecoder.decode(nettyRequest.getUri().substring(0, index), "UTF-8"); querystring = nettyRequest.getUri().substring(index + 1); } final Request request = new Request(); request.remoteAddress = getRemoteIPAddress(ctx); request.method = nettyRequest.getMethod().getName(); request.path = path; request.querystring = querystring; final String contentType = nettyRequest.getHeader(CONTENT_TYPE); if (contentType != null) { request.contentType = contentType.split(";")[0].trim().toLowerCase(); } else { request.contentType = "text/html"; } if (nettyRequest.getHeader("X-HTTP-Method-Override") != null) { request.method = nettyRequest.getHeader("X-HTTP-Method-Override").intern(); } ChannelBuffer b = nettyRequest.getContent(); if (b instanceof FileChannelBuffer) { FileChannelBuffer buffer = (FileChannelBuffer) b; // An error occurred Integer max = Integer .valueOf(Play.configuration.getProperty("play.module.netty.maxContentLength", "-1")); request.body = buffer.getInputStream(); if (!(max == -1 || request.body.available() < max)) { request.body = new ByteArrayInputStream(new byte[0]); } } else { ByteArrayOutputStream out = new ByteArrayOutputStream(); IOUtils.copy(new ChannelBufferInputStream(b), out); byte[] n = out.toByteArray(); request.body = new ByteArrayInputStream(n); } request.url = nettyRequest.getUri(); request.host = nettyRequest.getHeader(HOST); if (request.host.contains(":")) { final String[] host = request.host.split(":"); request.port = Integer.parseInt(host[1]); request.domain = host[0]; } else { request.port = 80; request.domain = request.host; } if (Play.configuration.containsKey("XForwardedSupport") && nettyRequest.getHeader("X-Forwarded-For") != null) { if (!Arrays.asList(Play.configuration.getProperty("XForwardedSupport", "127.0.0.1").split(",")) .contains(request.remoteAddress)) { throw new RuntimeException("This proxy request is not authorized: " + request.remoteAddress); } else { request.secure = ("https".equals(Play.configuration.get("XForwardedProto")) || "https".equals(nettyRequest.getHeader("X-Forwarded-Proto")) || "on".equals(nettyRequest.getHeader("X-Forwarded-Ssl"))); if (Play.configuration.containsKey("XForwardedHost")) { request.host = (String) Play.configuration.get("XForwardedHost"); } else if (nettyRequest.getHeader("X-Forwarded-Host") != null) { request.host = nettyRequest.getHeader("X-Forwarded-Host"); } if (nettyRequest.getHeader("X-Forwarded-For") != null) { request.remoteAddress = nettyRequest.getHeader("X-Forwarded-For"); } } } addToRequest(nettyRequest, request); request.resolveFormat(); request._init(); Logger.trace("parseRequest: end"); return request; } protected static void addToRequest(HttpRequest nettyRequest, Request request) { for (String key : nettyRequest.getHeaderNames()) { Http.Header hd = new Http.Header(); hd.name = key.toLowerCase(); hd.values = new ArrayList<String>(); for (String next : nettyRequest.getHeaders(key)) { hd.values.add(next); } request.headers.put(hd.name, hd); } String value = nettyRequest.getHeader(COOKIE); if (value != null) { Set<Cookie> cookies = new CookieDecoder().decode(value); if (cookies != null) { for (Cookie cookie : cookies) { Http.Cookie playCookie = new Http.Cookie(); playCookie.name = cookie.getName(); playCookie.path = cookie.getPath(); playCookie.domain = cookie.getDomain(); playCookie.secure = cookie.isSecure(); playCookie.value = cookie.getValue(); request.cookies.put(playCookie.name, playCookie); } } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { //e.getCause().printStackTrace(); e.getChannel().close(); } public static void serve404(NotFound e, ChannelHandlerContext ctx, Request request, HttpRequest nettyRequest) { Logger.trace("serve404: begin"); HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); nettyResponse.setHeader(SERVER, signature); nettyResponse.setHeader(CONTENT_TYPE, "text/html"); Map<String, Object> binding = getBindingForErrors(e, false); String format = Request.current().format; if (format == null || ("XMLHttpRequest".equals(request.headers.get("x-requested-with")) && "html".equals(format))) { format = "txt"; } nettyResponse.setHeader(CONTENT_TYPE, (MimeTypes.getContentType("404." + format, "text/plain"))); String errorHtml = TemplateLoader.load("errors/404." + format).render(binding); try { ChannelBuffer buf = ChannelBuffers.copiedBuffer(errorHtml.getBytes("utf-8")); nettyResponse.setContent(buf); ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); writeFuture.addListener(ChannelFutureListener.CLOSE); } catch (UnsupportedEncodingException fex) { Logger.error(fex, "(utf-8 ?)"); } Logger.trace("serve404: end"); } protected static Map<String, Object> getBindingForErrors(Exception e, boolean isError) { Map<String, Object> binding = new HashMap<String, Object>(); if (!isError) { binding.put("result", e); } else { binding.put("exception", e); } binding.put("session", Scope.Session.current()); binding.put("request", Http.Request.current()); binding.put("flash", Scope.Flash.current()); binding.put("params", Scope.Params.current()); binding.put("play", new Play()); try { binding.put("errors", Validation.errors()); } catch (Exception ex) { //Logger.error(ex, "Error when getting Validation errors"); } return binding; } // TODO: add request and response as parameter public static void serve500(Exception e, ChannelHandlerContext ctx, HttpRequest nettyRequest) { Logger.trace("serve500: begin"); HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR); nettyResponse.setHeader(SERVER, signature); Request request = Request.current(); Response response = Response.current(); try { if (!(e instanceof PlayException)) { e = new play.exceptions.UnexpectedException(e); } // Flush some cookies try { Map<String, Http.Cookie> cookies = response.cookies; for (Http.Cookie cookie : cookies.values()) { CookieEncoder encoder = new CookieEncoder(true); Cookie c = new DefaultCookie(cookie.name, cookie.value); c.setSecure(cookie.secure); c.setPath(cookie.path); if (cookie.domain != null) { c.setDomain(cookie.domain); } if (cookie.maxAge != null) { c.setMaxAge(cookie.maxAge); } encoder.addCookie(c); nettyResponse.addHeader(SET_COOKIE, encoder.encode()); //nettyResponse.setHeader("Cookie", encoder.encode()); } } catch (Exception exx) { Logger.error(e, "Trying to flush cookies"); // humm ? } Map<String, Object> binding = getBindingForErrors(e, true); String format = request.format; if (format == null || ("XMLHttpRequest".equals(request.headers.get("x-requested-with")) && "html".equals(format))) { format = "txt"; } nettyResponse.setHeader("Content-Type", (MimeTypes.getContentType("500." + format, "text/plain"))); try { String errorHtml = TemplateLoader.load("errors/500." + format).render(binding); ChannelBuffer buf = ChannelBuffers.copiedBuffer(errorHtml.getBytes("utf-8")); nettyResponse.setContent(buf); ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); writeFuture.addListener(ChannelFutureListener.CLOSE); Logger.error(e, "Internal Server Error (500) for request %s", request.method + " " + request.url); } catch (Throwable ex) { Logger.error(e, "Internal Server Error (500) for request %s", request.method + " " + request.url); Logger.error(ex, "Error during the 500 response generation"); try { ChannelBuffer buf = ChannelBuffers .copiedBuffer("Internal Error (check logs)".getBytes("utf-8")); nettyResponse.setContent(buf); ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); writeFuture.addListener(ChannelFutureListener.CLOSE); } catch (UnsupportedEncodingException fex) { Logger.error(fex, "(utf-8 ?)"); } } } catch (Throwable exxx) { try { ChannelBuffer buf = ChannelBuffers.copiedBuffer("Internal Error (check logs)".getBytes("utf-8")); nettyResponse.setContent(buf); ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse); writeFuture.addListener(ChannelFutureListener.CLOSE); } catch (Exception fex) { Logger.error(fex, "(utf-8 ?)"); } if (exxx instanceof RuntimeException) { throw (RuntimeException) exxx; } throw new RuntimeException(exxx); } Logger.trace("serve500: end"); } public static void serveStatic(RenderStatic renderStatic, ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest, MessageEvent e) { Logger.trace("serveStatic: begin"); HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status)); nettyResponse.setHeader("Server", signature); try { VirtualFile file = Play.getVirtualFile(renderStatic.file); if (file != null && file.exists() && file.isDirectory()) { file = file.child("index.html"); if (file != null) { renderStatic.file = file.relativePath(); } } if ((file == null || !file.exists())) { serve404(new NotFound("The file " + renderStatic.file + " does not exist"), ctx, request, nettyRequest); } else { boolean raw = false; for (PlayPlugin plugin : Play.plugins) { if (plugin.serveStatic(file, Request.current(), Response.current())) { raw = true; break; } } if (raw) { copyResponse(ctx, request, response, nettyRequest); } else { final File localFile = file.getRealFile(); final boolean keepAlive = isKeepAlive(nettyRequest); nettyResponse = addEtag(nettyRequest, nettyResponse, localFile); if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { Channel ch = e.getChannel(); // Write the initial line and the header. ChannelFuture writeFuture = ch.write(nettyResponse); if (!keepAlive) { // Write the content. writeFuture.addListener(ChannelFutureListener.CLOSE); } } else { RandomAccessFile raf; raf = new RandomAccessFile(localFile, "r"); long fileLength = raf.length(); Logger.trace("keep alive " + keepAlive); Logger.trace( "content type " + (MimeTypes.getContentType(localFile.getName(), "text/plain"))); if (keepAlive && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) { // Add 'Content-Length' header only for a keep-alive connection. Logger.trace("file length " + fileLength); setContentLength(nettyResponse, fileLength); } nettyResponse.setHeader(CONTENT_TYPE, (MimeTypes.getContentType(localFile.getName(), "text/plain"))); Channel ch = e.getChannel(); // Write the initial line and the header. ch.write(nettyResponse); // Write the content. ChannelFuture writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); if (!keepAlive) { // Close the connection when the whole content is written out. writeFuture.addListener(ChannelFutureListener.CLOSE); } } } } } catch (Exception ez) { Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url); try { ChannelBuffer buf = ChannelBuffers.copiedBuffer("Internal Error (check logs)".getBytes("utf-8")); nettyResponse.setContent(buf); ChannelFuture future = ctx.getChannel().write(nettyResponse); future.addListener(ChannelFutureListener.CLOSE); } catch (Exception ex) { Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url); } } Logger.trace("serveStatic: end"); } public static boolean isModified(String etag, long last, HttpRequest nettyRequest) { if (nettyRequest.containsHeader(IF_NONE_MATCH)) { final String browserEtag = nettyRequest.getHeader(IF_NONE_MATCH); if (browserEtag.equals(etag)) { return false; } return true; } if (nettyRequest.containsHeader(IF_MODIFIED_SINCE)) { final String ifModifiedSince = nettyRequest.getHeader(IF_MODIFIED_SINCE); if (!StringUtils.isEmpty(ifModifiedSince)) { try { Date browserDate = Utils.getHttpDateFormatter().parse(ifModifiedSince); if (browserDate.getTime() >= last) { return false; } } catch (ParseException ex) { Logger.warn("Can't parse HTTP date", ex); } return true; } } return true; } private static HttpResponse addEtag(HttpRequest nettyRequest, HttpResponse httpResponse, File file) { if (Play.mode == Play.Mode.DEV) { httpResponse.setHeader(CACHE_CONTROL, "no-cache"); } else { String maxAge = Play.configuration.getProperty("http.cacheControl", "3600"); if (maxAge.equals("0")) { httpResponse.setHeader(CACHE_CONTROL, "no-cache"); } else { httpResponse.setHeader(CACHE_CONTROL, "max-age=" + maxAge); } } boolean useEtag = Play.configuration.getProperty("http.useETag", "true").equals("true"); long last = file.lastModified(); final String etag = "\"" + last + "-" + file.hashCode() + "\""; if (!isModified(etag, last, nettyRequest)) { if (nettyRequest.getMethod().equals(HttpMethod.GET)) { httpResponse.setStatus(HttpResponseStatus.NOT_MODIFIED); } if (useEtag) { httpResponse.setHeader(ETAG, etag); } } else { httpResponse.setHeader(LAST_MODIFIED, Utils.getHttpDateFormatter().format(new Date(last))); if (useEtag) { httpResponse.setHeader(ETAG, etag); } } return httpResponse; } public static boolean isKeepAlive(HttpMessage message) { return HttpHeaders.isKeepAlive(message) && message.getProtocolVersion().equals(HttpVersion.HTTP_1_1); } public static void setContentLength(HttpMessage message, long contentLength) { message.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(contentLength)); } }