You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2013/08/26 08:57:52 UTC

[18/21] git commit: Complete the rename of the module and the packages

Complete the rename of the module and the packages


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/6e42b37c
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/6e42b37c
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/6e42b37c

Branch: refs/heads/master
Commit: 6e42b37cf25536a16f8e9a1c483296e98a38b6b8
Parents: 31fedfe
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Sat Aug 24 21:51:46 2013 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Sat Aug 24 21:51:46 2013 -0700

----------------------------------------------------------------------
 tapestry-webresources/LICENSE-YUICompressor.txt |   3 +
 tapestry-webresources/LICENSE.txt               | 202 ++++++
 tapestry-webresources/NOTICE.txt                |   5 +
 tapestry-webresources/build.gradle              |  32 +
 .../platform/yui/compressor/CssCompressor.java  | 490 +++++++++++++
 .../webresources/AbstractMinimizer.java         | 107 +++
 .../internal/webresources/CSSMinimizer.java     |  58 ++
 .../internal/webresources/CacheMode.java        |  40 +
 .../webresources/CoffeeScriptCompiler.java      |  82 +++
 .../webresources/ContentChangeTracker.java      |  61 ++
 .../webresources/GoogleClosureMinimizer.java    |  79 ++
 .../webresources/LessResourceTransformer.java   | 120 +++
 .../ResourceDependenciesSplitter.java           |  40 +
 .../webresources/ResourceTransformUtils.java    |  68 ++
 .../ResourceTransformerFactory.java             |  45 ++
 .../ResourceTransformerFactoryImpl.java         | 269 +++++++
 .../internal/webresources/RhinoExecutor.java    |  24 +
 .../webresources/RhinoExecutorPool.java         | 147 ++++
 .../webresources/WebResourcesSymbols.java       |  25 +
 .../modules/WebResourcesModule.java             | 126 ++++
 .../webresources/internal/coffeescript-1.6.3.js |  12 +
 .../internal/invoke-coffeescript.js             |  14 +
 .../t5/webresources/components/Layout.groovy    |  20 +
 .../groovy/t5/webresources/pages/Index.groovy   |   9 +
 .../t5/webresources/pages/MultiLess.groovy      |   7 +
 .../webresources/tests/WebResourcesSpec.groovy  |  61 ++
 .../t5/webresources/services/AppModule.java     |  50 ++
 .../src/test/resources/GebConfig.groovy         |   8 +
 .../test/resources/META-INF/assets/colors.less  |   2 +
 .../test/resources/META-INF/assets/index.less   |   5 +
 .../test/resources/META-INF/assets/multi.less   |  11 +
 .../resources/META-INF/modules/index.coffee     |   3 +
 .../src/test/resources/log4j.properties         |  12 +
 .../t5/webresources/components/Layout.tml       |  42 ++
 .../resources/t5/webresources/pages/Index.tml   |   7 +
 .../t5/webresources/pages/MultiLess.tml         |  16 +
 .../src/test/webapp/WEB-INF/web.xml             |  19 +
 .../fonts/glyphicons-halflings-regular.eot      | Bin 0 -> 14079 bytes
 .../fonts/glyphicons-halflings-regular.svg      | 228 ++++++
 .../fonts/glyphicons-halflings-regular.ttf      | Bin 0 -> 29512 bytes
 .../fonts/glyphicons-halflings-regular.woff     | Bin 0 -> 16448 bytes
 .../src/test/webapp/bootstrap/js/affix.js       | 126 ++++
 .../src/test/webapp/bootstrap/js/alert.js       |  98 +++
 .../src/test/webapp/bootstrap/js/button.js      | 109 +++
 .../src/test/webapp/bootstrap/js/carousel.js    | 217 ++++++
 .../src/test/webapp/bootstrap/js/collapse.js    | 179 +++++
 .../src/test/webapp/bootstrap/js/dropdown.js    | 154 ++++
 .../src/test/webapp/bootstrap/js/modal.js       | 246 +++++++
 .../src/test/webapp/bootstrap/js/popover.js     | 117 +++
 .../src/test/webapp/bootstrap/js/scrollspy.js   | 158 ++++
 .../src/test/webapp/bootstrap/js/tab.js         | 135 ++++
 .../src/test/webapp/bootstrap/js/tooltip.js     | 386 ++++++++++
 .../src/test/webapp/bootstrap/js/transition.js  |  56 ++
 .../src/test/webapp/bootstrap/less/alerts.less  |  67 ++
 .../src/test/webapp/bootstrap/less/badges.less  |  51 ++
 .../test/webapp/bootstrap/less/bootstrap.less   |  59 ++
 .../test/webapp/bootstrap/less/breadcrumbs.less |  23 +
 .../webapp/bootstrap/less/button-groups.less    | 248 +++++++
 .../src/test/webapp/bootstrap/less/buttons.less | 160 ++++
 .../test/webapp/bootstrap/less/carousel.less    | 209 ++++++
 .../src/test/webapp/bootstrap/less/close.less   |  33 +
 .../src/test/webapp/bootstrap/less/code.less    |  56 ++
 .../bootstrap/less/component-animations.less    |  29 +
 .../test/webapp/bootstrap/less/dropdowns.less   | 193 +++++
 .../src/test/webapp/bootstrap/less/forms.less   | 353 +++++++++
 .../test/webapp/bootstrap/less/glyphicons.less  | 232 ++++++
 .../src/test/webapp/bootstrap/less/grid.less    | 346 +++++++++
 .../webapp/bootstrap/less/input-groups.less     | 127 ++++
 .../test/webapp/bootstrap/less/jumbotron.less   |  40 +
 .../src/test/webapp/bootstrap/less/labels.less  |  58 ++
 .../test/webapp/bootstrap/less/list-group.less  |  88 +++
 .../src/test/webapp/bootstrap/less/media.less   |  56 ++
 .../src/test/webapp/bootstrap/less/mixins.less  | 723 +++++++++++++++++++
 .../src/test/webapp/bootstrap/less/modals.less  | 141 ++++
 .../src/test/webapp/bootstrap/less/navbar.less  | 621 ++++++++++++++++
 .../src/test/webapp/bootstrap/less/navs.less    | 229 ++++++
 .../test/webapp/bootstrap/less/normalize.less   | 396 ++++++++++
 .../src/test/webapp/bootstrap/less/pager.less   |  55 ++
 .../test/webapp/bootstrap/less/pagination.less  |  83 +++
 .../src/test/webapp/bootstrap/less/panels.less  | 148 ++++
 .../test/webapp/bootstrap/less/popovers.less    | 133 ++++
 .../src/test/webapp/bootstrap/less/print.less   | 100 +++
 .../webapp/bootstrap/less/progress-bars.less    |  95 +++
 .../bootstrap/less/responsive-utilities.less    | 220 ++++++
 .../test/webapp/bootstrap/less/scaffolding.less | 130 ++++
 .../src/test/webapp/bootstrap/less/tables.less  | 236 ++++++
 .../src/test/webapp/bootstrap/less/theme.less   | 232 ++++++
 .../test/webapp/bootstrap/less/thumbnails.less  |  31 +
 .../src/test/webapp/bootstrap/less/tooltip.less |  95 +++
 .../src/test/webapp/bootstrap/less/type.less    | 238 ++++++
 .../test/webapp/bootstrap/less/utilities.less   |  42 ++
 .../test/webapp/bootstrap/less/variables.less   | 620 ++++++++++++++++
 .../src/test/webapp/bootstrap/less/wells.less   |  29 +
 93 files changed, 11555 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/LICENSE-YUICompressor.txt
----------------------------------------------------------------------
diff --git a/tapestry-webresources/LICENSE-YUICompressor.txt b/tapestry-webresources/LICENSE-YUICompressor.txt
new file mode 100644
index 0000000..9d17737
--- /dev/null
+++ b/tapestry-webresources/LICENSE-YUICompressor.txt
@@ -0,0 +1,3 @@
+BSD LICENSE
+
+(Need to provide the real text; but I'm on a plane!)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/LICENSE.txt
----------------------------------------------------------------------
diff --git a/tapestry-webresources/LICENSE.txt b/tapestry-webresources/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/tapestry-webresources/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/NOTICE.txt
----------------------------------------------------------------------
diff --git a/tapestry-webresources/NOTICE.txt b/tapestry-webresources/NOTICE.txt
new file mode 100644
index 0000000..a5155e9
--- /dev/null
+++ b/tapestry-webresources/NOTICE.txt
@@ -0,0 +1,5 @@
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes source from the YUI Compressor library, available under a BSD License.
+http://yui.github.io/yuicompressor/
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/build.gradle
----------------------------------------------------------------------
diff --git a/tapestry-webresources/build.gradle b/tapestry-webresources/build.gradle
new file mode 100644
index 0000000..0d9813e
--- /dev/null
+++ b/tapestry-webresources/build.gradle
@@ -0,0 +1,32 @@
+description = "Integration with WRO4J to perform runtime CoffeeScript compilation, JavaScript minimization, and more."
+
+dependencies {
+    compile project(":tapestry-core")
+    compile "com.github.sommeri:less4j:1.1.1"
+    compile "com.google.javascript:closure-compiler:v20130722"
+    compile "org.mozilla:rhino:1.7R4"
+
+    testCompile project(":tapestry-runner")
+    testCompile "org.gebish:geb-spock:${versions.geb}"
+    testCompile "org.spockframework:spock-core:${versions.spock}"
+
+    testCompile "org.seleniumhq.selenium:selenium-java:${versions.selenium}", {
+        exclude group: "org.eclipse.jetty"
+    }
+    testCompile "org.seleniumhq.selenium:selenium-server:${versions.selenium}", {
+        exclude group: "org.eclipse.jetty"
+    }
+}
+
+jar.manifest {
+    attributes 'Tapestry-Module-Classes': 'org.apache.tapestry5.webresources.modules.WebResourcesModule'
+}
+
+
+test {
+    useJUnit()
+
+    systemProperties("geb.build.reportsDir": "$reporting.baseDir/geb",
+        "tapestry.compiled-asset-cache-dir": "$buildDir/compiled-asset-cache",
+        "tapestry.production-mode": "false")
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/com/yahoo/platform/yui/compressor/CssCompressor.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/com/yahoo/platform/yui/compressor/CssCompressor.java b/tapestry-webresources/src/main/java/com/yahoo/platform/yui/compressor/CssCompressor.java
new file mode 100644
index 0000000..6db09d7
--- /dev/null
+++ b/tapestry-webresources/src/main/java/com/yahoo/platform/yui/compressor/CssCompressor.java
@@ -0,0 +1,490 @@
+/*
+ * YUI Compressor
+ * http://developer.yahoo.com/yui/compressor/
+ * Author: Julien Lecomte -  http://www.julienlecomte.net/
+ * Author: Isaac Schlueter - http://foohack.com/
+ * Author: Stoyan Stefanov - http://phpied.com/
+ * Contributor: Dan Beam - http://danbeam.org/
+ * Copyright (c) 2013 Yahoo! Inc.  All rights reserved.
+ * The copyrights embodied in the content of this file are licensed
+ * by Yahoo! Inc. under the BSD (revised) open source license.
+ */
+package com.yahoo.platform.yui.compressor;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CssCompressor {
+
+    private StringBuffer srcsb = new StringBuffer();
+
+    public CssCompressor(Reader in) throws IOException {
+        // Read the stream...
+        int c;
+        while ((c = in.read()) != -1) {
+            srcsb.append((char) c);
+        }
+    }
+
+    // Leave data urls alone to increase parse performance.
+    protected String extractDataUrls(String css, ArrayList preservedTokens) {
+
+        int maxIndex = css.length() - 1;
+        int appendIndex = 0;
+
+        StringBuffer sb = new StringBuffer();
+
+        Pattern p = Pattern.compile("(?i)url\\(\\s*([\"']?)data\\:");
+        Matcher m = p.matcher(css);
+
+        /*
+         * Since we need to account for non-base64 data urls, we need to handle
+         * ' and ) being part of the data string. Hence switching to indexOf,
+         * to determine whether or not we have matching string terminators and
+         * handling sb appends directly, instead of using matcher.append* methods.
+         */
+
+        while (m.find()) {
+
+            int startIndex = m.start() + 4;      // "url(".length()
+            String terminator = m.group(1);     // ', " or empty (not quoted)
+
+            if (terminator.length() == 0) {
+                terminator = ")";
+            }
+
+            boolean foundTerminator = false;
+
+            int endIndex = m.end() - 1;
+            while(foundTerminator == false && endIndex+1 <= maxIndex) {
+                endIndex = css.indexOf(terminator, endIndex+1);
+
+                if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) {
+                    foundTerminator = true;
+                    if (!")".equals(terminator)) {
+                        endIndex = css.indexOf(")", endIndex);
+                    }
+                }
+            }
+
+            // Enough searching, start moving stuff over to the buffer
+            sb.append(css.substring(appendIndex, m.start()));
+
+            if (foundTerminator) {
+                String token = css.substring(startIndex, endIndex);
+                token = token.replaceAll("\\s+", "");
+                preservedTokens.add(token);
+
+                String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)";
+                sb.append(preserver);
+
+                appendIndex = endIndex + 1;
+            } else {
+                // No end terminator found, re-add the whole match. Should we throw/warn here?
+                sb.append(css.substring(m.start(), m.end()));
+                appendIndex = m.end();
+            }
+        }
+
+        sb.append(css.substring(appendIndex));
+
+        return sb.toString();
+    }
+
+    private String preserveOldIESpecificMatrixDefinition(String css, ArrayList preservedTokens) {
+        StringBuffer sb = new StringBuffer();
+        Pattern p = Pattern.compile("\\s*filter:\\s*progid:DXImageTransform.Microsoft.Matrix\\(([^\\)]+)\\);");
+        Matcher m = p.matcher(css);
+        while (m.find()) {
+            String token = m.group(1);
+            preservedTokens.add(token);
+            String preserver = "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___";
+            m.appendReplacement(sb, "filter:progid:DXImageTransform.Microsoft.Matrix(" + preserver + ");");
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    public void compress(Writer out, int linebreakpos)
+            throws IOException {
+
+        Pattern p;
+        Matcher m;
+        String css = srcsb.toString();
+
+        int startIndex = 0;
+        int endIndex = 0;
+        int i = 0;
+        int max = 0;
+        ArrayList preservedTokens = new ArrayList(0);
+        ArrayList comments = new ArrayList(0);
+        String token;
+        int totallen = css.length();
+        String placeholder;
+
+        css = this.extractDataUrls(css, preservedTokens);
+
+        StringBuffer sb = new StringBuffer(css);
+
+        // collect all comment blocks...
+        while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) {
+            endIndex = sb.indexOf("*/", startIndex + 2);
+            if (endIndex < 0) {
+                endIndex = totallen;
+            }
+
+            token = sb.substring(startIndex + 2, endIndex);
+            comments.add(token);
+            sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___");
+            startIndex += 2;
+        }
+        css = sb.toString();
+
+        // preserve strings so their content doesn't get accidentally minified
+        sb = new StringBuffer();
+        p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')");
+        m = p.matcher(css);
+        while (m.find()) {
+            token = m.group();
+            char quote = token.charAt(0);
+            token = token.substring(1, token.length() - 1);
+
+            // maybe the string contains a comment-like substring?
+            // one, maybe more? put'em back then
+            if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) {
+                for (i = 0, max = comments.size(); i < max; i += 1) {
+                    token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString());
+                }
+            }
+
+            // minify alpha opacity in filter strings
+            token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
+
+            preservedTokens.add(token);
+            String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote;
+            m.appendReplacement(sb, preserver);
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+
+        // strings are safe, now wrestle the comments
+        for (i = 0, max = comments.size(); i < max; i += 1) {
+
+            token = comments.get(i).toString();
+            placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___";
+
+            // ! in the first position of the comment means preserve
+            // so push to the preserved tokens while stripping the !
+            if (token.startsWith("!")) {
+                preservedTokens.add(token);
+                css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
+                continue;
+            }
+
+            // \ in the last position looks like hack for Mac/IE5
+            // shorten that to /*\*/ and the next one to /**/
+            if (token.endsWith("\\")) {
+                preservedTokens.add("\\");
+                css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
+                i = i + 1; // attn: advancing the loop
+                preservedTokens.add("");
+                css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___",  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
+                continue;
+            }
+
+            // keep empty comments after child selectors (IE7 hack)
+            // e.g. html >/**/ body
+            if (token.length() == 0) {
+                startIndex = css.indexOf(placeholder);
+                if (startIndex > 2) {
+                    if (css.charAt(startIndex - 3) == '>') {
+                        preservedTokens.add("");
+                        css = css.replace(placeholder,  "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___");
+                    }
+                }
+            }
+
+            // in all other cases kill the comment
+            css = css.replace("/*" + placeholder + "*/", "");
+        }
+
+
+        // Normalize all whitespace strings to single spaces. Easier to work with that way.
+        css = css.replaceAll("\\s+", " ");
+
+        css = this.preserveOldIESpecificMatrixDefinition(css, preservedTokens);
+
+        // Remove the spaces before the things that should not have spaces before them.
+        // But, be careful not to turn "p :link {...}" into "p:link{...}"
+        // Swap out any pseudo-class colons with the token, and then swap back.
+        sb = new StringBuffer();
+        p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)");
+        m = p.matcher(css);
+        while (m.find()) {
+            String s = m.group();
+            s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___");
+            s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" );
+            m.appendReplacement(sb, s);
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+        // Remove spaces before the things that should not have spaces before them.
+        css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1");
+        // Restore spaces for !important
+        css = css.replaceAll("!important", " !important");
+        // bring back the colon
+        css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":");
+
+        // retain space for special IE6 cases
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i):first\\-(line|letter)(\\{|,)");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, ":first-" + m.group(1).toLowerCase() + " " + m.group(2));
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // no space after the end of a preserved comment
+        css = css.replaceAll("\\*/ ", "*/");
+
+        // If there are multiple @charset directives, push them to the top of the file.
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)^(.*)(@charset)( \"[^\"]*\";)");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, m.group(2).toLowerCase() + m.group(3) + m.group(1));
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // When all @charset are at the top, remove the second and after (as they are completely ignored).
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)^((\\s*)(@charset)( [^;]+;\\s*))+");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, m.group(2) + m.group(3).toLowerCase() + m.group(4));
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // lowercase some popular @directives (@charset is done right above)
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, '@' + m.group(1).toLowerCase());
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // lowercase some more common pseudo-elements
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i):(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, ':' + m.group(1).toLowerCase());
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // lowercase some more common functions
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i):(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\\(");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, ':' + m.group(1).toLowerCase() + '(');
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // lower case some common function that can be values
+        // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)([:,\\( ]\\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, m.group(1) + m.group(2).toLowerCase());
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // Put the space back in some cases, to support stuff like
+        // @media screen and (-webkit-min-device-pixel-ratio:0){
+        css = css.replaceAll("(?i)\\band\\(", "and (");
+
+        // Remove the spaces after the things that should not have spaces after them.
+        css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1");
+
+        // remove unnecessary semicolons
+        css = css.replaceAll(";+}", "}");
+
+        // Replace 0(px,em,%) with 0.
+        css = css.replaceAll("(?i)(^|[^0-9])(?:0?\\.)?0(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)", "$10");
+
+        // Replace 0 0 0 0; with 0.
+        css = css.replaceAll(":0 0 0 0(;|})", ":0$1");
+        css = css.replaceAll(":0 0 0(;|})", ":0$1");
+        css = css.replaceAll(":0 0(;|})", ":0$1");
+
+
+        // Replace background-position:0; with background-position:0 0;
+        // same for transform-origin
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)(background-position|webkit-mask-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2));
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // Replace 0.6 to .6, but only when preceded by : or a white-space
+        css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2");
+
+        // Shorten colors from rgb(51,102,153) to #336699
+        // This makes it more likely that it'll get further compressed in the next step.
+        p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)");
+        m = p.matcher(css);
+        sb = new StringBuffer();
+        while (m.find()) {
+            String[] rgbcolors = m.group(1).split(",");
+            StringBuffer hexcolor = new StringBuffer("#");
+            for (i = 0; i < rgbcolors.length; i++) {
+                int val = Integer.parseInt(rgbcolors[i]);
+                if (val < 16) {
+                    hexcolor.append("0");
+                }
+
+                // If someone passes an RGB value that's too big to express in two characters, round down.
+                // Probably should throw out a warning here, but generating valid CSS is a bigger concern.
+                if (val > 255) {
+                    val = 255;
+                }
+                hexcolor.append(Integer.toHexString(val));
+            }
+            m.appendReplacement(sb, hexcolor.toString());
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // Shorten colors from #AABBCC to #ABC. Note that we want to make sure
+        // the color is not preceded by either ", " or =. Indeed, the property
+        //     filter: chroma(color="#FFFFFF");
+        // would become
+        //     filter: chroma(color="#FFF");
+        // which makes the filter break in IE.
+        // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} )
+        // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD)
+        p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})");
+
+        m = p.matcher(css);
+        sb = new StringBuffer();
+        int index = 0;
+
+        while (m.find(index)) {
+
+            sb.append(css.substring(index, m.start()));
+
+            boolean isFilter = (m.group(1) != null && !"".equals(m.group(1)));
+
+            if (isFilter) {
+                // Restore, as is. Compression will break filters
+                sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7));
+            } else {
+                if( m.group(2).equalsIgnoreCase(m.group(3)) &&
+                        m.group(4).equalsIgnoreCase(m.group(5)) &&
+                        m.group(6).equalsIgnoreCase(m.group(7))) {
+
+                    // #AABBCC pattern
+                    sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase());
+
+                } else {
+
+                    // Non-compressible color, restore, but lower case.
+                    sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase());
+                }
+            }
+
+            index = m.end(7);
+        }
+
+        sb.append(css.substring(index));
+        css = sb.toString();
+
+        // Replace #f00 -> red
+        css = css.replaceAll("(:|\\s)(#f00)(;|})", "$1red$3");
+        // Replace other short color keywords
+        css = css.replaceAll("(:|\\s)(#000080)(;|})", "$1navy$3");
+        css = css.replaceAll("(:|\\s)(#808080)(;|})", "$1gray$3");
+        css = css.replaceAll("(:|\\s)(#808000)(;|})", "$1olive$3");
+        css = css.replaceAll("(:|\\s)(#800080)(;|})", "$1purple$3");
+        css = css.replaceAll("(:|\\s)(#c0c0c0)(;|})", "$1silver$3");
+        css = css.replaceAll("(:|\\s)(#008080)(;|})", "$1teal$3");
+        css = css.replaceAll("(:|\\s)(#ffa500)(;|})", "$1orange$3");
+        css = css.replaceAll("(:|\\s)(#800000)(;|})", "$1maroon$3");
+
+        // border: none -> border:0
+        sb = new StringBuffer();
+        p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|})");
+        m = p.matcher(css);
+        while (m.find()) {
+            m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2));
+        }
+        m.appendTail(sb);
+        css = sb.toString();
+
+        // shorter opacity IE filter
+        css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity=");
+
+        // Find a fraction that is used for Opera's -o-device-pixel-ratio query
+        // Add token to add the "\" back in later
+        css = css.replaceAll("\\(([\\-A-Za-z]+):([0-9]+)\\/([0-9]+)\\)", "($1:$2___YUI_QUERY_FRACTION___$3)");
+
+        // Remove empty rules.
+        css = css.replaceAll("[^\\}\\{/;]+\\{\\}", "");
+
+        // Add "\" back to fix Opera -o-device-pixel-ratio query
+        css = css.replaceAll("___YUI_QUERY_FRACTION___", "/");
+
+        // TODO: Should this be after we re-insert tokens. These could alter the break points. However then
+        // we'd need to make sure we don't break in the middle of a string etc.
+        if (linebreakpos >= 0) {
+            // Some source control tools don't like it when files containing lines longer
+            // than, say 8000 characters, are checked in. The linebreak option is used in
+            // that case to split long lines after a specific column.
+            i = 0;
+            int linestartpos = 0;
+            sb = new StringBuffer(css);
+            while (i < sb.length()) {
+                char c = sb.charAt(i++);
+                if (c == '}' && i - linestartpos > linebreakpos) {
+                    sb.insert(i, '\n');
+                    linestartpos = i;
+                }
+            }
+
+            css = sb.toString();
+        }
+
+        // Replace multiple semi-colons in a row by a single one
+        // See SF bug #1980989
+        css = css.replaceAll(";;+", ";");
+
+        // restore preserved comments and strings
+        for(i = preservedTokens.size() - 1; i >= 0 ; i--) {
+            css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString());
+        }
+
+        // Trim the final string (for any leading or trailing white spaces)
+        css = css.trim();
+
+        // Write the output...
+        out.write(css);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/AbstractMinimizer.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/AbstractMinimizer.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/AbstractMinimizer.java
new file mode 100644
index 0000000..7548bed
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/AbstractMinimizer.java
@@ -0,0 +1,107 @@
+// Copyright 2011-2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.internal.services.assets.BytestreamCache;
+import org.apache.tapestry5.internal.services.assets.StreamableResourceImpl;
+import org.apache.tapestry5.ioc.IOOperation;
+import org.apache.tapestry5.ioc.OperationTracker;
+import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
+import org.apache.tapestry5.services.assets.CompressionStatus;
+import org.apache.tapestry5.services.assets.ResourceMinimizer;
+import org.apache.tapestry5.services.assets.StreamableResource;
+import org.slf4j.Logger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Base class for resource minimizers.
+ *
+ * @since 5.3
+ */
+public abstract class AbstractMinimizer implements ResourceMinimizer
+{
+    private static final double NANOS_TO_MILLIS = 1.0d / 1000000.0d;
+
+    protected final Logger logger;
+
+    protected final OperationTracker tracker;
+
+    private final AssetChecksumGenerator checksumGenerator;
+
+    private final String resourceType;
+
+    public AbstractMinimizer(Logger logger, OperationTracker tracker, AssetChecksumGenerator checksumGenerator, String resourceType)
+    {
+        this.logger = logger;
+        this.tracker = tracker;
+        this.resourceType = resourceType;
+        this.checksumGenerator = checksumGenerator;
+    }
+
+    public StreamableResource minimize(final StreamableResource input) throws IOException
+    {
+        long startNanos = System.nanoTime();
+
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
+
+        tracker.perform("Minimizing " + input, new IOOperation<Void>()
+        {
+            public Void perform() throws IOException
+            {
+                InputStream in = doMinimize(input);
+
+                TapestryInternalUtils.copy(in, bos);
+
+                return null;
+            }
+        });
+
+        // The content is minimized, but can still be (GZip) compressed.
+
+        StreamableResource output = new StreamableResourceImpl("minimized " + input.getDescription(),
+                input.getContentType(), CompressionStatus.COMPRESSABLE,
+                input.getLastModified(), new BytestreamCache(bos), checksumGenerator);
+
+        if (logger.isInfoEnabled())
+        {
+            long elapsedNanos = System.nanoTime() - startNanos;
+
+            int inputSize = input.getSize();
+            int outputSize = output.getSize();
+
+            double elapsedMillis = ((double) elapsedNanos) * NANOS_TO_MILLIS;
+            // e.g., reducing 100 bytes to 25 would be a (100-25)/100 reduction, or 75%
+            double reduction = 100d * ((double) (inputSize - outputSize)) / ((double) inputSize);
+
+            logger.info(String.format("Minimized %s (%,d input bytes of %s to %,d output bytes in %.2f ms, %.2f%% reduction)",
+                    input.getDescription(), inputSize, resourceType, outputSize, elapsedMillis, reduction));
+        }
+
+        return output;
+    }
+
+    /**
+     * Implemented in subclasses to do the actual work.
+     *
+     * @param resource
+     *         content to minimize
+     * @return stream of minimized content
+     */
+    protected abstract InputStream doMinimize(StreamableResource resource) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CSSMinimizer.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CSSMinimizer.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CSSMinimizer.java
new file mode 100644
index 0000000..a33c6e8
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CSSMinimizer.java
@@ -0,0 +1,58 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import com.yahoo.platform.yui.compressor.CssCompressor;
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.ioc.OperationTracker;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
+import org.apache.tapestry5.services.assets.StreamableResource;
+import org.slf4j.Logger;
+
+import java.io.*;
+
+/**
+ * A wrapper around YUI Compressor. This module does not have a dependency on YUICompressor;
+ * isntead a local copy of the YUICompressor CSS minimizer is kept (because the reset of YUICompressor
+ * is painful to mix due to how it attempts to patch Rhino).
+ */
+public class CSSMinimizer extends AbstractMinimizer
+{
+    public CSSMinimizer(Logger logger, OperationTracker tracker, AssetChecksumGenerator checksumGenerator)
+    {
+        super(logger, tracker, checksumGenerator, "text/css");
+    }
+
+    @Override
+    protected InputStream doMinimize(StreamableResource resource) throws IOException
+    {
+        StringWriter writer = new StringWriter(1000);
+        Reader reader = new InputStreamReader(resource.openStream());
+
+        try
+        {
+            new CssCompressor(reader).compress(writer, -1);
+
+            writer.flush();
+
+            return IOUtils.toInputStream(writer.getBuffer());
+        } finally
+        {
+            InternalUtils.close(reader);
+            InternalUtils.close(writer);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CacheMode.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CacheMode.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CacheMode.java
new file mode 100644
index 0000000..b2dfee6
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CacheMode.java
@@ -0,0 +1,40 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+/**
+ * Controls caching for {@link ResourceTransformerFactory} in <em>development mode</em>. In production mode, caching at this
+ * level is not needed, because artifacts are also cached later in the pipeline. This caching is all about avoid unwanted
+ */
+public enum CacheMode
+{
+    /**
+     * Cache the content on the file system, in the directory defined by {@link org.apache.tapestry5.webresources.WebResourcesSymbols#CACHE_DIR}.
+     * This allows compilation to be avoided even after a restart, as long as the source file has not changed. This only works
+     * for compilations that operate on a single file (such as CoffeeScript, but not Less, which has an {@code @import} statement).
+     */
+    SINGLE_FILE,
+
+    /**
+     * The source may be multiple files (e.g., Less). Cache in memory, and invalidate the cache if any of the multiple
+     * file's content changes.
+     */
+    MULTIPLE_FILE,
+
+    /**
+     * Do no caching. This is appropriate for extremely cheap compilers.
+     */
+    NONE;
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CoffeeScriptCompiler.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CoffeeScriptCompiler.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CoffeeScriptCompiler.java
new file mode 100644
index 0000000..849fc22
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/CoffeeScriptCompiler.java
@@ -0,0 +1,82 @@
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.annotations.Path;
+import org.apache.tapestry5.ioc.OperationTracker;
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.services.assets.ResourceDependencies;
+import org.apache.tapestry5.services.assets.ResourceTransformer;
+import org.mozilla.javascript.NativeObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+
+public class CoffeeScriptCompiler implements ResourceTransformer
+{
+    private final static Charset UTF8 = Charset.forName("utf-8");
+
+    private final RhinoExecutorPool executorPool;
+
+    public String getTransformedContentType()
+    {
+        return "text/javascript";
+    }
+
+    public CoffeeScriptCompiler(@Path("classpath:org/apache/tapestry5/webresources/internal/coffeescript-1.6.3.js")
+                                Resource mainCompiler,
+                                @Path("classpath:org/apache/tapestry5/webresources/internal/invoke-coffeescript.js")
+                                Resource shim,
+                                OperationTracker tracker)
+    {
+
+        executorPool = new RhinoExecutorPool(tracker, toList(mainCompiler, shim));
+    }
+
+    private List<Resource> toList(Resource... resources)
+    {
+        List<Resource> list = CollectionFactory.newList();
+
+        for (Resource r : resources)
+        {
+            list.add(r);
+        }
+
+        return list;
+    }
+
+
+    private String getString(NativeObject object, String key)
+    {
+        return object.get(key).toString();
+    }
+
+
+    public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
+    {
+        String content = IOUtils.toString(source.openStream(), UTF8);
+
+        RhinoExecutor executor = executorPool.get();
+
+        try
+        {
+
+            NativeObject result = (NativeObject) executor.invokeFunction("compileCoffeeScriptSource", content, source.toString());
+
+            if (result.containsKey("exception"))
+            {
+                throw new RuntimeException(getString(result, "exception"));
+            }
+
+            return IOUtils.toInputStream(getString(result, "output"), UTF8);
+
+        } finally
+        {
+            executor.discard();
+        }
+
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ContentChangeTracker.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ContentChangeTracker.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ContentChangeTracker.java
new file mode 100644
index 0000000..cff26f8
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ContentChangeTracker.java
@@ -0,0 +1,61 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.services.assets.ResourceDependencies;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Manages a collection of Resources and can check to see if any resource's actual content has changed.
+ *
+ * @since 5.4
+ */
+public class ContentChangeTracker implements ResourceDependencies
+{
+    private final Map<Resource, Long> checksums = CollectionFactory.newMap();
+
+    public void addDependency(Resource dependency)
+    {
+        long checksum = ResourceTransformUtils.toChecksum(dependency);
+
+        checksums.put(dependency, checksum);
+    }
+
+    /**
+     * Checks all resources tracked by this instance and returns true if any resource's content has changed.
+     *
+     * @return true if a change has occurred
+     */
+    public boolean dirty() throws IOException
+    {
+        for (Map.Entry<Resource, Long> e : checksums.entrySet())
+        {
+            long current = ResourceTransformUtils.toChecksum(e.getKey());
+
+            if (current != e.getValue().longValue())
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/GoogleClosureMinimizer.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/GoogleClosureMinimizer.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/GoogleClosureMinimizer.java
new file mode 100644
index 0000000..edd484f
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/GoogleClosureMinimizer.java
@@ -0,0 +1,79 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import com.google.javascript.jscomp.*;
+import com.google.javascript.jscomp.Compiler;
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.ioc.OperationTracker;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.services.assets.AssetChecksumGenerator;
+import org.apache.tapestry5.services.assets.StreamableResource;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * A wrapper around the Google Closure {@link Compiler} used to minimize
+ * a JavaScript resource.
+ */
+public class GoogleClosureMinimizer extends AbstractMinimizer
+{
+    private final List<SourceFile> EXTERNS = Collections.emptyList();
+
+    static
+    {
+        Compiler.setLoggingLevel(Level.SEVERE);
+    }
+
+    public GoogleClosureMinimizer(Logger logger, OperationTracker tracker, AssetChecksumGenerator checksumGenerator)
+    {
+        super(logger, tracker, checksumGenerator, "text/javascript");
+    }
+
+    @Override
+    protected InputStream doMinimize(StreamableResource resource) throws IOException
+    {
+        // Don't bother to pool the Compiler
+
+        CompilerOptions options = new CompilerOptions();
+        options.setCodingConvention(new ClosureCodingConvention());
+        options.setOutputCharset("utf-8");
+        options.setWarningLevel(DiagnosticGroups.CHECK_VARIABLES, CheckLevel.WARNING);
+
+        Compiler compiler = new Compiler();
+
+        compiler.disableThreads();
+
+        SourceFile input = SourceFile.fromInputStream(resource.toString(), resource.openStream());
+
+        List<SourceFile> inputs = Collections.singletonList(input);
+
+        Result result = compiler.compile(EXTERNS, inputs, options);
+
+        if (result.success)
+        {
+            return IOUtils.toInputStream(compiler.toSource());
+        }
+
+        throw new RuntimeException(String.format("Compilation failed: %s.",
+                InternalUtils.join(CollectionFactory.newList(result.errors), ";")));
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/LessResourceTransformer.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/LessResourceTransformer.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/LessResourceTransformer.java
new file mode 100644
index 0000000..5bd80af
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/LessResourceTransformer.java
@@ -0,0 +1,120 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import com.github.sommeri.less4j.Less4jException;
+import com.github.sommeri.less4j.LessCompiler;
+import com.github.sommeri.less4j.LessSource;
+import com.github.sommeri.less4j.core.DefaultLessCompiler;
+import org.apache.commons.io.IOUtils;
+import org.apache.tapestry5.internal.services.assets.BytestreamCache;
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.services.assets.ResourceDependencies;
+import org.apache.tapestry5.services.assets.ResourceTransformer;
+
+import java.io.*;
+
+/**
+ * Direct wrapper around the LessCompiler, so that Less source files may use {@code @import}, which isn't
+ * supported by the normal WRO4J processor.
+ */
+public class LessResourceTransformer implements ResourceTransformer
+{
+    private final LessCompiler compiler = new DefaultLessCompiler();
+
+    public String getTransformedContentType()
+    {
+        return "text/css";
+    }
+
+    class ResourceLessSource extends LessSource
+    {
+        private final Resource resource;
+
+        private final ResourceDependencies dependencies;
+
+
+        ResourceLessSource(Resource resource, ResourceDependencies dependencies)
+        {
+            this.resource = resource;
+            this.dependencies = dependencies;
+        }
+
+        @Override
+        public LessSource relativeSource(String filename) throws FileNotFound, CannotReadFile, StringSourceException
+        {
+            Resource relative = resource.forFile(filename);
+
+            if (!relative.exists())
+            {
+                throw new FileNotFound();
+            }
+
+            dependencies.addDependency(relative);
+
+            return new ResourceLessSource(relative, dependencies);
+        }
+
+        @Override
+        public String getContent() throws FileNotFound, CannotReadFile
+        {
+            // Adapted from Less's URLSource
+            try
+            {
+                Reader input = new InputStreamReader(resource.openStream());
+                String content = IOUtils.toString(input).replace("\r\n", "\n");
+
+                input.close();
+
+                return content;
+            } catch (FileNotFoundException ex)
+            {
+                throw new FileNotFound();
+            } catch (IOException ex)
+            {
+                throw new CannotReadFile();
+            }
+        }
+    }
+
+
+    public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
+    {
+        BytestreamCache compiled = invokeLessCompiler(source, dependencies);
+
+        return compiled.openStream();
+    }
+
+    private BytestreamCache invokeLessCompiler(Resource source, ResourceDependencies dependencies) throws IOException
+    {
+        try
+        {
+            LessSource lessSource = new ResourceLessSource(source, dependencies);
+
+            LessCompiler.CompilationResult compilationResult = compiler.compile(lessSource);
+
+            // Currently, ignoring any warnings.
+
+            return new BytestreamCache(compilationResult.getCss().getBytes("utf-8"));
+
+        } catch (Less4jException ex)
+        {
+            throw new IOException(ex);
+        } catch (UnsupportedEncodingException ex)
+        {
+            throw new IOException(ex);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceDependenciesSplitter.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceDependenciesSplitter.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceDependenciesSplitter.java
new file mode 100644
index 0000000..6363a7a
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceDependenciesSplitter.java
@@ -0,0 +1,40 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.services.assets.ResourceDependencies;
+
+/**
+ * A wrapper around two ResourceDependencies.
+ *
+ * @since 5.4
+ */
+public class ResourceDependenciesSplitter implements ResourceDependencies
+{
+    private final ResourceDependencies left, right;
+
+    public ResourceDependenciesSplitter(ResourceDependencies left, ResourceDependencies right)
+    {
+        this.left = left;
+        this.right = right;
+    }
+
+    public void addDependency(Resource dependency)
+    {
+        left.addDependency(dependency);
+        right.addDependency(dependency);
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformUtils.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformUtils.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformUtils.java
new file mode 100644
index 0000000..aeb9c27
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformUtils.java
@@ -0,0 +1,68 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.ioc.Resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Adler32;
+
+/**
+ * @since 5.4
+ */
+public class ResourceTransformUtils
+{
+    private static final double NANOS_TO_MILLIS = 1.0d / 1000000.0d;
+
+    public static double nanosToMillis(long nanos)
+    {
+        return ((double) nanos) * NANOS_TO_MILLIS;
+    }
+
+    public static long toChecksum(Resource resource)
+    {
+        Adler32 checksum = new Adler32();
+
+        byte[] buffer = new byte[1024];
+
+        InputStream is = null;
+
+        try
+        {
+            is = resource.openStream();
+
+            while (true)
+            {
+                int length = is.read(buffer);
+
+                if (length < 0)
+                {
+                    break;
+                }
+
+                checksum.update(buffer, 0, length);
+            }
+
+            is.close();
+
+            // Reduces it down to just 32 bits which we express in hex.'
+            return checksum.getValue();
+        } catch (IOException ex)
+        {
+            throw new RuntimeException(ex);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactory.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactory.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactory.java
new file mode 100644
index 0000000..c509cce
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactory.java
@@ -0,0 +1,45 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.services.assets.ResourceTransformer;
+
+/**
+ * Creates ResourceTransformer around a named {@link org.apache.tapestry5.webresources.services.ResourceProcessor}.
+ *
+ * @see org.apache.tapestry5.services.assets.StreamableResourceSource
+ * @since 5.4
+ */
+public interface ResourceTransformerFactory
+{
+
+    /**
+     * Constructs a compiler around a another ResourceTransformer implementation. In development mode, the wrapped version
+     * will handle caching, as well as logging output of timing for the real implementation.
+     *
+     * @param sourceName
+     *         for debugging: source name, e.g., "Less"
+     * @param targetName
+     *         for debugging: target name, e.g., "CSS"
+     * @param transformer
+     *         performs the actual work
+     * @param cacheMode
+     *         Indicates if and how the compiled content should be cached (in development mode only)
+     * @return transformer
+     * @see org.apache.tapestry5.webresources.services.ResourceProcessorSource
+     */
+    ResourceTransformer createCompiler(String contentType, String sourceName, String targetName, ResourceTransformer transformer, CacheMode cacheMode);
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactoryImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactoryImpl.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactoryImpl.java
new file mode 100644
index 0000000..f02d94d
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/ResourceTransformerFactoryImpl.java
@@ -0,0 +1,269 @@
+// Copyright 2013 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.webresources;
+
+import org.apache.tapestry5.SymbolConstants;
+import org.apache.tapestry5.internal.TapestryInternalUtils;
+import org.apache.tapestry5.internal.services.assets.BytestreamCache;
+import org.apache.tapestry5.ioc.IOOperation;
+import org.apache.tapestry5.ioc.OperationTracker;
+import org.apache.tapestry5.ioc.Resource;
+import org.apache.tapestry5.ioc.annotations.PostInjection;
+import org.apache.tapestry5.ioc.annotations.Symbol;
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.services.assets.ResourceDependencies;
+import org.apache.tapestry5.services.assets.ResourceTransformer;
+import org.apache.tapestry5.webresources.WebResourcesSymbols;
+import org.slf4j.Logger;
+
+import java.io.*;
+import java.util.Map;
+
+public class ResourceTransformerFactoryImpl implements ResourceTransformerFactory
+{
+    private final Logger logger;
+
+    private final OperationTracker tracker;
+
+    private final boolean productionMode;
+
+    private final File cacheDir;
+
+    public ResourceTransformerFactoryImpl(Logger logger, OperationTracker tracker,
+                                          @Symbol(SymbolConstants.PRODUCTION_MODE)
+                                          boolean productionMode,
+                                          @Symbol(WebResourcesSymbols.CACHE_DIR)
+                                          String cacheDir)
+    {
+        this.logger = logger;
+        this.tracker = tracker;
+        this.productionMode = productionMode;
+
+        this.cacheDir = new File(cacheDir);
+
+        if (!productionMode)
+        {
+            logger.info(String.format("Using %s to store compiled assets (development mode only).", cacheDir));
+        }
+    }
+
+    @PostInjection
+    public void createCacheDir()
+    {
+        cacheDir.mkdirs();
+    }
+
+    static class Compiled extends ContentChangeTracker
+    {
+        private BytestreamCache bytestreamCache;
+
+        Compiled(Resource root)
+        {
+            addDependency(root);
+        }
+
+        void store(InputStream stream) throws IOException
+        {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+            TapestryInternalUtils.copy(stream, bos);
+
+            stream.close();
+            bos.close();
+
+            this.bytestreamCache = new BytestreamCache(bos);
+        }
+
+        InputStream openStream()
+        {
+            return bytestreamCache.openStream();
+        }
+    }
+
+
+    public ResourceTransformer createCompiler(String contentType, String sourceName, String targetName, ResourceTransformer transformer, CacheMode cacheMode)
+    {
+        ResourceTransformer trackingCompiler = wrapWithTracking(sourceName, targetName, transformer);
+
+        if (productionMode)
+        {
+            return trackingCompiler;
+        }
+
+        ResourceTransformer timingCompiler = wrapWithTiming(targetName, trackingCompiler);
+
+        switch (cacheMode)
+        {
+            case NONE:
+
+                return timingCompiler;
+
+            case SINGLE_FILE:
+
+                return wrapWithFileSystemCaching(timingCompiler, targetName);
+
+            case MULTIPLE_FILE:
+
+                return wrapWithInMemoryCaching(timingCompiler, targetName);
+
+            default:
+
+                throw new IllegalStateException();
+        }
+    }
+
+    private ResourceTransformer wrapWithTracking(final String sourceName, final String targetName, final ResourceTransformer core)
+    {
+        return new ResourceTransformer()
+        {
+            public String getTransformedContentType()
+            {
+                return core.getTransformedContentType();
+            }
+
+            public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException
+            {
+                final String description = String.format("Compiling %s from %s to %s", source, sourceName, targetName);
+
+                return tracker.perform(description, new IOOperation<InputStream>()
+                {
+                    public InputStream perform() throws IOException
+                    {
+                        return core.transform(source, dependencies);
+                    }
+                });
+            }
+        };
+    }
+
+    private ResourceTransformer wrapWithTiming(final String targetName, final ResourceTransformer coreCompiler)
+    {
+        return new ResourceTransformer()
+        {
+            public String getTransformedContentType()
+            {
+                return coreCompiler.getTransformedContentType();
+            }
+
+            public InputStream transform(final Resource source, final ResourceDependencies dependencies) throws IOException
+            {
+                final long startTime = System.nanoTime();
+
+                InputStream result = coreCompiler.transform(source, dependencies);
+
+                final long elapsedTime = System.nanoTime() - startTime;
+
+                logger.info(String.format("Compiled %s to %s in %.2f ms",
+                        source, targetName,
+                        ResourceTransformUtils.nanosToMillis(elapsedTime)));
+
+                return result;
+            }
+        };
+    }
+
+    /**
+     * Caching is not needed in production, because caching of streamable resources occurs at a higher level
+     * (possibly after sources have been aggregated and minimized and gzipped). However, in development, it is
+     * very important to avoid costly CoffeeScript compilation (or similar operations); Tapestry's caching is
+     * somewhat primitive: a change to *any* resource in a given domain results in the cache of all of those resources
+     * being discarded.
+     */
+    private ResourceTransformer wrapWithInMemoryCaching(final ResourceTransformer core, final String targetName)
+    {
+        return new ResourceTransformer()
+        {
+            final Map<Resource, Compiled> cache = CollectionFactory.newConcurrentMap();
+
+            public String getTransformedContentType()
+            {
+                return core.getTransformedContentType();
+            }
+
+            public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
+            {
+                Compiled compiled = cache.get(source);
+
+                if (compiled != null && !compiled.dirty())
+                {
+                    logger.info(String.format("Resource %s and dependencies are unchanged; serving compiled %s content from in-memory cache",
+                            source, targetName));
+
+                    return compiled.openStream();
+                }
+
+                compiled = new Compiled(source);
+
+                InputStream is = core.transform(source, new ResourceDependenciesSplitter(dependencies, compiled));
+
+                compiled.store(is);
+
+                cache.put(source, compiled);
+
+                return compiled.openStream();
+            }
+        };
+    }
+
+    private ResourceTransformer wrapWithFileSystemCaching(final ResourceTransformer core, final String targetName)
+    {
+        return new ResourceTransformer()
+        {
+            public String getTransformedContentType()
+            {
+                return core.getTransformedContentType();
+            }
+
+            public InputStream transform(Resource source, ResourceDependencies dependencies) throws IOException
+            {
+                long checksum = ResourceTransformUtils.toChecksum(source);
+
+                String fileName = Long.toHexString(checksum) + "-" + source.getFile();
+
+                File cacheFile = new File(cacheDir, fileName);
+
+                if (cacheFile.exists())
+                {
+                    logger.debug(String.format("Serving up compiled %s content for %s from file system cache", targetName, source));
+
+                    return new BufferedInputStream(new FileInputStream(cacheFile));
+                }
+
+                InputStream compiled = core.transform(source, dependencies);
+
+                // We need the InputStream twice; once to return, and once to write out to the cache file for later.
+
+                ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+                TapestryInternalUtils.copy(compiled, bos);
+
+                BytestreamCache cache = new BytestreamCache(bos);
+
+                writeToCacheFile(cacheFile, cache.openStream());
+
+                return cache.openStream();
+            }
+        };
+    }
+
+    private void writeToCacheFile(File file, InputStream stream) throws IOException
+    {
+        OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file));
+
+        TapestryInternalUtils.copy(stream, outputStream);
+
+        outputStream.close();
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/6e42b37c/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/RhinoExecutor.java
----------------------------------------------------------------------
diff --git a/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/RhinoExecutor.java b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/RhinoExecutor.java
new file mode 100644
index 0000000..7f884fa
--- /dev/null
+++ b/tapestry-webresources/src/main/java/org/apache/tapestry5/internal/webresources/RhinoExecutor.java
@@ -0,0 +1,24 @@
+package org.apache.tapestry5.internal.webresources;
+
+
+import org.mozilla.javascript.ScriptableObject;
+
+public interface RhinoExecutor
+{
+    /**
+     * Invokes the named function, which must return a scriptable object (typically, a JavaScript Object).
+     *
+     * @param functionName
+     *         name of function visible to the executor's scope (e.g., loaded from the scripts associated
+     *         with the executor).
+     * @param arguments
+     *         Arguments to pass to the object which must be convertable to JavaScript types; Strings work well here.
+     * @return result of invoking the function.
+     */
+    ScriptableObject invokeFunction(String functionName, Object... arguments);
+
+    /**
+     * Discards the executor, returning it to the pool for reuse.
+     */
+    void discard();
+}