Allow CSS class name prefixes to be reserved in the CssResource obfuscator.
http://gwt-code-reviews.appspot.com/600801/show
Patch by: bobv
Review by: rjrjr
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8244 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 77c55fb..d4c5b83 100644
--- a/user/src/com/google/gwt/resources/Resources.gwt.xml
+++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -71,6 +71,11 @@
<define-configuration-property name="CssResource.obfuscationPrefix" is-multi-valued="false" />
<set-configuration-property name="CssResource.obfuscationPrefix" value="default" />
+ <!-- A multi-valued configuration property that defines class name prefixes -->
+ <!-- the CssResource obfuscator should not use. -->
+ <define-configuration-property name="CssResource.reservedClassPrefixes" is-multi-valued="true" />
+ <extend-configuration-property name="CssResource.reservedClassPrefixes" value="gwt-" />
+
<!-- This can be used to make CssResource produce human-readable CSS -->
<define-configuration-property name="CssResource.style" is-multi-valued="false" />
<set-configuration-property name="CssResource.style" value="obf" />
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
index 069722a..f1cca0f 100644
--- a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -102,11 +102,15 @@
* A lookup table of base-32 chars we use to encode CSS idents. Because CSS
* class selectors may be case-insensitive, we don't have enough characters to
* use a base-64 encoding.
+ * <p>
+ * Note that the character {@value #RESERVED_IDENT_CHAR} is intentionally
+ * missing from this array. It is used to prefix identifiers produced by
+ * {@link #makeIdent} if they conflict with reserved class-name prefixes.
*/
- private static final char[] BASE32_CHARS = new char[] {
+ static final char[] BASE32_CHARS = new char[] {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
- 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '0',
- '1', '2', '3', '4'};
+ 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', '-', '0', '1',
+ '2', '3', '4', '5'};
/**
* This value is used by {@link #concatOp} to help create a more balanced AST
@@ -118,10 +122,16 @@
* These constants are used to cache obfuscated class names.
*/
private static final String KEY_BY_CLASS_AND_METHOD = "classAndMethod";
- private static final String KEY_HAS_CACHED_DATA = "hasCachedData";
- private static final String KEY_SHARED_METHODS = "sharedMethods";
private static final String KEY_CLASS_PREFIX = "prefix";
private static final String KEY_CLASS_COUNTER = "counter";
+ private static final String KEY_HAS_CACHED_DATA = "hasCachedData";
+ private static final String KEY_SHARED_METHODS = "sharedMethods";
+ private static final String KEY_RESERVED_PREFIXES = "CssResource.reservedClassPrefixes";
+
+ /**
+ * This character must not appear in {@link #BASE32_CHARS}.
+ */
+ private static final char RESERVED_IDENT_CHAR = 'Z';
/**
* Returns the import prefix for a type, including the trailing hyphen.
@@ -190,6 +200,40 @@
}
/**
+ * Compute an obfuscated CSS class name that is guaranteed not to conflict
+ * with a set of reserved prefixes. Visible for testing.
+ */
+ static String computeObfuscatedClassName(String classPrefix,
+ Counter classCounter, SortedSet<String> reservedPrefixes) {
+ String obfuscatedClassName = classPrefix + makeIdent(classCounter.next());
+
+ /*
+ * Ensure that the name won't conflict with any reserved prefixes. We can't
+ * just keep incrementing the counter, because that could take an
+ * arbitrarily long amount of time to return a good value.
+ */
+ String conflict = stringStartsWithAny(obfuscatedClassName, reservedPrefixes);
+ while (conflict != null) {
+ Adler32 hash = new Adler32();
+ hash.update(Util.getBytes(conflict));
+ /*
+ * Compute a new prefix for the identifier to mask the prefix and add the
+ * reserved identifier character to prevent conflicts with makeIdent().
+ *
+ * Assuming "gwt-" is a reserved prefix: gwt-A -> ab32ZA
+ */
+ String newPrefix = makeIdent(hash.getValue()).substring(0,
+ conflict.length())
+ + RESERVED_IDENT_CHAR;
+ obfuscatedClassName = newPrefix
+ + obfuscatedClassName.substring(conflict.length());
+ conflict = stringStartsWithAny(obfuscatedClassName, reservedPrefixes);
+ }
+
+ return obfuscatedClassName;
+ }
+
+ /**
* Create a Java expression that evaluates to a string representation of the
* given node. Visible only for testing.
*/
@@ -318,6 +362,33 @@
}
/**
+ * Returns <code>true</code> if <code>target</code> starts with any of the
+ * prefixes in the supplied set. The check is performed in a case-insensitive
+ * manner, assuming that the values in <code>prefixes</code> have already been
+ * converted to lower-case.
+ */
+ private static String stringStartsWithAny(String target,
+ SortedSet<String> prefixes) {
+ if (prefixes.isEmpty()) {
+ return null;
+ }
+ /*
+ * The headSet() method returns values strictly less than the search value,
+ * so we want to append a trailing character to the end of the search in
+ * case the obfuscated class name is exactly equal to one of the prefixes.
+ */
+ String search = target.toString().toLowerCase() + " ";
+ SortedSet<String> headSet = prefixes.headSet(search);
+ if (!headSet.isEmpty()) {
+ String prefix = headSet.last();
+ if (search.startsWith(prefix)) {
+ return prefix;
+ }
+ }
+ return null;
+ }
+
+ /**
* This function validates any context-sensitive Values.
*/
private static void validateValue(TreeLogger logger,
@@ -485,8 +556,14 @@
(new RequirementsCollector(logger, requirements)).accept(sheet);
}
+ /**
+ * Determine the class prefix that will be used. If a value is automatically
+ * computed, the <code>reservedPrefixes</code> set will be cleared because the
+ * returned value is guaranteed to not conflict with any reserved prefixes.
+ */
private String computeClassPrefix(String classPrefix,
- SortedSet<JClassType> cssResourceSubtypes) {
+ SortedSet<JClassType> cssResourceSubtypes,
+ TreeSet<String> reservedPrefixes) {
if ("default".equals(classPrefix)) {
classPrefix = null;
} else if ("empty".equals(classPrefix)) {
@@ -502,8 +579,17 @@
for (JClassType type : cssResourceSubtypes) {
checksum.update(Util.getBytes(type.getQualifiedSourceName()));
}
- classPrefix = "G"
- + Long.toString(checksum.getValue(), Character.MAX_RADIX);
+
+ final int seed = Math.abs((int) checksum.getValue());
+ classPrefix = "G" + computeObfuscatedClassName("", new Counter() {
+ @Override
+ int next() {
+ return seed;
+ }
+ }, reservedPrefixes);
+
+ // No conflicts are possible now
+ reservedPrefixes.clear();
}
return classPrefix;
@@ -515,7 +601,7 @@
* interface that is tagged with {@code @Shared}.
*/
private void computeObfuscatedNames(TreeLogger logger, String classPrefix,
- Set<JClassType> cssResourceSubtypes) {
+ SortedSet<String> reservedPrefixes, Set<JClassType> cssResourceSubtypes) {
logger = logger.branch(TreeLogger.DEBUG, "Computing CSS class replacements");
for (JClassType type : cssResourceSubtypes) {
@@ -539,8 +625,9 @@
name = classNameOverride.value();
}
- String obfuscatedClassName = classPrefix
- + makeIdent(classCounter.next());
+ String obfuscatedClassName = computeObfuscatedClassName(classPrefix,
+ classCounter, reservedPrefixes);
+
if (prettyOutput) {
obfuscatedClassName += "-"
+ type.getQualifiedSourceName().replaceAll("[.$]", "-") + "-"
@@ -682,14 +769,42 @@
SortedSet<JClassType> cssResourceSubtypes = computeOperableTypes(logger);
if (context.getCachedData(KEY_HAS_CACHED_DATA, Boolean.class) != Boolean.TRUE) {
- context.putCachedData(KEY_CLASS_COUNTER, new Counter());
+
+ ConfigurationProperty prop;
+ TreeSet<String> reservedPrefixes = new TreeSet<String>();
+ try {
+ prop = context.getGeneratorContext().getPropertyOracle().getConfigurationProperty(
+ KEY_RESERVED_PREFIXES);
+ for (String value : prop.getValues()) {
+ value = value.trim();
+ if (value.length() == 0) {
+ logger.log(TreeLogger.WARN,
+ "Ignoring nonsensical empty string value for "
+ + KEY_RESERVED_PREFIXES + " configuration property");
+ continue;
+ }
+
+ // Strip leading dots
+ if (value.startsWith(".")) {
+ value = value.substring(1);
+ }
+ reservedPrefixes.add(value.toLowerCase());
+ }
+ } catch (BadPropertyValueException e) {
+ // Do nothing. Unexpected, but we can live with it.
+ }
+
+ String computedPrefix = computeClassPrefix(classPrefix,
+ cssResourceSubtypes, reservedPrefixes);
+
context.putCachedData(KEY_BY_CLASS_AND_METHOD,
new IdentityHashMap<JClassType, Map<JMethod, String>>());
+ context.putCachedData(KEY_CLASS_PREFIX, computedPrefix);
+ context.putCachedData(KEY_CLASS_COUNTER, new Counter());
+ context.putCachedData(KEY_HAS_CACHED_DATA, Boolean.TRUE);
+ context.putCachedData(KEY_RESERVED_PREFIXES, reservedPrefixes);
context.putCachedData(KEY_SHARED_METHODS,
new IdentityHashMap<JMethod, String>());
- context.putCachedData(KEY_CLASS_PREFIX, computeClassPrefix(classPrefix,
- cssResourceSubtypes));
- context.putCachedData(KEY_HAS_CACHED_DATA, Boolean.TRUE);
}
classCounter = context.getCachedData(KEY_CLASS_COUNTER, Counter.class);
@@ -697,9 +812,13 @@
KEY_BY_CLASS_AND_METHOD, Map.class);
replacementsForSharedMethods = context.getCachedData(KEY_SHARED_METHODS,
Map.class);
- classPrefix = context.getCachedData(KEY_CLASS_PREFIX, String.class);
- computeObfuscatedNames(logger, classPrefix, cssResourceSubtypes);
+ classPrefix = context.getCachedData(KEY_CLASS_PREFIX, String.class);
+ SortedSet<String> reservedPrefixes = context.getCachedData(
+ KEY_RESERVED_PREFIXES, SortedSet.class);
+
+ computeObfuscatedNames(logger, classPrefix, reservedPrefixes,
+ cssResourceSubtypes);
}
/**
diff --git a/user/test/com/google/gwt/resources/ResourcesSuite.java b/user/test/com/google/gwt/resources/ResourcesSuite.java
index 35ecb35..679ed4e 100644
--- a/user/test/com/google/gwt/resources/ResourcesSuite.java
+++ b/user/test/com/google/gwt/resources/ResourcesSuite.java
@@ -28,6 +28,7 @@
import com.google.gwt.resources.css.ExtractClassNamesVisitorTest;
import com.google.gwt.resources.css.UnknownAtRuleTest;
import com.google.gwt.resources.ext.ResourceGeneratorUtilTest;
+import com.google.gwt.resources.rg.CssClassNamesTestCase;
import junit.framework.Test;
@@ -38,6 +39,7 @@
public static Test suite() {
GWTTestSuite suite = new GWTTestSuite("Test for com.google.gwt.resources");
+ suite.addTestSuite(CssClassNamesTestCase.class);
suite.addTestSuite(CssExternalTest.class);
suite.addTestSuite(CSSResourceTest.class);
suite.addTestSuite(CssReorderTest.class);
diff --git a/user/test/com/google/gwt/resources/rg/CssClassNamesTestCase.java b/user/test/com/google/gwt/resources/rg/CssClassNamesTestCase.java
new file mode 100644
index 0000000..f179ae2
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/CssClassNamesTestCase.java
@@ -0,0 +1,84 @@
+/*
+ * 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.resources.rg;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Tests CssResourceGenerator's generation of unique CSS class names.
+ */
+public class CssClassNamesTestCase extends TestCase {
+ static class ConstantCounter extends Counter {
+ @Override
+ int next() {
+ return 16;
+ }
+ }
+
+ private static final SortedSet<String> EMPTY_SET = new TreeSet<String>();
+ private static final int NUM_CYCLES = 1000;
+
+ public void testPrefix() {
+ assertEquals("p-A", CssResourceGenerator.computeObfuscatedClassName("p-",
+ new Counter(), EMPTY_SET));
+ }
+
+ public void testReservedPrefixes() {
+ Counter counter = new ConstantCounter();
+ SortedSet<String> hateful = new TreeSet<String>(Arrays.asList("a"));
+
+ // Value with no prefixes
+ assertEquals("AB", CssResourceGenerator.computeObfuscatedClassName("",
+ counter, EMPTY_SET));
+
+ assertEquals("CZB", CssResourceGenerator.computeObfuscatedClassName("",
+ counter, hateful));
+
+ hateful.add("c");
+ assertEquals("EZZB", CssResourceGenerator.computeObfuscatedClassName("",
+ counter, hateful));
+
+ hateful.add("ezz");
+ assertEquals("KVAZB", CssResourceGenerator.computeObfuscatedClassName("",
+ counter, hateful));
+ }
+
+ /**
+ * Quick sanity check to ensure that the initial sequence of idents is unique
+ * and stable.
+ */
+ public void testSimple() {
+ Counter counter = new Counter();
+ Counter counter2 = new Counter();
+ Set<String> seen = new HashSet<String>();
+
+ for (int i = 0; i < NUM_CYCLES; i++) {
+ String ident = CssResourceGenerator.computeObfuscatedClassName("",
+ counter, EMPTY_SET);
+ assertTrue(seen.add(ident));
+
+ assertEquals(ident, CssResourceGenerator.computeObfuscatedClassName("",
+ counter2, EMPTY_SET));
+ }
+ assertEquals(1000, counter.next());
+ }
+}