Skip to content

Commit

Permalink
play-gzip v0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
jaytaylor committed Apr 11, 2012
0 parents commit d82bbb3
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
@@ -0,0 +1,7 @@
*.iml
*.jar
/documentation
/dist
/lib
/modules
/tmp
84 changes: 84 additions & 0 deletions README.md
@@ -0,0 +1,84 @@
# Play-framework 1.2.x series support for HTTP GZIP compression

Author: [Jay Taylor][jaytaylor]
Date of first release: April 10, 2012

This module is hosted [on github](https://github.com/jaytaylor/play-gzip/ "Play-gzip on github")

## Description

This is a plugin to enable GZIP compression of HTTP responses for [Play-framework][]
applications of the 1.2.x variety.


## Requirements

A play-framework application running a [1.2.x series release][Play-framework-releases],
e.g. [play-1.2.4][].


## Adding the plugin to your project

First, add the gzip dependency and repository to `conf/dependencies.yml`:

require:
- play
- play -> gzip 0.1

repositories:
- play-gzip:
type: http
artifact: "https://github.com/jaytaylor/jaytaylor-mvn-repo/raw/master/releases/play/[module]/[revision]/[module]-[revision].[ext]"
contains:
- play -> *

and run `play deps --sync --verbose` to retrieve the module.

Then you're ready to integrate GZIP support into your application! This is as
simple as adding an import statement and extending the controller class with the
`Compress` trait. For DRY purposes, this should usually be a base controller
trait which gets inherited by all other controllers.

import play.modules.gzip.Compress

class MyBaseControllerTrait extends Compress { ... }


## Configuration

Directives available for `conf/application.conf`:

# Set to true to disable gzip compression (defaults to false)
gzip.disabled=false

# Set to true to disable gzip module logging (defaults to false)
gzip.logging.disabled=false

If these directives are not present in your configuration, they will both
default to "false" (meaning that these features will be enabled.)


## Future iterations

- Automatically detect the average compression ratio and optimize
accordingly
- Automatically detect the average response size in real-time as
we go and then automatically optimize the buffer allocation
sizes


## References

The development of this Play module has been fun, and there were a few documents
which helped greatly:

- [engintekin's compression gist][engintekin]
- [lights51's compression optimization commit][lights51]

[jaytaylor]: http://jaytaylor.com/ "Jay Taylor's website"
[Play-framework]: http://playframework.org/ "Play-framework"
[Play-framework-releases]: http://download.playframework.org/releases/ "Play-framework releases"
[play-1.2.4]: http://download.playframework.org/releases/play-1.2.4.zip "Play v1.2.4"
[engintekin]: https://gist.github.com/1317626 "engintekin's compression gist"
[lights51]: https://github.com/lights51/play/commit/a029b74a143464a1dec008b0de6d7ba7a75b8f20 "lights51's compression optimization commit"

92 changes: 92 additions & 0 deletions build.xml
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="play-gzip" default="build" basedir=".">

<path id="project.classpath">
<fileset dir="${play.path}/framework">
<include name="*.jar" />
</fileset>
<fileset dir="${play.path}/framework/lib">
<include name="*.jar" />
</fileset>
<fileset dir="${play.path}/modules/scala-0.9.1/lib">
<include name="*.jar" />
</fileset>
<fileset dir="lib">
<include name="*.jar" />
</fileset>
<pathelement path="tmp/classes" />
</path>


<target name="clean">
<delete dir="tmp" />
<delete file="lib/play-gzip-0.1.jar" />
<delete dir="lib" />
<mkdir dir="lib" />
</target>

<target name="build" depends="clean">
<taskdef resource="scala/tools/ant/antlib.xml">
<classpath refid="project.classpath" />
</taskdef>

<mkdir dir="tmp/classes" />

<scalac srcdir="src" destdir="tmp/classes" force="changed">
<classpath refid="project.classpath" />
</scalac>

<javac srcdir="src" destdir="tmp/classes" debug="true" includeantruntime="false">
<classpath refid="project.classpath" />
</javac>

<copy todir="tmp/classes">
<fileset dir="src">
<include name="**/*.properties" />
<include name="**/*.xml" />
<include name="**/play.plugins" />
</fileset>
</copy>

<jar destfile="lib/play-gzip-0.1.jar" basedir="tmp/classes">
<manifest>
<section name="Play-module">
<attribute name="Specification-Title" value="play-gzip"/>
</section>
</manifest>
</jar>
</target>

<target name="test" depends="build">
<echo message="Using ${play.path}/play" />
<delete dir="${basedir}/samples-and-tests/just-test-cases/tmp" />

<antcall target="play-test">
<param name="testAppPath" value="${basedir}/samples-and-tests/just-test-cases" />
</antcall>

<echo message="****************" />
<echo message="All test passed!" />
<echo message="****************" />
</target>

<target name="play-test">
<echo message="${play.path}/play auto-test ${testAppPath} (wait)" />
<exec executable="${play.path}/play" failonerror="true">
<arg value="clean" />
<arg value="${testAppPath}" />
</exec>
<exec executable="${play.path}/play" failonerror="true">
<arg value="auto-test" />
<arg value="${testAppPath}" />
</exec>
<available file="${testAppPath}/test-result/result.passed" property="${testAppPath}testPassed" />
<fail message="Last test has failed ! (Check results in file://${testAppPath}/test-result)">
<condition>
<not>
<isset property="${testAppPath}testPassed" />
</not>
</condition>
</fail>
</target>
</project>
5 changes: 5 additions & 0 deletions conf/dependencies.yml
@@ -0,0 +1,5 @@
self: play -> gzip 0.1

require:
- play [1.2.4,)
- play -> scala 0.9.1
Empty file added conf/messages
Empty file.
Empty file added conf/routes
Empty file.
39 changes: 39 additions & 0 deletions example/DefaultController.scala
@@ -0,0 +1,39 @@
package controllers

import play.Logger
import play.mvc.{Before, Controller, Finally}
import play.modules.gzip.Compress
import org.joda.time.DateTime

/**
* @author Jay Taylor [@jtaylor]
*
* @date 2011-05-23
*/

trait DefaultController extends Compress {

self: Controller =>

@Before
def setDefaults {
Logger info "Incoming request :: Action=" + request.action + ", params=" + request.params.allSimple
renderArgs += "started" -> new DateTime
}

@Finally
def finish {
val started = renderArgs.get[DateTime]("started", classOf[DateTime])
val ended = new DateTime

Option(started) match {
case Some(started: DateTime) =>
val duration = ended.getMillis - started.getMillis
Logger info "Request took " + duration + "ms) :: Action=" + request.action + ", params=" + request.params.allSimple

case _ =>
Logger warn "[WEIRD] For some reason renderArgs.get(\"started\") was null! :: Action=" + request.action + ", params=" + request.params.allSimple
}
}
}

1 change: 1 addition & 0 deletions src/play.plugins
@@ -0,0 +1 @@
0:play.modules.gzip.GzipCompressionPlugin
95 changes: 95 additions & 0 deletions src/play/modules/gzip/Compress.scala
@@ -0,0 +1,95 @@
package play.modules.gzip

import GzipCompressionPlugin.{gzipDisabled, loggingDisabled}
import play.Logger
import play.mvc.{Controller, Finally}
import scala.collection.JavaConverters._
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.util.zip.GZIPOutputStream

/**
* @author Jay Taylor [@jtaylor]
*
* @date 2012-04-02
*/
trait Compress {


self: Controller =>

/**
* Apply GZIP compression to the response output.
*/
@Finally
def compress() {
// Check whether or not module is enabled.
gzipDisabled match {
case true => Logger info "GZIP compression module disabled by configuration"

case false =>
// Look for the "Accept-Encoding" header.
request.headers.asScala find { _._1.equalsIgnoreCase("accept-encoding") } match {
case Some((_, header)) =>
// Look for "gzip" as a supplied accepted encoding.
header.values.asScala mkString "," split ',' find { _.trim.equalsIgnoreCase("gzip") } match {
case Some(_) =>
val text = response.out.toString
val gzipped = gzip(text)
val bytesOut = gzipped.size

if (!loggingDisabled) {
val bytesIn = (text getBytes "UTF-8").length

// Print some stats to the log.
val percentage = if (text.length == 0) {
0
} else {
((1 - (1.0 * gzipped.size / text.length)) * 100).round
}
Logger info "GZIP compression deflated " + bytesOut + "/" + bytesIn + " (" + percentage + "%)"
}

response.setHeader("Content-Encoding", "gzip")
response.setHeader("Content-Length", bytesOut.toString)
response.out = gzipped

case None => // Client did not support gzip responses.
Logger info "GZIP compression not possible - not supported by client ('gzip' not found in 'accept-encoding' header [value=" + header.toString + "])"
}

case None => // Client did not support gzip responses.
Logger info "GZIP compression not possible - not supported by client (\"accept-encoding\" was not specified)"
}
}
}

/**
* Compress the input string using the GZIP algorithm.
*/
def gzip(input: String): ByteArrayOutputStream = {
val inputStream = new ByteArrayInputStream(input.getBytes)

// Pre-allocate some bytes based on the optimistic assumption that
// some amount of compression will occur
val hopingForCompressibility = (input.length * 0.50).toInt

val byteArrayOut = new ByteArrayOutputStream(hopingForCompressibility)

val gzipper = new GZIPOutputStream(byteArrayOut)

val buf = new Array[Byte](4096)

var numBytesRead = inputStream read buf
while (numBytesRead > 0) {
gzipper.write(buf, 0, numBytesRead)
numBytesRead = inputStream read buf
}

// Clean up.
inputStream.close
gzipper.close

byteArrayOut
}
}

27 changes: 27 additions & 0 deletions src/play/modules/gzip/GzipCompressionPlugin.scala
@@ -0,0 +1,27 @@
package play.modules.gzip

import play.{configuration, Logger, PlayPlugin}

/**
* GZIP Compression Plugin
*
* @author Jay Taylor [@jtaylor]
* @date 2012-04-06
*/

object GzipCompressionPlugin {
val gzipDisabled = configuration("gzip.disabled", "false").toBoolean
val loggingDisabled = configuration("gzip.logging.disabled", "false").toBoolean
}

class GzipCompressionPlugin extends PlayPlugin {

import GzipCompressionPlugin._

/**
* Play Framework Hook: onApplicationStart
*/
override def onApplicationStart: Unit =
Logger info "GZIP deflate module started (gzipDisabled=" + gzipDisabled + ", loggingDisabled=" + loggingDisabled + ")"
}

0 comments on commit d82bbb3

Please sign in to comment.