Created
June 18, 2015 10:32
-
-
Save kritzikratzi/e301030778289401f607 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@With({ETagAdder.class, GZipCompression.class}) | |
public class Application extends Controller { | |
@CacheFor(value="5min") | |
public static void doSomething(){ | |
//... | |
render(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() ); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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