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