diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a52f24b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/.classpath
+/.project
+/tmp
+/dist
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c8e9225
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+Press plugin for playframework - Multiple servers and CDN support
+=================================================================
+
+This is a fork of the press plugin for the Play! framework, which can be found here -
+https://github.com/dirkmc/press
+
+This fork comes to address two issues - using press on a web farm with multiple
+servers, and handling files that are cached on a CDN.
+
+Motivation
+----------
+
+The original version of press relies on the local web server cache for returning the
+compressed file. It generates a hash for the file on the HTML, and then looks for
+that hash on the cache for resolving it to the compressed file.
+This poses a problem when working in a multiple server environment - one server may
+generate the HTML, but another one can get the request for the file and fail since
+it doesn't have the hash in the local cache yet.
+
+The second issue is routing the files through a CDN. We want the CDN to automatically
+re-fetch updated files without the need to manually refresh them on the CDN.
+
+Features
+--------
+
+### Multiple Servers
+* Press supports a new operation mode - serverFarm. To enable it add in the conf file:
+ `press.serverFarm=true` .
+ When this mode is set, Press won't use a hash in the requests for the compressed files.
+ Instead it will generate a file name consisting of the combined files and their time.
+ Since this file name can be long, if it's too long it will break it to several files.
+* The combined files should maintain a certain order, otherwise it can cause Javascript errors.
+ To do so, in serverFarm mode you have to specify the order of the combined files. Do it
+ by using the new "pos" parameter on the press.script tag:
+ `#{press.script '/my.js', pos:1 /}`
+
+### CDN Support
+* To route all press file requests through a CDN add to the conf file:
+ `press.contentHostingDomain=http://my.cdn.host` .
+* Press supports a new "cache buster" mode that will add the timestamp of the file as a request parameter. To enable it
+ add in the conf file: `press.cacheBuster=true`
+* Cache buster has two modes. In the default mode, press will add the timestamp of the file to all requests
+ generated with the single-script tag (The combined scripts already include the file time).
+ That will make the CDN re-fetch the file if the file time is different.
+* Some CDN providers (like Amazon CloudFront) doesn't support query strings. To handle this there is a seconds cache buster mode that doesn't use query string. Insead it will append a press suffix to the filename, and press will capture these kind of requests and will return the original file. To enable this mode add in the conf file: `press.cacheBusterInQueryString=false`
+* If you need to route other non-js or css files (like images) through the CDN, you can use the new "versioned-file" tag. This will render a CDN versioned file name instead of the original one. Example:
+ ``
+ will output:
+ ``
+
\ No newline at end of file
diff --git a/app/controllers/press/Press.java b/app/controllers/press/Press.java
index cb47218..21b8845 100644
--- a/app/controllers/press/Press.java
+++ b/app/controllers/press/Press.java
@@ -4,22 +4,46 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.Locale;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
-
+import play.Play;
import play.exceptions.UnexpectedException;
+import play.libs.MimeTypes;
import play.mvc.Controller;
+import play.mvc.Http;
+import play.utils.Utils;
+import play.vfs.VirtualFile;
import press.CSSCompressor;
import press.CachingStrategy;
import press.JSCompressor;
+import press.Plugin;
import press.PluginConfig;
import press.io.CompressedFile;
import press.io.FileIO;
public class Press extends Controller {
- public static final DateTimeFormatter httpDateTimeFormatter = DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
+ public static final DateTimeFormatter httpDateTimeFormatter = DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'").withLocale(Locale.US);
+
+ static boolean eTag_ = Play.configuration.getProperty("http.useETag", "true").equalsIgnoreCase("true");
+
+ public static void handleVersionedFile(String path) {
+ if (null == path) {
+ notFound();
+ return;
+ }
+ // we need to remove the filetime suffix
+ int pos = path.lastIndexOf('.');
+ if (pos > -1){
+ path = path.substring(0, pos);
+ }
+ VirtualFile file = FileIO.getVirtualFile(path);
+ renderFile(file, path);
+ }
+
public static void getCompressedJS(String key) {
key = FileIO.unescape(key);
CompressedFile compressedFile = JSCompressor.getCompressedFile(key);
@@ -44,11 +68,113 @@ public static void getSingleCompressedCSS(String key) {
renderCompressedFile(compressedFile, "CSS");
}
+ private static void renderFile(VirtualFile file, String fileName) {
+ if (file == null || !file.exists()) {
+ //renderBadResponse(fileName);
+ notFound();
+ return;
+ }
+
+ String mimeType = MimeTypes.getContentType(fileName);
+ // check for last modified
+ long l = file.lastModified();
+ final String etag = "\"" + l + "-" + file.hashCode() + "\"";
+
+ if (l > 0){
+ // if the file is not modified, return 304
+ if (!request.isModified(etag, l)){
+ if (request.method.equalsIgnoreCase("GET")) {
+ response.status = Http.StatusCode.NOT_MODIFIED;
+ if (eTag_) {
+ response.setHeader("Etag", etag);
+ //response.setHeader("Content-Type", mimeType);
+ }
+ }
+ return;
+ }
+ }
+
+ InputStream inputStream = file.inputstream();
+
+ // This seems to be buggy, so instead of passing the file length we
+ // reset the input stream and allow play to manually copy the bytes from
+ // the input stream to the response
+ // renderBinary(inputStream, compressedFile.name(),
+ // compressedFile.length());
+
+ try {
+ if(inputStream.markSupported()) {
+ inputStream.reset();
+ }
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+
+ // special handling for sass - let the plugin a change to handle this
+ boolean raw = Play.pluginCollection.serveStatic(file, request, response);
+
+ // If the caching strategy is always, the timestamp is not part of the key. If
+ // we let the browser cache, then the browser will keep holding old copies, even after
+ // changing the files at the server and restarting the server, since the key will
+ // stay the same.
+ // If the caching strategy is never, we also don't want to cache at the browser, for
+ // obvious reasons.
+ // If the caching strategy is Change, then the modified timestamp is a part of the key,
+ // so if the file changes, the key in the html file will be modified, and the browser will
+ // request a new version. Each version can therefore be cached indefinitely.
+ if(PluginConfig.cache.equals(CachingStrategy.Change)) {
+ response.setHeader("Cache-Control", "max-age=" + 31536000); // A year
+ response.setHeader("Expires", httpDateTimeFormatter.print(new DateTime().plusYears(1)));
+ response.setHeader("Last-Modified", Utils.getHttpDateFormatter().format(new Date(l + 1000)));
+ if (!raw)
+ response.setHeader("Content-Type", mimeType);
+ if (eTag_) {
+ response.setHeader("Etag", etag);
+ }
+ }
+ if (raw) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ throw new UnexpectedException(e);
+ }
+ }
+ else{
+ renderBinary(inputStream, file.getName(), true);
+ }
+ }
+
private static void renderCompressedFile(CompressedFile compressedFile, String type) {
if (compressedFile == null) {
- renderBadResponse(type);
+ //renderBadResponse(type);
+ notFound();
+ return;
+ }
+
+ String contentType = "application/javascript; charset=utf-8";
+ if(type.equals("CSS")) {
+ contentType = "text/css; charset=utf-8";
}
+ // check for last modified
+ long l = compressedFile.lastModified();
+ final String etag = "\"" + l + "-" + compressedFile.originalHashCode() + "\"";
+
+ if (l > 0){
+ // if the file is not modified, return 304
+ if (!request.isModified(etag, l)){
+ if (request.method.equalsIgnoreCase("GET")) {
+ response.status = Http.StatusCode.NOT_MODIFIED;
+ if (eTag_) {
+ response.setHeader("Etag", etag);
+ response.setHeader("Content-Type", contentType);
+
+ }
+ }
+ return;
+ }
+ }
+
InputStream inputStream = compressedFile.inputStream();
// This seems to be buggy, so instead of passing the file length we
@@ -77,10 +203,13 @@ private static void renderCompressedFile(CompressedFile compressedFile, String t
if(PluginConfig.cache.equals(CachingStrategy.Change)) {
response.setHeader("Cache-Control", "max-age=" + 31536000); // A year
response.setHeader("Expires", httpDateTimeFormatter.print(new DateTime().plusYears(1)));
+ response.setHeader("Last-Modified", Utils.getHttpDateFormatter().format(new Date(l + 1000)));
+ response.setHeader("Content-Type", contentType);
+ if (eTag_) {
+ response.setHeader("Etag", etag);
+ }
}
-
- renderBinary(inputStream, compressedFile.name());
-
+ renderBinary(inputStream, compressedFile.name(), true);
}
public static void clearJSCache() {
diff --git a/app/press/CSSCompressor.java b/app/press/CSSCompressor.java
index a7eea49..762327b 100644
--- a/app/press/CSSCompressor.java
+++ b/app/press/CSSCompressor.java
@@ -21,7 +21,7 @@ public CSSCompressor() {
}
public String compressedSingleFileUrl(String fileName) {
- return compressedSingleFileUrl(cssFileCompressor, fileName);
+ return compressedSingleFileUrl(cssFileCompressor, fileName, null);
}
public static CompressedFile getCompressedFile(String key) {
diff --git a/app/press/Compressor.java b/app/press/Compressor.java
index 0f686c4..76eb309 100644
--- a/app/press/Compressor.java
+++ b/app/press/Compressor.java
@@ -3,21 +3,23 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-
import play.PlayPlugin;
import play.cache.Cache;
import play.exceptions.UnexpectedException;
@@ -26,6 +28,7 @@
import play.mvc.Http.Response;
import play.templates.JavaExtensions;
import play.vfs.VirtualFile;
+import press.Compressor.FileInfo;
import press.io.CompressedFile;
import press.io.FileIO;
@@ -94,20 +97,25 @@ public Compressor(String fileType, String extension, String tagName, String comp
* have the same name as the original file with .min appended before the
* extension. eg the compressed output of widget.js will be in widget.min.js
*/
- public String compressedSingleFileUrl(FileCompressor compressor, String fileName) {
+ public String compressedSingleFileUrl(FileCompressor compressor, String fileName, String dir) {
PressLogger.trace("Request to compress single file %s", fileName);
+ VirtualFile srcFile = checkFileExists(fileName, dir);
int lastDot = fileName.lastIndexOf('.');
String compressedFileName = fileName.substring(0, lastDot) + ".min";
+ if (press.PluginConfig.cacheBuster && !press.PluginConfig.cacheBusterInQueryString){
+ compressedFileName += "." + srcFile.lastModified();
+ }
compressedFileName += fileName.substring(lastDot);
// The process for compressing a single file is the same as for a group
// of files, the list just has a single entry
- VirtualFile srcFile = checkFileExists(fileName);
List componentFiles = new ArrayList(1);
- componentFiles.add(new FileInfo(compressedFileName, true, srcFile));
+ componentFiles.add(new FileInfo(compressedFileName, true, srcFile, -1));
// Check whether the compressed file needs to be generated
+ if (compressedDir.endsWith("/") && compressedFileName.startsWith("/"))
+ compressedFileName = compressedFileName.substring(1);
String outputFilePath = compressedDir + compressedFileName;
CompressedFile outputFile = CompressedFile.create(outputFilePath);
if (outputFile.exists() && useCache(componentFiles, outputFile, extension)) {
@@ -118,6 +126,10 @@ public String compressedSingleFileUrl(FileCompressor compressor, String fileName
writeCompressedFile(compressor, componentFiles, outputFile);
}
+ if (press.PluginConfig.cacheBuster && press.PluginConfig.cacheBusterInQueryString){
+ outputFilePath += "?" + srcFile.lastModified();
+ }
+
return outputFilePath;
}
@@ -125,12 +137,57 @@ public static CompressedFile getSingleCompressedFile(String requestKey) {
return CompressedFile.create(requestKey);
}
+
+ /*
+ * Used for creating a versioned file name
+ */
+ String combinePath(String path, String filename){
+ if (null == path) return filename;
+ if (null == filename) return path;
+ if (path == "/") path = "";
+ if (path.endsWith("/") && filename.startsWith("/"))
+ filename = filename.substring(1);
+ else if (!path.endsWith("/") && !filename.startsWith("/"))
+ return path + "/" + filename;
+ return path + filename;
+ }
+
+ boolean copyFile(VirtualFile srcFile, VirtualFile destFile){
+ FileChannel source = null;
+ FileChannel destination = null;
+ try {
+ if(!destFile.exists()) {
+ destFile.getRealFile().createNewFile();
+ }
+ source = new FileInputStream(srcFile.getRealFile()).getChannel();
+ destination = new FileOutputStream(destFile.getRealFile()).getChannel();
+ destination.transferFrom(source, 0, source.size());
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ finally {
+ if(source != null) {
+ try {
+ source.close();
+ } catch (IOException e) {
+ }
+ }
+ if(destination != null) {
+ try {
+ destination.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
/**
* Adds a file to the list of files to be compressed
*
* @return the file request signature to be output in the HTML
*/
- public String add(String fileName, boolean compress) {
+ public String add(String fileName, boolean compress, int position) {
if (compress) {
PressLogger.trace("Adding %s to output", fileName);
} else {
@@ -142,7 +199,7 @@ public String add(String fileName, boolean compress) {
}
// Add the file to the list of files to be compressed
- fileInfos.put(fileName, new FileInfo(fileName, compress, checkFileExists(fileName)));
+ fileInfos.put(fileName, new FileInfo(fileName, compress, checkFileExists(fileName), position));
return getFileRequestSignature(fileName);
}
@@ -172,20 +229,61 @@ public String closeRequest() {
*/
private String getRequestKey() {
String key = "";
- for (Entry entry : fileInfos.entrySet()) {
- key += entry.getKey();
- // If we use the 'Change' caching strategy, make the modified timestamp
- // of each file part of the key.
- if(PluginConfig.cache.equals(CachingStrategy.Change)) {
- key += entry.getValue().getLastModified();
+ // for a farm we need to create the server list in order. we rely on the user to
+ // specify the order in the tag as we can't count on play to call the tags in the order specified
+ // and we can't rely on the hash key if it comes to a different server
+ // so we want to be able to generate the combined file on the fly according to the explicitly set order
+ if (press.PluginConfig.serverFarm){
+ TreeSet sorted = new TreeSet();
+ for (Entry entry : fileInfos.entrySet()) {
+ FileInfo fi = entry.getValue();
+ sorted.add(fi);
+ }
+ // now create the key in order
+ for (FileInfo entry : sorted) {
+ if (key.length() > 0)
+ key += ",";
+ key += sanitizeKey(entry.fileName);
+ if(PluginConfig.cache.equals(CachingStrategy.Change)) {
+ key += "|" + entry.getLastModified();
+ }
+ }
+ }
+ else{
+ for (Entry entry : fileInfos.entrySet()) {
+ key += entry.getKey();
+ // If we use the 'Change' caching strategy, make the modified timestamp
+ // of each file part of the key.
+ if(PluginConfig.cache.equals(CachingStrategy.Change)) {
+ key += entry.getValue().getLastModified();
+ }
}
}
-
// Get a hash of the url to keep it short
- String hashed = Crypto.passwordHash(key);
- return FileIO.lettersOnly(hashed);
+ if (press.PluginConfig.serverFarm){
+ // if we're on multiple servers we can't rely on the cache key as the combined JS may not be generated yet
+ return key;
+ }
+ else{
+ // Get a hash of the url to keep it short
+ String hashed = Crypto.passwordHash(key);
+ return FileIO.lettersOnly(hashed);
+ }
}
+ private String sanitizeKey(String key) {
+ if (null == key)
+ return key;
+ if (key.startsWith("/"))
+ key = key.substring(1);
+ if (key.endsWith(".js")){
+ key = key.substring(0, key.length()-3);
+ }
+ else if (key.endsWith(".css")){
+ key = key.substring(0, key.length()-4);
+ }
+ return key;
+ }
public void saveFileList() {
// If the request key has not been set, that means there was no request
// for compressed source anywhere in the template file, so we don't
@@ -207,20 +305,24 @@ public void saveFileList() {
return;
}
- // The press tag may not always have been executed by the template
- // engine in the same order that the resulting \n";
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ return sb.toString();
}
/**
@@ -250,7 +345,15 @@ private static String getScriptTag(String src) {
* within the HTML.
*/
private static String getLinkTag(String src) {
- return ""
- + (press.PluginConfig.htmlCompatible ? "" : "") + "\n";
+ StringBuilder sb = new StringBuilder();
+ sb.append(" 0)
+ sb.append(press.PluginConfig.contentHostingDomain);
+ sb.append(src);
+ sb.append("\" rel=\"stylesheet\" type=\"text/css\" charset=\"utf-8\">");
+ if (!press.PluginConfig.htmlCompatible)
+ sb.append("");
+ sb.append("\n");
+ return sb.toString();
}
}
diff --git a/app/press/PluginConfig.java b/app/press/PluginConfig.java
index dc50635..afb5249 100644
--- a/app/press/PluginConfig.java
+++ b/app/press/PluginConfig.java
@@ -40,6 +40,16 @@ public static class DefaultConfig {
// Will be used to turn a relative URI into an absolute URL.
public static final String contentHostingDomain = "";
+ // Is this a server farm with multiple servers. In this case we need to generate
+ // different JS names, and support minification on the fly.
+ public static final boolean serverFarm = false;
+
+ // Whether to append a cache buster (the file time) to the JS request
+ public static final boolean cacheBuster = false;
+
+ // Whether to use the query string when using the cache buster (only if cache buster is enabled). this doesn't work on all CDN providers.
+ public static final boolean cacheBusterInQueryString = true;
+
public static class js {
// The directory where source javascript files are read from
public static final String srcDir = "/public/javascripts/";
@@ -75,7 +85,10 @@ public static class css {
public static int maxCompressionTimeMillis;
public static boolean htmlCompatible;
public static String contentHostingDomain;
-
+ public static boolean serverFarm;
+ public static boolean cacheBuster;
+ public static boolean cacheBusterInQueryString;
+
public static class js {
public static String srcDir = DefaultConfig.js.srcDir;
public static String compressedDir = DefaultConfig.js.compressedDir;
@@ -119,8 +132,10 @@ public static void readConfig() {
DefaultConfig.maxCompressionTimeMillis);
htmlCompatible = ConfigHelper.getBoolean("press.htmlCompatible",
DefaultConfig.htmlCompatible);
- contentHostingDomain = ConfigHelper.getString("press.contentHostingDomain",
- DefaultConfig.contentHostingDomain);
+ contentHostingDomain = ConfigHelper.getString("press.contentHostingDomain", DefaultConfig.contentHostingDomain);
+ serverFarm = ConfigHelper.getBoolean("press.serverFarm", DefaultConfig.serverFarm);
+ cacheBuster = ConfigHelper.getBoolean("press.cacheBuster", DefaultConfig.cacheBuster);
+ cacheBusterInQueryString = ConfigHelper.getBoolean("press.cacheBusterInQueryString", DefaultConfig.cacheBusterInQueryString);
css.srcDir = ConfigHelper.getString("press.css.sourceDir", DefaultConfig.css.srcDir);
css.compressedDir = ConfigHelper.getString("press.css.outputDir",
diff --git a/app/press/io/CompressedFile.java b/app/press/io/CompressedFile.java
index 16818e1..65fcc3d 100644
--- a/app/press/io/CompressedFile.java
+++ b/app/press/io/CompressedFile.java
@@ -43,4 +43,8 @@ public static int clearCache(String compressedDir, String extension) {
public abstract void close();
public abstract long length();
+
+ public abstract long lastModified();
+
+ public abstract int originalHashCode();
}
diff --git a/app/press/io/InMemoryCompressedFile.java b/app/press/io/InMemoryCompressedFile.java
index 686c4ec..18f2d97 100644
--- a/app/press/io/InMemoryCompressedFile.java
+++ b/app/press/io/InMemoryCompressedFile.java
@@ -167,4 +167,25 @@ public long length() {
return bytes.length;
}
+
+ @Override
+ public long lastModified() {
+ if (!exists()) {
+ throw new PressException("Can't get lastModified. File with key " + getCacheKey()
+ + " does not exist in cache");
+ }
+
+ return 0;
+ }
+
+ @Override
+ public int originalHashCode() {
+ if (!exists()) {
+ throw new PressException("Can't get originalHashCode. File with key " + getCacheKey()
+ + " does not exist in cache");
+ }
+
+ return bytes.hashCode();
+ }
+
}
diff --git a/app/press/io/OnDiskCompressedFile.java b/app/press/io/OnDiskCompressedFile.java
index 695770a..ad05796 100644
--- a/app/press/io/OnDiskCompressedFile.java
+++ b/app/press/io/OnDiskCompressedFile.java
@@ -96,19 +96,18 @@ public void close() {
String tmpPath = tmpOutputFile.getAbsolutePath();
String finalPath = file.getRealFile().getAbsolutePath();
PressLogger.trace(msg, tmpPath, finalPath);
- if (!tmpOutputFile.renameTo(file.getRealFile())) {
- String ex = "Successfully wrote compressed file to temporary path\n" + tmpPath;
- ex += "\nBut could not move it to final path\n" + finalPath;
- throw new PressException(ex);
- }
-
try {
- tmpOutputFile = null;
writer.flush();
writer.close();
} catch (IOException e) {
throw new UnexpectedException(e);
}
+ if (!tmpOutputFile.renameTo(file.getRealFile())) {
+ String ex = "Successfully wrote compressed file to temporary path\n" + tmpPath;
+ ex += "\nBut could not move it to final path\n" + finalPath;
+ throw new PressException(ex);
+ }
+ tmpOutputFile = null;
}
private static File getTmpOutputFile(VirtualFile file) {
@@ -183,4 +182,22 @@ public long length() {
return file.length();
}
+ @Override
+ public long lastModified() {
+ if (!exists()) {
+ throw new PressException("Can't get lastModified. File does not exist");
+ }
+
+ return file.lastModified();
+ }
+
+ @Override
+ public int originalHashCode() {
+ if (!exists()) {
+ throw new PressException("Can't get originalHashCode. File does not exist");
+ }
+
+ return file.hashCode();
+ }
+
}
diff --git a/app/views/tags/press/compressed-script.tag b/app/views/tags/press/compressed-script.tag
index 40dbb50..ee18c42 100644
--- a/app/views/tags/press/compressed-script.tag
+++ b/app/views/tags/press/compressed-script.tag
@@ -11,5 +11,4 @@
*
* See the plugin documentation for more information.
*
-}*
-${ press.Plugin.compressedJSTag() }
\ No newline at end of file
+}*${ press.Plugin.compressedJSTag() }
\ No newline at end of file
diff --git a/app/views/tags/press/compressed-stylesheet.tag b/app/views/tags/press/compressed-stylesheet.tag
index eca3a74..e56d73a 100644
--- a/app/views/tags/press/compressed-stylesheet.tag
+++ b/app/views/tags/press/compressed-stylesheet.tag
@@ -15,5 +15,4 @@
* Source css files MUST be in utf-8 format.
* See the plugin documentation for more information.
*
-}*
-${ press.Plugin.compressedCSSTag() }
\ No newline at end of file
+}*${ press.Plugin.compressedCSSTag() }
\ No newline at end of file
diff --git a/app/views/tags/press/script.tag b/app/views/tags/press/script.tag
index 65b06a9..4fa7fd5 100644
--- a/app/views/tags/press/script.tag
+++ b/app/views/tags/press/script.tag
@@ -23,18 +23,19 @@
* Source script files MUST be in utf-8 format.
* See the plugin documentation for more information.
*
-}*
-%{
+}*%{
( _arg ) && ( _src = _arg);
// compress defaults to true
if(_compress == null) {
_compress = true;
}
+ if(_pos == null) {
+ _pos = -1;
+ }
if(! _src) {
throw new play.exceptions.TagInternalException("src attribute cannot be empty for press.script tag");
}
-}%
-${ press.Plugin.addJS(_src, _compress) }
\ No newline at end of file
+}%${ press.Plugin.addJS(_src, _compress, _pos) }
\ No newline at end of file
diff --git a/app/views/tags/press/single-script.tag b/app/views/tags/press/single-script.tag
index 2ea6baa..8c6ae69 100644
--- a/app/views/tags/press/single-script.tag
+++ b/app/views/tags/press/single-script.tag
@@ -12,12 +12,14 @@
*
* See the plugin documentation for more information.
*
-}*
-%{
+}*%{
( _arg ) && ( _src = _arg);
-
+ // compress defaults to true
+ if(_compress == null) {
+ _compress = true;
+ }
+
if(! _src) {
throw new play.exceptions.TagInternalException("src attribute cannot be empty for press.single-script tag");
}
-}%
-${ press.Plugin.addSingleJS(_src) }
\ No newline at end of file
+}%${ press.Plugin.addSingleJS(_src, _dir, _compress) }
\ No newline at end of file
diff --git a/app/views/tags/press/single-stylesheet.tag b/app/views/tags/press/single-stylesheet.tag
index e5647ce..cdaeb1f 100644
--- a/app/views/tags/press/single-stylesheet.tag
+++ b/app/views/tags/press/single-stylesheet.tag
@@ -12,12 +12,14 @@
*
* See the plugin documentation for more information.
*
-}*
-%{
+}*%{
( _arg ) && ( _src = _arg);
+ // compress defaults to true
+ if(_compress == null) {
+ _compress = true;
+ }
if(! _src) {
throw new play.exceptions.TagInternalException("src attribute cannot be empty for press.single-stylesheet tag");
}
-}%
-${ press.Plugin.addSingleCSS(_src) }
\ No newline at end of file
+}%${ press.Plugin.addSingleCSS(_src, _compress) }
\ No newline at end of file
diff --git a/app/views/tags/press/stylesheet.tag b/app/views/tags/press/stylesheet.tag
index 3cd9f61..db348d2 100644
--- a/app/views/tags/press/stylesheet.tag
+++ b/app/views/tags/press/stylesheet.tag
@@ -22,18 +22,19 @@
* Source css files MUST be in utf-8 format.
* See the plugin documentation for more information.
*
-}*
-%{
+}*%{
( _arg ) && ( _src = _arg);
// compress defaults to true
if(_compress == null) {
_compress = true;
}
+ if(_pos == null) {
+ _pos = -1;
+ }
if(! _src) {
throw new play.exceptions.TagInternalException("src attribute cannot be empty for stylesheet tag");
}
-}%
-${ press.Plugin.addCSS(_src, _compress) }
\ No newline at end of file
+}%${ press.Plugin.addCSS(_src, _compress, _pos) }
\ No newline at end of file
diff --git a/app/views/tags/press/versioned-file.tag b/app/views/tags/press/versioned-file.tag
new file mode 100644
index 0000000..b4f06f2
--- /dev/null
+++ b/app/views/tags/press/versioned-file.tag
@@ -0,0 +1,20 @@
+*{
+ * Outputs a versioned file name whose source is the file
+ * specified as a parameter
+ * When the plugin is disabled, outputs the original file name
+ * for easy debugging.
+ *
+ * eg:
+ *
+ *
+ * will output:
+ *
+ *
+ * See the plugin documentation for more information.
+ *
+}*%{
+ ( _arg ) && ( _src = _arg);
+ if(! _src) {
+ throw new play.exceptions.TagInternalException("src attribute cannot be empty for press.versioned-file tag");
+ }
+}%${ press.Plugin.addVersionedFile(_src) }
\ No newline at end of file
diff --git a/conf/routes b/conf/routes
index 8015484..4c178ca 100644
--- a/conf/routes
+++ b/conf/routes
@@ -11,3 +11,4 @@ GET /press/js/single/{key} press.Press.getSingleCompressedJS
GET /press/css/clear press.Press.clearCSSCache
GET /press/css/{key} press.Press.getCompressedCSS
GET /press/css/single/{key} press.Press.getSingleCompressedCSS
+GET /{<.*>path}.press press.Press.handleVersionedFile
\ No newline at end of file
diff --git a/lib/.gitignore b/lib/.gitignore
new file mode 100644
index 0000000..0381d1c
--- /dev/null
+++ b/lib/.gitignore
@@ -0,0 +1 @@
+/play-press.jar
diff --git a/lib/commons-io-2.0.1.jar b/lib/commons-io-2.0.1.jar
new file mode 100644
index 0000000..5b64b7d
Binary files /dev/null and b/lib/commons-io-2.0.1.jar differ
diff --git a/lib/joda-time-2.0.jar b/lib/joda-time-2.0.jar
new file mode 100644
index 0000000..169a7a4
Binary files /dev/null and b/lib/joda-time-2.0.jar differ