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>
+ * <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;
+ }
+}