Add MHTML / RFC 2557 support to ClientBundle for ie6 user.agent.
Enable data: urls for ie8 user.agent.
Update documentation location in CssResource JavaDoc.

Patch by: bobv
Review by: rjrjr


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5475 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/resources/Resources.gwt.xml b/user/src/com/google/gwt/resources/Resources.gwt.xml
index 8e93b39..1db713a 100644
--- a/user/src/com/google/gwt/resources/Resources.gwt.xml
+++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -23,7 +23,7 @@
   <define-property name="ClientBundle.enableInlining" values="true,false" />
   <set-property name="ClientBundle.enableInlining" value="true" />
 
-  <!-- Specify the default behavior -->
+  <!-- Specify the default behavior which should work on all browsers -->
   <generate-with
     class="com.google.gwt.resources.rebind.context.StaticClientBundleGenerator">
 
@@ -34,6 +34,24 @@
 
   <!-- Last-matches wins, so this will selectively override the previous rule -->
   <generate-with
+    class="com.google.gwt.resources.rebind.context.MhtmlClientBundleGenerator">
+
+    <!-- We have a number of conditions that must be satisfied -->
+    <all>
+      <!-- Is inlining enabled? -->
+      <when-property-is name="ClientBundle.enableInlining" value="true" />
+
+      <!-- Again, it's necessary to specify which types the generator runs on -->
+      <when-type-assignable
+        class="com.google.gwt.resources.client.ClientBundle" />
+
+      <!-- Do this only with IE6 browsers -->
+      <when-property-is name="user.agent" value="ie6" />
+    </all>
+  </generate-with>
+
+  <!-- Last-matches wins, so this will selectively override the previous rule -->
+  <generate-with
     class="com.google.gwt.resources.rebind.context.InlineClientBundleGenerator">
 
     <!-- We have a number of conditions that must be satisfied -->
@@ -51,6 +69,7 @@
         <when-property-is name="user.agent" value="opera" />
         <when-property-is name="user.agent" value="gecko" />
         <when-property-is name="user.agent" value="gecko1_8" />
+        <when-property-is name="user.agent" value="ie8" />
       </any>
     </all>
   </generate-with>
diff --git a/user/src/com/google/gwt/resources/client/CssResource.java b/user/src/com/google/gwt/resources/client/CssResource.java
index ab03971..f56efdc 100644
--- a/user/src/com/google/gwt/resources/client/CssResource.java
+++ b/user/src/com/google/gwt/resources/client/CssResource.java
@@ -90,8 +90,7 @@
  * </li>
  * </ul>
  * 
- * @see <a
- *      href="http://code.google.com/p/google-web-toolkit-incubator/wiki/CssResource"
+ * @see <a href="http://code.google.com/p/google-web-toolkit/wiki/CssResource"
  *      >CssResource design doc</a>
  */
 @ResourceGeneratorType(CssResourceGenerator.class)
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
index 86cfa6d..7ff745d 100644
--- a/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
@@ -170,6 +170,7 @@
     FieldsImpl fields = new FieldsImpl();
     RequirementsImpl requirements = new RequirementsImpl(
         generatorContext.getPropertyOracle());
+    doAddFieldsAndRequirements(logger, generatorContext, fields, requirements);
 
     /*
      * Initialize the ResourceGenerators and prepare them for subsequent code
@@ -230,6 +231,7 @@
     }
 
     finish(logger, resourceContext, generators.keySet());
+    doFinish();
 
     // Return the name of the concrete class
     return createdClassName;
@@ -241,7 +243,23 @@
    * custom logic in the resource generation pass.
    */
   protected abstract AbstractResourceContext createResourceContext(
-      TreeLogger logger, GeneratorContext context, JClassType resourceBundleType);
+      TreeLogger logger, GeneratorContext context, JClassType resourceBundleType)
+      throws UnableToCompleteException;
+
+  /**
+   * Provides a hook for subtypes to add additional fields or requirements to
+   * the bundle.
+   */
+  protected void doAddFieldsAndRequirements(TreeLogger logger,
+      GeneratorContext context, ClientBundleFields fields,
+      ClientBundleRequirements requirements) throws UnableToCompleteException {
+  }
+
+  /**
+   * Provides a hook for finalizing generated resources.
+   */
+  protected void doFinish() throws UnableToCompleteException {
+  }
 
   /**
    * Create fields and assignments for a single ResourceGenerator.
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
index 9e4e549..458ee59 100644
--- a/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
@@ -17,13 +17,32 @@
 
 import com.google.gwt.core.ext.GeneratorContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.dev.util.Util;
 import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+
+import java.io.IOException;
+import java.net.URL;
 
 /**
  * Defines base methods for ResourceContext implementations.
  */
 public abstract class AbstractResourceContext implements ResourceContext {
+  /**
+   * The largest file size that will be inlined. Note that this value is taken
+   * before any encodings are applied.
+   */
+  protected static final int MAX_INLINE_SIZE = 2 << 15;
+
+  protected static String toBase64(byte[] data) {
+    // This is bad, but I am lazy and don't want to write _another_ encoder
+    sun.misc.BASE64Encoder enc = new sun.misc.BASE64Encoder();
+    String base64Contents = enc.encode(data).replaceAll("\\s+", "");
+    return base64Contents;
+  }
+
   private final TreeLogger logger;
   private final GeneratorContext context;
   private final JClassType resourceBundleType;
@@ -36,6 +55,20 @@
     this.resourceBundleType = resourceBundleType;
   }
 
+  public String deploy(URL resource, boolean xhrCompatible)
+      throws UnableToCompleteException {
+    String fileName = ResourceGeneratorUtil.baseName(resource);
+    byte[] bytes = Util.readURLAsBytes(resource);
+    try {
+      return deploy(fileName, resource.openConnection().getContentType(),
+          bytes, xhrCompatible);
+    } catch (IOException e) {
+      getLogger().log(TreeLogger.ERROR,
+          "Unable to determine mime type of resource", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
   public JClassType getClientBundleType() {
     return resourceBundleType;
   }
@@ -52,6 +85,10 @@
     return simpleSourceName;
   }
 
+  protected GeneratorContext getContext() {
+    return context;
+  }
+
   protected TreeLogger getLogger() {
     return logger;
   }
diff --git a/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java
index 53aef65..f39d190 100644
--- a/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java
+++ b/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java
@@ -21,15 +21,6 @@
 import com.google.gwt.core.ext.typeinfo.JClassType;
 
 class InlineResourceContext extends StaticResourceContext {
-  /**
-   * The largest file size that will be inlined. Note that this value is taken
-   * before any encodings are applied.
-   */
-  // The JLS specifies a maximum size for any string to be 2^16 characters, so
-  // we'll leave some padding. Assuming a Base64 encoding, it is true that
-  // (2 ^ 15) * 4/3 < 2 ^ 16, so we can safely inline files up to 32k.
-  private static final int MAX_INLINE_SIZE = 2 << 15;
-
   InlineResourceContext(TreeLogger logger, GeneratorContext context,
       JClassType resourceBundleType) {
     super(logger, context, resourceBundleType);
@@ -44,9 +35,7 @@
     if ((!xhrCompatible) && (data.length < MAX_INLINE_SIZE)) {
       logger.log(TreeLogger.DEBUG, "Inlining", null);
 
-      // This is bad, but I am lazy and don't want to write _another_ encoder
-      sun.misc.BASE64Encoder enc = new sun.misc.BASE64Encoder();
-      String base64Contents = enc.encode(data).replaceAll("\\s+", "");
+      String base64Contents = toBase64(data);
 
       return "\"data:" + mimeType + ";base64," + base64Contents + "\"";
     } else {
diff --git a/user/src/com/google/gwt/resources/rebind/context/MhtmlClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/MhtmlClientBundleGenerator.java
new file mode 100644
index 0000000..4ca979a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/MhtmlClientBundleGenerator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * 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 com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.TypeOracleException;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.resources.ext.ClientBundleFields;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+
+/**
+ * Generates Multipart HTML files.
+ */
+public class MhtmlClientBundleGenerator extends AbstractClientBundleGenerator {
+
+  private static final String BUNDLE_EXTENSION = ".cache.txt";
+
+  private MhtmlResourceContext resourceContext;
+  private String partialPath;
+
+  @Override
+  protected AbstractResourceContext createResourceContext(TreeLogger logger,
+      GeneratorContext context, JClassType resourceBundleType) {
+    resourceContext = new MhtmlResourceContext(logger, context,
+        resourceBundleType);
+
+    /*
+     * TODO: figure out how to make the filename stable based on actual content.
+     */
+    partialPath = Util.computeStrongName(Util.getBytes(resourceBundleType.getQualifiedSourceName()
+        + System.currentTimeMillis()))
+        + BUNDLE_EXTENSION;
+    resourceContext.setPartialPath(partialPath);
+
+    return resourceContext;
+  }
+
+  @Override
+  protected void doAddFieldsAndRequirements(TreeLogger logger,
+      GeneratorContext generatorContext, ClientBundleFields fields,
+      ClientBundleRequirements requirements) throws UnableToCompleteException {
+    JType booleanType;
+    JType stringType;
+    try {
+      booleanType = generatorContext.getTypeOracle().parse("boolean");
+      stringType = generatorContext.getTypeOracle().parse("java.lang.String");
+    } catch (TypeOracleException e) {
+      logger.log(TreeLogger.ERROR, "Expected type not in type oracle", e);
+      throw new UnableToCompleteException();
+    }
+
+    // GWT.getModuleBaseURL().startsWith("https")
+    String isHttpsIdent = fields.define(booleanType, "isHttps",
+        "GWT.getModuleBaseURL().startsWith(\"https\")", true, true);
+    resourceContext.setIsHttpsIdent(isHttpsIdent);
+
+    // "mhtml:" + GWT.getModuleBaseURL() + "partialPath!cid:"
+    String bundleBaseIdent = fields.define(stringType, "bundleBase",
+        "\"mhtml:\" + GWT.getModuleBaseURL() + \"" + partialPath + "!cid:\"",
+        true, true);
+    resourceContext.setBundleBaseIdent(bundleBaseIdent);
+  }
+
+  @Override
+  protected void doFinish() throws UnableToCompleteException {
+    resourceContext.finish();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/MhtmlResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/MhtmlResourceContext.java
new file mode 100644
index 0000000..2707ab7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/MhtmlResourceContext.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * 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 com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+/**
+ * Encodes resources into Multipart HTML files. In order to avoid mixed-content
+ * warnings using the mhtml: protocol, this context will include a fallback to
+ * static files when the module has been loaded from an https source.
+ * 
+ * @see "RFC 2557"
+ */
+public class MhtmlResourceContext extends StaticResourceContext {
+
+  /**
+   * The MIME multipart boundary token. This is chosen so that it does not
+   * overlap with any possible base64 sequences.
+   */
+  private static final String BOUNDARY = "_GWT";
+  private static final String ID_PREFIX = "r";
+
+  private String bundleBaseIdent;
+  private int id = 0;
+  private String isHttpsIdent;
+
+  /**
+   * Output is lazily initialized in the case that all deployed resources are
+   * large.
+   */
+  private OutputStream out;
+  private String partialPath;
+  private PrintWriter pw;
+
+  MhtmlResourceContext(TreeLogger logger, GeneratorContext context,
+      JClassType resourceBundleType) {
+    super(logger, context, resourceBundleType);
+  }
+
+  @Override
+  public String deploy(String suggestedFileName, String mimeType, byte[] data,
+      boolean xhrCompatible) throws UnableToCompleteException {
+
+    assert partialPath != null : "partialPath";
+    assert isHttpsIdent != null : "isHttpsIdent";
+    assert bundleBaseIdent != null : "bundleBaseIdent";
+
+    /*
+     * mhtml URLs don't work in HTTPS, so we'll always need the static versions
+     * as a fallback.
+     */
+    String staticLocation = super.deploy(suggestedFileName, mimeType, data,
+        xhrCompatible);
+
+    /*
+     * ie6 doesn't treat XHRs to mhtml as cross-site, but ie8 does, so we'll
+     * play it safe here.
+     */
+    if (xhrCompatible || data.length > MAX_INLINE_SIZE) {
+      return staticLocation;
+    }
+
+    if (out == null) {
+      out = getContext().tryCreateResource(getLogger(), partialPath);
+      pw = new PrintWriter(out);
+      pw.println("Content-Type: multipart/related; boundary=\"" + BOUNDARY
+          + "\"");
+      pw.println();
+    }
+
+    String location = ID_PREFIX + id++;
+    String base64 = toBase64(data);
+
+    pw.println("--" + BOUNDARY);
+    pw.println("Content-Id:<" + location + ">");
+    pw.println("Content-Type:" + mimeType);
+    pw.println("Content-Transfer-Encoding:base64");
+    pw.println();
+    pw.println(base64);
+    pw.println();
+
+    /*
+     * Return a Java expression:
+     * 
+     * isHttps ? (staticLocation) : (bundleBaseIdent + "location")
+     */
+    return isHttpsIdent + " ? (" + staticLocation + ") : (" + bundleBaseIdent
+        + " + \"" + location + "\")";
+  }
+
+  public void finish() throws UnableToCompleteException {
+    if (out != null) {
+      pw.close();
+      getContext().commitResource(getLogger(), out);
+    }
+  }
+
+  @Override
+  public boolean supportsDataUrls() {
+    return true;
+  }
+
+  void setBundleBaseIdent(String ident) {
+    bundleBaseIdent = ident;
+  }
+
+  void setIsHttpsIdent(String ident) {
+    isHttpsIdent = ident;
+  }
+
+  void setPartialPath(String partialPath) {
+    this.partialPath = partialPath;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java
index 047e0dd..4f9ce27 100644
--- a/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java
+++ b/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java
@@ -23,11 +23,9 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.dev.util.Util;
-import com.google.gwt.resources.ext.ResourceGeneratorUtil;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.net.URL;
 
 class StaticResourceContext extends AbstractResourceContext {
   /**
@@ -50,8 +48,7 @@
     // See if filename obfuscation should be enabled
     String enableRenaming = null;
     try {
-      ConfigurationProperty prop = propertyOracle.getConfigurationProperty(
-          ENABLE_RENAMING);
+      ConfigurationProperty prop = propertyOracle.getConfigurationProperty(ENABLE_RENAMING);
       enableRenaming = prop.getValues().get(0);
     } catch (BadPropertyValueException e) {
       logger.log(TreeLogger.ERROR, "Bad value for " + ENABLE_RENAMING, e);
@@ -105,20 +102,6 @@
     return "GWT.getModuleBaseURL() + \"" + outputName + "\"";
   }
 
-  public String deploy(URL resource, boolean xhrCompatible)
-      throws UnableToCompleteException {
-    String fileName = ResourceGeneratorUtil.baseName(resource);
-    byte[] bytes = Util.readURLAsBytes(resource);
-    try {
-      return deploy(fileName, resource.openConnection().getContentType(),
-          bytes, xhrCompatible);
-    } catch (IOException e) {
-      getLogger().log(TreeLogger.ERROR,
-          "Unable to determine mime type of resource", e);
-      throw new UnableToCompleteException();
-    }
-  }
-
   public boolean supportsDataUrls() {
     return false;
   }