Adds a Precompress linker that can be used to compress public artifacts
as part of a GWT build.  The design doc is here:

http://code.google.com/p/google-web-toolkit/wiki/PrecompressLinker

Review at http://gwt-code-reviews.appspot.com/254801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7802 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt20_21userApi.conf b/tools/api-checker/config/gwt20_21userApi.conf
index ab880e3..1959dcd 100644
--- a/tools/api-checker/config/gwt20_21userApi.conf
+++ b/tools/api-checker/config/gwt20_21userApi.conf
@@ -71,6 +71,7 @@
 :user/src/com/google/gwt/junit/client/GWTTestCase.java\
 :user/src/com/google/gwt/junit/client/impl/GWTRunner.java\
 :user/src/com/google/gwt/junit/remote\
+:user/src/com/google/gwt/precompress/linker\
 :user/src/com/google/gwt/resources/css\
 :user/src/com/google/gwt/resources/ext\
 :user/src/com/google/gwt/resources/rg\
@@ -82,6 +83,7 @@
 :user/src/com/google/gwt/uibinder/attributeparsers\
 :user/src/com/google/gwt/uibinder/elementparsers\
 :user/src/com/google/gwt/uibinder/testing\
+:user/src/com/google/gwt/util/regexfilter\
 
 ##############################################
 #excluded packages
diff --git a/user/src/com/google/gwt/precompress/Precompress.gwt.xml b/user/src/com/google/gwt/precompress/Precompress.gwt.xml
new file mode 100644
index 0000000..594e3ce
--- /dev/null
+++ b/user/src/com/google/gwt/precompress/Precompress.gwt.xml
@@ -0,0 +1,34 @@
+<!--                                                                        -->
+<!-- Copyright 2010 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   -->
+<!-- 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. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<!-- The Precompress linker for precompressing public emitted artifacts.    -->
+<!-- See the class comment of com.google.gwt.precompress.PrecompressLinker  -->
+<!-- for information on using it.                                           -->
+
+<!--                                                                        -->
+<!-- This module is typically inherited via com.google.gwt.user.User        -->
+<!--                                                                        -->
+
+<module>
+  <define-linker name="precompress" class="com.google.gwt.precompress.linker.PrecompressLinker" />
+  <add-linker name="precompress" />
+  
+  <define-configuration-property name="precompress.leave.originals" is_multi_valued="false" />
+  <set-configuration-property name="precompress.leave.originals" value="true" />
+
+  <define-configuration-property name="precompress.path.regexes" is_multi_valued="true" />
+  <extend-configuration-property name="precompress.path.regexes" value=".*\.html" />
+  <extend-configuration-property name="precompress.path.regexes" value=".*\.js" />
+  <extend-configuration-property name="precompress.path.regexes" value=".*\.css" />
+</module>
diff --git a/user/src/com/google/gwt/precompress/linker/PrecompressLinker.java b/user/src/com/google/gwt/precompress/linker/PrecompressLinker.java
new file mode 100644
index 0000000..e219763
--- /dev/null
+++ b/user/src/com/google/gwt/precompress/linker/PrecompressLinker.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2010 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.precompress.linker;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.AbstractLinker;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.EmittedArtifact;
+import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.Shardable;
+import com.google.gwt.core.ext.linker.LinkerOrder.Order;
+import com.google.gwt.dev.util.collect.HashSet;
+import com.google.gwt.util.regexfilter.RegexFilter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * <p>
+ * A linker that precompresses the public artifacts that it sees. That way, a
+ * web server that uses gzip transfer encoding can use the precompressed files
+ * instead of having to compress them on the fly.
+ * 
+ * <p>
+ * To use this linker, add the following to your module definition:
+ * 
+ * <pre>
+ *   &lt;inherits name="com.google.gwt.precompress.Precompress"/>
+ * </pre>
+ * 
+ * <p>
+ * The files to precompress are specified by the configuration property
+ * <code>precompress.path.regexes</code>. By default, the uncompressed artifacts
+ * are left in the artifact set. If the configuration property
+ * <code>precompress.leave.originals</code> is set to <code>false</code>,
+ * however, then the uncompressed version is removed.
+ */
+@Shardable
+@LinkerOrder(Order.POST)
+public class PrecompressLinker extends AbstractLinker {
+  private static class PrecompressFilter extends RegexFilter {
+    public PrecompressFilter(TreeLogger logger, List<String> regexes)
+        throws UnableToCompleteException {
+      super(logger, regexes);
+    }
+
+    @Override
+    protected boolean acceptByDefault() {
+      return false;
+    }
+
+    @Override
+    protected boolean entriesArePositiveByDefault() {
+      return true;
+    }
+  }
+
+  /**
+   * Buffer size to use when streaming data from artifacts and through
+   * {@link GZIPOutputStream}.
+   */
+  private static final int BUF_SIZE = 10000;
+
+  private static final String PROP_LEAVE_ORIGINALS = "precompress.leave.originals";
+
+  private static final String PROP_PATH_REGEXES = "precompress.path.regexes";
+
+  private static ConfigurationProperty findProperty(
+      TreeLogger logger,
+      Iterable<com.google.gwt.core.ext.linker.ConfigurationProperty> properties,
+      String propName) throws UnableToCompleteException {
+    for (ConfigurationProperty prop : properties) {
+      if (prop.getName().equals(propName)) {
+        return prop;
+      }
+    }
+
+    logger.log(TreeLogger.ERROR, "Could not find configuration property "
+        + propName);
+    throw new UnableToCompleteException();
+  }
+
+  @Override
+  public String getDescription() {
+    return "PrecompressLinker";
+  }
+
+  @Override
+  public ArtifactSet link(TreeLogger logger, LinkerContext context,
+      ArtifactSet artifacts, boolean onePermutation)
+      throws UnableToCompleteException {
+    ConfigurationProperty leaveOriginalsProp = findProperty(logger,
+        context.getConfigurationProperties(), PROP_LEAVE_ORIGINALS);
+    boolean leaveOriginals = Boolean.valueOf(leaveOriginalsProp.getValues().get(
+        0));
+
+    PrecompressFilter filter = new PrecompressFilter(logger.branch(
+        TreeLogger.TRACE, "Analyzing the path patterns"), findProperty(logger,
+        context.getConfigurationProperties(), PROP_PATH_REGEXES).getValues());
+
+    // Record the list of all paths for later lookup
+    Set<String> allPaths = new HashSet<String>();
+    for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) {
+      allPaths.add(art.getPartialPath());
+    }
+
+    try {
+      // Buffer for streaming data to be compressed
+      byte[] buf = new byte[BUF_SIZE];
+
+      ArtifactSet updated = new ArtifactSet(artifacts);
+      for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) {
+        if (art.isPrivate()) {
+          continue;
+        }
+        if (art.getPartialPath().endsWith(".gz")) {
+          // Already a compressed artifact
+          continue;
+        }
+        if (allPaths.contains(art.getPartialPath() + ".gz")) {
+          // It's already been compressed
+          continue;
+        }
+        if (!filter.isIncluded(logger.branch(TreeLogger.TRACE,
+            "Checking the path patterns"), art.getPartialPath())) {
+          continue;
+        }
+
+        TreeLogger compressBranch = logger.branch(TreeLogger.TRACE,
+            "Compressing " + art.getPartialPath());
+
+        InputStream originalBytes = art.getContents(compressBranch);
+        ByteArrayOutputStream compressedBytes = new ByteArrayOutputStream();
+        GZIPOutputStream gzip = new GZIPOutputStream(compressedBytes);
+
+        int originalLength = 0;
+        int n;
+        while ((n = originalBytes.read(buf)) > 0) {
+          originalLength += n;
+          gzip.write(buf, 0, n);
+        }
+        gzip.close();
+
+        byte[] compressed = compressedBytes.toByteArray();
+        if (compressed.length < originalLength) {
+          updated.add(emitBytes(compressBranch, compressed,
+              art.getPartialPath() + ".gz"));
+          if (!leaveOriginals) {
+            updated.remove(art);
+          }
+        }
+      }
+      return updated;
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unexpected exception", e);
+      throw new UnableToCompleteException();
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/user/rebind/rpc/BlacklistTypeFilter.java b/user/src/com/google/gwt/user/rebind/rpc/BlacklistTypeFilter.java
index d2d71e2..6532eda 100644
--- a/user/src/com/google/gwt/user/rebind/rpc/BlacklistTypeFilter.java
+++ b/user/src/com/google/gwt/user/rebind/rpc/BlacklistTypeFilter.java
@@ -23,108 +23,75 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JRealClassType;
+import com.google.gwt.util.regexfilter.RegexFilter;
 
-import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
 
 class BlacklistTypeFilter implements TypeFilter {
-  
-  private List<Boolean> includeType;
+  private static String PROP_RPC_BLACKLIST = "rpc.blacklist";
+
+  /**
+   * Configure {@link RegexFilter} for use for RPC blacklists.
+   */
+  private static class RpcBlacklist extends RegexFilter {
+    public RpcBlacklist(TreeLogger logger, List<String> regexes)
+        throws UnableToCompleteException {
+      super(logger, regexes);
+    }
+
+    @Override
+    protected boolean acceptByDefault() {
+      return true;
+    }
+
+    @Override
+    protected boolean entriesArePositiveByDefault() {
+      return false;
+    }
+  }
+
+  private final RpcBlacklist blacklist;
   private TreeLogger logger;
-  private List<Pattern> typePatterns;
-  private List<String> values;
-  
+
   public BlacklistTypeFilter(TreeLogger logger, PropertyOracle propertyOracle)
       throws UnableToCompleteException {
+    ConfigurationProperty prop;
+    try {
+      prop = propertyOracle.getConfigurationProperty(PROP_RPC_BLACKLIST);
+    } catch (BadPropertyValueException e) {
+      logger.log(TreeLogger.ERROR, "Could not find property "
+          + PROP_RPC_BLACKLIST);
+      throw new UnableToCompleteException();
+    }
+
     this.logger = logger.branch(TreeLogger.DEBUG,
         "Analyzing RPC blacklist information");
-    try {
-      ConfigurationProperty prop
-          = propertyOracle.getConfigurationProperty("rpc.blacklist");
-
-      values = prop.getValues();
-      int size = values.size();
-      typePatterns = new ArrayList<Pattern>(size);
-      includeType = new ArrayList<Boolean>(size);
-
-      // TODO investigate grouping multiple patterns into a single regex
-      for (String regex : values) {
-        // Patterns that don't start with [+-] are considered to be [-]
-        boolean include = false;
-        // Ignore empty regexes
-        if (regex.length() == 0) {
-          logger.log(TreeLogger.ERROR, "Got empty RPC blacklist entry");
-          throw new UnableToCompleteException();
-        }
-        char c = regex.charAt(0);
-        if (c == '+' || c == '-') {
-          regex = regex.substring(1); // skip initial character
-          include = (c == '+');
-        }
-        try {
-          Pattern p = Pattern.compile(regex);
-          typePatterns.add(p);
-          includeType.add(include);
-          
-          logger.log(TreeLogger.DEBUG,
-              "Got RPC blacklist entry '" + regex + "'");
-        } catch (PatternSyntaxException e) {
-          logger.log(TreeLogger.ERROR,
-              "Got malformed RPC blacklist entry '" + regex + "'");
-          throw new UnableToCompleteException();
-        }
-      }
-    } catch (BadPropertyValueException e) {
-      logger.log(TreeLogger.DEBUG, "No RPC blacklist entries present");
-    }
+    blacklist = new RpcBlacklist(logger, prop.getValues());
   }
 
   public String getName() {
     return "BlacklistTypeFilter";
   }
-  
+
   public boolean isAllowed(JClassType type) {
     String name = getBaseTypeName(type);
     // For types not handled by getBaseTypeName just return true.
     if (name == null) {
       return true;
     }
-    
-    // Process patterns in reverse order for early exit
-    int size = typePatterns.size();
-    for (int idx = size - 1; idx >= 0; idx--) {
-      logger.log(TreeLogger.DEBUG, "Considering RPC rule " + values.get(idx)
-          + " for type " + name);
-      boolean include = includeType.get(idx);
-      Pattern pattern = typePatterns.get(idx);
-      if (pattern.matcher(name).matches()) {
-        if (include) {
-          logger.log(TreeLogger.DEBUG, "Whitelisting type " + name
-              + " according to rule " + values.get(idx));
-          return true;
-        } else {
-          logger.log(TreeLogger.DEBUG, "Blacklisting type " + name
-              + " according to rule " + values.get(idx));
-          return false;
-        }
-      }
-    }
-    
-    // Type does not match any pattern, pass it through
-    return true;
+
+    return blacklist.isIncluded(logger, name);
   }
 
   /**
    * Returns a simple qualified name for simple types, including classes and
-   * interfaces, parameterized, and raw types.  Null is returned for other types
+   * interfaces, parameterized, and raw types. Null is returned for other types
    * such as arrays and type parameters (e.g., 'E' in java.util.List<E>) because
    * filtering is meaningless for such types.
    */
   private String getBaseTypeName(JClassType type) {
     JClassType baseType = null;
-    
+
     if (type instanceof JRealClassType) {
       baseType = type;
     } else if (type.isParameterized() != null) {
@@ -132,8 +99,7 @@
     } else if (type.isRawType() != null) {
       baseType = type.isRawType();
     }
-    
+
     return baseType == null ? null : baseType.getQualifiedSourceName();
   }
 }
-
diff --git a/user/src/com/google/gwt/util/regexfilter/RegexFilter.java b/user/src/com/google/gwt/util/regexfilter/RegexFilter.java
new file mode 100644
index 0000000..07a60fa
--- /dev/null
+++ b/user/src/com/google/gwt/util/regexfilter/RegexFilter.java
@@ -0,0 +1,113 @@
+/*
+ * 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.util.regexfilter;
+
+import com.google.gwt.core.ext.ConfigurationProperty;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * This class implements filters that are configured with a sequence of regexes.
+ * Each regex in the sequence can be preceded by a + or a - to indicate whether
+ * it indicates that queries matching the regex should be included or excluded.
+ * Concrete subclasses indicate the default behaviors by overriding
+ * {@link #acceptByDefault()} and {@link #entriesArePositiveByDefault()}.
+ */
+public abstract class RegexFilter {
+  private final List<String> values;
+  private final ArrayList<Pattern> typePatterns;
+  private final ArrayList<Boolean> includeType;
+
+  public RegexFilter(TreeLogger logger, List<String> values)
+      throws UnableToCompleteException {
+    ConfigurationProperty prop;
+    this.values = values;
+    int size = values.size();
+    typePatterns = new ArrayList<Pattern>(size);
+    includeType = new ArrayList<Boolean>(size);
+
+    // TODO investigate grouping multiple patterns into a single regex
+    for (String regex : values) {
+      // Patterns that don't start with [+-] are considered to be [-]
+      boolean include = entriesArePositiveByDefault();
+      // Ignore empty regexes
+      if (regex.length() == 0) {
+        logger.log(TreeLogger.ERROR, "Got empty blacklist entry");
+        throw new UnableToCompleteException();
+      }
+      char c = regex.charAt(0);
+      if (c == '+' || c == '-') {
+        regex = regex.substring(1); // skip initial character
+        include = (c == '+');
+      }
+      try {
+        Pattern p = Pattern.compile(regex);
+        typePatterns.add(p);
+        includeType.add(include);
+
+        logger.log(TreeLogger.DEBUG, "Got filter entry '" + regex + "'");
+      } catch (PatternSyntaxException e) {
+        logger.log(TreeLogger.ERROR, "Got malformed filter entry '" + regex
+            + "'");
+        throw new UnableToCompleteException();
+      }
+    }
+  }
+
+  public boolean isIncluded(TreeLogger logger, String query) {
+    logger = logger.branch(TreeLogger.DEBUG, "Considering query " + query);
+
+    // Process patterns in reverse order for early exit
+    int size = typePatterns.size();
+    for (int idx = size - 1; idx >= 0; idx--) {
+      logger.log(TreeLogger.DEBUG, "Considering filter rule "
+          + values.get(idx) + " for query " + query);
+      boolean include = includeType.get(idx);
+      Pattern pattern = typePatterns.get(idx);
+      if (pattern.matcher(query).matches()) {
+        if (include) {
+          logger.log(TreeLogger.DEBUG, "Whitelisting " + query
+              + " according to rule " + values.get(idx));
+          return true;
+        } else {
+          logger.log(TreeLogger.DEBUG, "Blacklisting " + query
+              + " according to rule " + values.get(idx));
+          return false;
+        }
+      }
+    }
+
+    // None of the patterns matched
+    return acceptByDefault();
+  }
+
+  /**
+   * If no pattern matches, whether the query should be considered as an accept.
+   */
+  protected abstract boolean acceptByDefault();
+
+  /**
+   * If a pattern is not preceded by + or -, whether the query should be
+   * considered positive.
+   */
+  protected abstract boolean entriesArePositiveByDefault();
+}
diff --git a/user/test/com/google/gwt/precompress/PrecompressLinkerSuite.java b/user/test/com/google/gwt/precompress/PrecompressLinkerSuite.java
new file mode 100644
index 0000000..3de058c
--- /dev/null
+++ b/user/test/com/google/gwt/precompress/PrecompressLinkerSuite.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010 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.precompress;
+
+import com.google.gwt.precompress.linker.PrecompressLinkerTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * Tests {@link PrecompressLinker}.
+ */
+public class PrecompressLinkerSuite {
+  public static Test suite() {
+    TestSuite suite = new TestSuite("PrecompressLinker tests");
+
+    // $JUnit-BEGIN$
+    suite.addTestSuite(PrecompressLinkerTest.class);
+    // $JUnit-END$
+
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/precompress/linker/PrecompressLinkerTest.java b/user/test/com/google/gwt/precompress/linker/PrecompressLinkerTest.java
new file mode 100644
index 0000000..f4f435c
--- /dev/null
+++ b/user/test/com/google/gwt/precompress/linker/PrecompressLinkerTest.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2010 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.precompress.linker;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.EmittedArtifact;
+import com.google.gwt.core.ext.linker.SelectionProperty;
+import com.google.gwt.core.ext.linker.SyntheticArtifact;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Tests {@link PrecompressLinker}.
+ */
+public class PrecompressLinkerTest extends TestCase {
+  private static class MockConfigurationProperty implements
+      ConfigurationProperty, Comparable<MockConfigurationProperty> {
+    private boolean hasMultipleValues;
+    private String name;
+    private List<String> values = new ArrayList<String>();
+
+    public MockConfigurationProperty(String name, boolean hasMultipleValues) {
+      this.name = name;
+      this.hasMultipleValues = hasMultipleValues;
+    }
+
+    @Override
+    public int compareTo(MockConfigurationProperty o) {
+      return getName().compareTo(o.getName());
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    @Deprecated
+    public String getValue() {
+      return values.get(0);
+    }
+
+    @Override
+    public List<String> getValues() {
+      return values;
+    }
+
+    @Override
+    public boolean hasMultipleValues() {
+      return hasMultipleValues;
+    }
+
+    public void setValue(String value) {
+      values.clear();
+      values.add(value);
+    }
+  }
+
+  private class MockLinkerContext implements LinkerContext {
+    @Override
+    public SortedSet<ConfigurationProperty> getConfigurationProperties() {
+      return new TreeSet<ConfigurationProperty>(Arrays.asList(
+          propLeaveOriginals, propPathRegexes));
+    }
+
+    @Override
+    public String getModuleFunctionName() {
+      return "MockModule";
+    }
+
+    @Override
+    public long getModuleLastModified() {
+      return 0;
+    }
+
+    @Override
+    public String getModuleName() {
+      return "MockModule";
+    }
+
+    @Override
+    public SortedSet<SelectionProperty> getProperties() {
+      return new TreeSet<SelectionProperty>();
+    }
+
+    @Override
+    public boolean isOutputCompact() {
+      return true;
+    }
+
+    @Override
+    public String optimizeJavaScript(TreeLogger logger, String jsProgram) {
+      return jsProgram;
+    }
+  }
+
+  private static final Charset UTF_8 = Charset.forName("UTF-8");
+
+  private static void assertEqualBytes(byte[] expected, byte[] actual) {
+    assertEquals(expected.length, actual.length);
+    for (int i = 0; i < expected.length; i++) {
+      assertEquals(expected[i], actual[i]);
+    }
+  }
+
+  private static byte[] compress(byte[] content) {
+    try {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      GZIPOutputStream gzip = new GZIPOutputStream(baos);
+      InputStream in = new ByteArrayInputStream(content);
+
+      byte[] buf = new byte[10000];
+      int n;
+      while ((n = in.read(buf)) > 0) {
+        gzip.write(buf, 0, n);
+      }
+      gzip.close();
+
+      return baos.toByteArray();
+    } catch (IOException e) {
+      throw new RuntimeException(
+          "Unexpected IO exception from memory operations");
+    }
+  }
+
+  private static byte[] contents(EmittedArtifact art)
+      throws UnableToCompleteException, IOException {
+    InputStream input = art.getContents(TreeLogger.NULL);
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+    byte[] buf = new byte[10000];
+    int n;
+    while ((n = input.read(buf)) > 0) {
+      baos.write(buf, 0, n);
+    }
+
+    return baos.toByteArray();
+  }
+
+  private static byte[] decompress(byte[] compressed) throws IOException {
+    GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(
+        compressed));
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    byte[] buf = new byte[10000];
+    int n;
+    while ((n = gzip.read(buf)) > 0) {
+      baos.write(buf, 0, n);
+    }
+
+    return baos.toByteArray();
+  }
+
+  private static SyntheticArtifact emit(String path, byte[] content) {
+    return new SyntheticArtifact(PrecompressLinker.class, path, content);
+  }
+
+  private static SyntheticArtifact emit(String path, String contents) {
+    return emit(path, contents.getBytes(UTF_8));
+  }
+
+  private static SyntheticArtifact emitPrivate(String string, String contents) {
+    SyntheticArtifact art = emit(string, contents);
+    art.setPrivate(true);
+    return art;
+  }
+
+  private static EmittedArtifact findArtifact(ArtifactSet artifacts, String path) {
+    for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) {
+      if (art.getPartialPath().equals(path)) {
+        return art;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Return a highly compressible string.
+   */
+  private static String fooFileContents() {
+    StringBuffer buf = new StringBuffer();
+    for (int i = 0; i < 1000; i++) {
+      buf.append("another identical line\n");
+    }
+    return buf.toString();
+  }
+
+  private static byte[] uncompressibleContent() {
+    byte[] content = fooFileContents().getBytes(UTF_8);
+    while (true) {
+      byte[] updated = compress(content);
+      if (updated.length >= content.length) {
+        return content;
+      }
+      content = updated;
+    }
+  }
+
+  private ArtifactSet artifacts;
+  private LinkerContext context = new MockLinkerContext();
+
+  private MockConfigurationProperty propLeaveOriginals;
+
+  private MockConfigurationProperty propPathRegexes;
+
+  /**
+   * Test that foo.js gets compressed to foo.js.gz, and bar.js is left alone.
+   */
+  public void testBasics() throws UnableToCompleteException, IOException {
+    ArtifactSet updated = linkArtifacts();
+
+    EmittedArtifact foo = findArtifact(updated, "foo.js");
+    assertNotNull(foo);
+
+    EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
+    assertNotNull(fooGz);
+    assertEqualBytes(contents(foo), decompress(contents(fooGz)));
+
+    EmittedArtifact barGz = findArtifact(updated, "bar.js.gz");
+    assertNull("bar.js is private and should not have been compressed", barGz);
+
+    EmittedArtifact uncompressibleGz = findArtifact(updated,
+        "uncompressible.js.gz");
+    assertNull(
+        "uncompressible.js is not compressible and should have been left alone",
+        uncompressibleGz);
+  }
+
+  /**
+   * Test that the blacklist takes effect.
+   */
+  public void testBlackList() throws UnableToCompleteException {
+    propPathRegexes.values.add("-foo\\.js");
+    ArtifactSet updated = linkArtifacts();
+
+    // foo.txt is not in the list of patterns, so don't compress
+    EmittedArtifact stuffGz = findArtifact(updated, "stuff.txt.gz");
+    assertNull("stuff.txt should not have been compressed", stuffGz);
+
+    // foo.js matches two regexes; the last should win
+    EmittedArtifact fooGz = findArtifact(updated, "foo.js.gz");
+    assertNull("foo.js should not have been compressed", fooGz);
+  }
+
+  /**
+   * Tests that if precompress.leave.original if false, the originals are
+   * removed.
+   */
+  public void testRemovingOriginals() throws UnableToCompleteException {
+    propLeaveOriginals.setValue("false");
+    ArtifactSet updated = linkArtifacts();
+    EmittedArtifact foo = findArtifact(updated, "foo.js");
+    assertNull("foo.js should have been removed", foo);
+  }
+
+  @Override
+  protected void setUp() {
+    // add some artifacts to test with
+    artifacts = new ArtifactSet();
+    artifacts.add(emit("foo.js", fooFileContents()));
+    artifacts.add(emitPrivate("bar.js", fooFileContents()));
+    artifacts.add(emit("uncompressible.js", uncompressibleContent()));
+    artifacts.add(emit("stuff.txt", fooFileContents()));
+    artifacts.add(emit("data.xml", fooFileContents()));
+    artifacts.freeze();
+
+    propLeaveOriginals = new MockConfigurationProperty(
+        "precompress.leave.originals", false);
+    propLeaveOriginals.setValue("true");
+
+    propPathRegexes = new MockConfigurationProperty("precompress.path.regexes",
+        true);
+    propPathRegexes.values.add(".*\\.html");
+    propPathRegexes.values.add(".*\\.js");
+    propPathRegexes.values.add(".*\\.css");
+  }
+
+  private ArtifactSet linkArtifacts() throws UnableToCompleteException {
+    return new PrecompressLinker().link(TreeLogger.NULL, context, artifacts,
+        true);
+  }
+}
diff --git a/user/test/com/google/gwt/user/RPCSuite.java b/user/test/com/google/gwt/user/RPCSuite.java
index 9c47d94..9821dc0 100644
--- a/user/test/com/google/gwt/user/RPCSuite.java
+++ b/user/test/com/google/gwt/user/RPCSuite.java
@@ -43,6 +43,7 @@
 import com.google.gwt.user.client.rpc.UnicodeEscapingTestWithTypeObfuscation;
 import com.google.gwt.user.client.rpc.ValueTypesTest;
 import com.google.gwt.user.client.rpc.ValueTypesTestWithTypeObfuscation;
+import com.google.gwt.user.rebind.rpc.BlacklistTypeFilterTest;
 import com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilderTest;
 import com.google.gwt.user.rebind.rpc.TypeHierarchyUtilsTest;
 import com.google.gwt.user.server.Base64Test;
@@ -74,6 +75,7 @@
         "Test for com.google.gwt.user.client.rpc");
 
     // Non GWTTestCases
+    suite.addTestSuite(BlacklistTypeFilterTest.class);
     suite.addTestSuite(SerializableTypeOracleBuilderTest.class);
     suite.addTestSuite(TypeHierarchyUtilsTest.class);
     suite.addTestSuite(RPCTest.class);
diff --git a/user/test/com/google/gwt/user/rebind/rpc/BlacklistTypeFilterTest.java b/user/test/com/google/gwt/user/rebind/rpc/BlacklistTypeFilterTest.java
new file mode 100644
index 0000000..80c54cd
--- /dev/null
+++ b/user/test/com/google/gwt/user/rebind/rpc/BlacklistTypeFilterTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2010 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.user.rebind.rpc;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.ConfigurationProperty;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.SelectionProperty;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.javac.TypeOracleTestingUtils;
+import com.google.gwt.dev.javac.impl.JavaResourceBase;
+import com.google.gwt.dev.javac.impl.MockJavaResource;
+import com.google.gwt.dev.javac.impl.StaticJavaResource;
+import com.google.gwt.dev.resource.Resource;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests {@link BlacklistTypeFilter}.
+ */
+public class BlacklistTypeFilterTest extends TestCase {
+  private static class MockConfigurationProperty implements
+      ConfigurationProperty {
+    String name;
+    List<String> values = new ArrayList<String>();
+
+    public MockConfigurationProperty(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public List<String> getValues() {
+      return values;
+    }
+  }
+
+  private class MockPropertyOracle implements PropertyOracle {
+    @Override
+    public ConfigurationProperty getConfigurationProperty(String propertyName)
+        throws BadPropertyValueException {
+      if (propertyName.equals(propRpcBlacklist.getName())) {
+        return propRpcBlacklist;
+      }
+      throw new BadPropertyValueException("No property named " + propertyName);
+    }
+
+    @Override
+    @Deprecated
+    public String getPropertyValue(TreeLogger logger, String propertyName)
+        throws BadPropertyValueException {
+      return getConfigurationProperty(propertyName).getValues().get(0);
+    }
+
+    @Override
+    @Deprecated
+    public String[] getPropertyValueSet(TreeLogger logger, String propertyName)
+        throws BadPropertyValueException {
+      return getConfigurationProperty(propertyName).getValues().toArray(
+          new String[0]);
+    }
+
+    @Override
+    public SelectionProperty getSelectionProperty(TreeLogger logger,
+        String propertyName) throws BadPropertyValueException {
+      throw new BadPropertyValueException("No property named " + propertyName);
+    }
+  }
+
+  private MockConfigurationProperty propRpcBlacklist =
+    new MockConfigurationProperty("rpc.blacklist");
+
+  public void testBasics() throws UnableToCompleteException, NotFoundException {
+    Set<Resource> resources = new HashSet<Resource>();
+    for (MockJavaResource resource : JavaResourceBase.getStandardResources()) {
+      resources.add(resource);
+    }
+    resources.add(makeClass("Type1"));
+    resources.add(makeClass("Type2"));
+    resources.add(makeClass("Type3"));
+    resources.add(makeClass("Type4"));
+    resources.add(makeClass("Type5"));
+
+    TypeOracle to = TypeOracleTestingUtils.buildTypeOracle(TreeLogger.NULL,
+        resources);
+
+    propRpcBlacklist.values.add("Type1");
+    propRpcBlacklist.values.add("+Type2");
+    propRpcBlacklist.values.add("-Type3");
+    propRpcBlacklist.values.add("+Type4");
+    propRpcBlacklist.values.add("-Type4");
+
+    BlacklistTypeFilter filter = new BlacklistTypeFilter(TreeLogger.NULL,
+        new MockPropertyOracle());
+    assertFalse(filter.isAllowed(to.getType("Type1")));
+    assertTrue(filter.isAllowed(to.getType("Type2")));
+    assertFalse(filter.isAllowed(to.getType("Type3")));
+    assertFalse(filter.isAllowed(to.getType("Type4")));
+    assertTrue(filter.isAllowed(to.getType("Type5")));
+  }
+
+  private StaticJavaResource makeClass(String className) {
+    StringBuilder code = new StringBuilder();
+    code.append("public class " + className + "{ }\n");
+    StaticJavaResource e = new StaticJavaResource(className, code);
+    return e;
+  }
+}