Skip to content

Instantly share code, notes, and snippets.

@kritzikratzi
Created June 18, 2015 10:32
Show Gist options
  • Save kritzikratzi/e301030778289401f607 to your computer and use it in GitHub Desktop.
Save kritzikratzi/e301030778289401f607 to your computer and use it in GitHub Desktop.
@With({ETagAdder.class, GZipCompression.class})
public class Application extends Controller {
@CacheFor(value="5min")
public static void doSomething(){
//...
render();
}
}
package controllers.modifiers;
import java.lang.reflect.Method;
import org.apache.commons.codec.digest.DigestUtils;
import play.Play;
import play.cache.Cache;
import play.cache.CacheFor;
import play.libs.Time;
import play.mvc.Before;
import play.mvc.Controller;
import play.mvc.Finally;
import play.mvc.Http.Header;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.mvc.results.Result;
/**
* Adds an etag to cached methods
* @author hansi
*
*/
public class ETagAdder extends Controller{
private static final ThreadLocal<Boolean> shouldAddEtag = new ThreadLocal<>();
private static final ThreadLocal<String> etag = new ThreadLocal<>();
private static final ThreadLocal<String> duration = new ThreadLocal<>();
@Before
public static void dieEarly(){
CacheFor cacheFor = getActionAnnotation(CacheFor.class);
Method actionMethod = request.invokedMethod;
boolean canCache =
cacheFor != null && // @CacheFor annotation is present
Play.mode.isDev() // production mode
;
shouldAddEtag.set(false);
if( cacheFor == null ) return;
String cacheKey = actionMethod.getAnnotation(CacheFor.class).id();
if ("".equals(cacheKey)) {
cacheKey = "gzip_urlcache:" + request.url; // req.querystring is already included in req.url
}
else{
cacheKey = "gzip_" + cacheKey;
}
final String serverEtag = Cache.get(cacheKey+".etag", String.class);
Header clientEtag = request.headers.get("if-none-match");
// we are caching and the etag looks fine? great!
if( canCache && serverEtag != null && clientEtag != null && clientEtag.values.size() > 0 && serverEtag.equals(clientEtag.values.get(0))){
throw new Result() {
@Override
public void apply(Request request, Response response) {
response.status = 304;
response.setHeader( "ETag", serverEtag );
}
};
}
else if( canCache ){
//well, let's create a new etag for this thing...
if( serverEtag == null ){
etag.set( DigestUtils.md5Hex( cacheKey + System.currentTimeMillis()) );
Cache.set(cacheKey+".etag", etag.get(), cacheFor.value() );
}
else{
etag.set(serverEtag);
}
duration.set(cacheFor.value());
shouldAddEtag.set(true);
}
}
@Finally
public static void addETag(){
if( shouldAddEtag.get() ){
response.cacheFor( etag.get(), duration.get(), System.currentTimeMillis() );
}
}
}
package controllers.modifiers;
import play.*;
import play.cache.Cache;
import play.cache.CacheFor;
import play.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer;
import play.exceptions.UnexpectedException;
import play.mvc.*;
import play.mvc.Http.Header;
import play.mvc.Http.Request;
import play.mvc.Http.Response;
import play.mvc.results.RenderBinary;
import play.mvc.results.RenderHtml;
import play.mvc.results.RenderTemplate;
import play.mvc.results.Result;
import play.templates.Template;
import play.templates.TemplateLoader;
import java.io.*;
import java.lang.reflect.Method;
import java.util.*;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.codec.digest.DigestUtils;
// based heavily found here: https://gist.github.com/engintekin/1317626
// and the actioninvoker source code: https://github.com/playframework/play1/blob/9285dcfbec25097b4a03e1b5b740301ce637b99f/framework/src/play/mvc/ActionInvoker.java#L158
public class GZipCompression extends Controller{
private static final ThreadLocal<Boolean> shouldCache = new ThreadLocal<>();
@Before
static void checkCache(){
CacheFor cacheFor = getActionAnnotation(CacheFor.class);
Method actionMethod = request.invokedMethod;
// Check for @CacheFor annotation
// only in prod mode.
boolean canCache =
cacheFor != null && // @CacheFor annotation is present
Play.mode.isProd() && // production mode
request.contentType != null && request.contentType.startsWith("text/") && // only compress tex
acceptsGzipCompression(); // client wants gzip compression
if( canCache ){
shouldCache.set(true);
// Check the cache (only for GET or HEAD)
if ((request.method.equals("GET") || request.method.equals("HEAD"))) {
String cacheKey = actionMethod.getAnnotation(CacheFor.class).id();
if ("".equals(cacheKey)) {
cacheKey = "gzip_urlcache:" + request.url; // req.querystring is already included in req.url
}
else{
cacheKey = "gzip_" + cacheKey;
}
System.out.println( "cache key = " + cacheKey );
Result actionResult = (Result) play.cache.Cache.get(cacheKey);
if( actionResult != null ){
System.out.println("found gzipped in cache");
shouldCache.set(false);
throw actionResult;
}
else{
// remember that we want to cache this response!
System.out.println( "going to store to cache soon...");
shouldCache.set(true);
}
}
}
else{
shouldCache.set(false);
}
}
private static boolean acceptsGzipCompression(){
Header encodings = request.headers.get("accept-encoding");
if( encodings == null || encodings.values == null ){
return false;
}
// no accepted encoding? dont encode!
boolean acceptsGzip = false;
if( encodings != null ){
for( String encoding : encodings.values ){
if( encoding.indexOf( "gzip" ) >= 0 ){
acceptsGzip = true;
break;
}
}
}
return acceptsGzip;
}
@Finally
static void compressResponse() throws IOException {
// leaving this here until i know if i want it or not.
/*if ("text/xml".equals(response.contentType)) {
text = new com.googlecode.htmlcompressor.compressor.XmlCompressor().compress(text);
} else if ("text/html; charset=utf-8".equals(response.contentType)) {
text = new com.googlecode.htmlcompressor.compressor.HtmlCompressor().compress(text);
}*/
if( response.status != 200 ){
// bye.
}
// do we still need this?
else if( "gzip".equals(response.getHeader("Content-Encoding"))){
// its already gzipped from the @Before
}
else{
//TODO: can we catch binary data? we shouldn't gzip that!
String text = response.out.toString();
final ByteArrayOutputStream gzip = gzip(text);
response.setHeader("Content-Encoding", "gzip");
response.setHeader("Content-Length", gzip.size() + "");
response.out = gzip;
// store this in the cache for later?
if( shouldCache.get() ){
Method actionMethod = request.invokedMethod;
String cacheKey = actionMethod.getAnnotation(CacheFor.class).id();
if ("".equals(cacheKey)) {
cacheKey = "gzip_urlcache:" + request.url; // req.querystring is already included in req.url
}
else{
cacheKey = "gzip_" + cacheKey;
}
System.out.println( "cache key = " + cacheKey );
System.out.println( "save gzipped cache");
Cache.set(cacheKey, new RenderGZipped(gzip.toByteArray()), actionMethod.getAnnotation(CacheFor.class).value());
}
}
}
private static ByteArrayOutputStream gzip(final String input)
throws IOException {
final InputStream inputStream = new ByteArrayInputStream(input.getBytes());
final ByteArrayOutputStream stringOutputStream = new ByteArrayOutputStream((int) (input.length() * 0.75));
final OutputStream gzipOutputStream = new GZIPOutputStream(stringOutputStream);
final byte[] buf = new byte[5000];
int len;
while ((len = inputStream.read(buf)) > 0) {
gzipOutputStream.write(buf, 0, len);
}
inputStream.close();
gzipOutputStream.close();
return stringOutputStream;
}
/**
* Used to cancel the new call early (before the play cache kicks in).
* We jump right out of the processing pipeline and into the @finally handlers
* Note: this might confuse some other @finally handlers.
* @author hansi
*
*/
public static class RenderGZipped extends Result {
byte[] bytes;
public RenderGZipped(byte[] bytes) {
this.bytes = bytes;
}
public void apply(Request request, Response response) {
try {
response.setHeader("Content-Encoding", "gzip");
setContentTypeIfNotSet(response, "text/html");
response.out.write(bytes);
} catch(Exception e) {
throw new UnexpectedException(e);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment