blob: d1780340b8a26eceb7b286d49b069167b763c312 [file] [log] [blame]
/*
* 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.css;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.resources.client.CssResource.ClassName;
import com.google.gwt.resources.css.ast.Context;
import com.google.gwt.resources.css.ast.CssCompilerException;
import com.google.gwt.resources.css.ast.CssDef;
import com.google.gwt.resources.css.ast.CssSelector;
import com.google.gwt.resources.css.ast.CssStylesheet;
import com.google.gwt.resources.css.ast.CssVisitor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
/**
* Renames class selectors to their obfuscated names.
*/
public class ClassRenamer extends CssVisitor {
/**
* A tag to indicate that an externally-defined CSS class has no JMethod that
* is used to access it.
*/
private static final Replacement UNREFERENCED_EXTERNAL = new Replacement(
null, null);
/*
* TODO: Replace with Pair<A, B>.
*/
private static class Replacement {
private JMethod method;
private String obfuscatedClassName;
public Replacement(JMethod method, String obfuscatedClassName) {
this.method = method;
this.obfuscatedClassName = obfuscatedClassName;
}
public JMethod getMethod() {
return method;
}
public String getObfuscatedClassName() {
return obfuscatedClassName;
}
/**
* For debugging use only.
*/
@Override
public String toString() {
if (this == UNREFERENCED_EXTERNAL) {
return "Unreferenced external class name";
} else {
return method.getName() + "=" + obfuscatedClassName;
}
}
}
/**
* Records replacements that have actually been performed.
*/
private final Map<JMethod, String> actualReplacements = new IdentityHashMap<JMethod, String>();
private final Map<String, Map<JMethod, String>> classReplacementsWithPrefix;
private final Set<String> cssDefs = new HashSet<String>();
private final Set<String> externalClasses;
private final TreeLogger logger;
private final Set<JMethod> missingClasses;
private final boolean strict;
private final Set<String> unknownClasses = new HashSet<String>();
public ClassRenamer(TreeLogger logger,
Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
boolean strict, Set<String> externalClasses) {
this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
this.classReplacementsWithPrefix = classReplacementsWithPrefix;
this.strict = strict;
this.externalClasses = externalClasses;
// Require a definition for all classes in the default namespace
assert classReplacementsWithPrefix.containsKey("");
missingClasses = new HashSet<JMethod>(
classReplacementsWithPrefix.get("").keySet());
}
@Override
public void endVisit(CssDef x, Context ctx) {
cssDefs.add(x.getKey());
}
@Override
public void endVisit(CssSelector x, Context ctx) {
final Map<String, Replacement> potentialReplacements;
potentialReplacements = computeReplacements(classReplacementsWithPrefix,
externalClasses);
String sel = x.getSelector();
int originalLength = sel.length();
Matcher ma = CssSelector.CLASS_SELECTOR_PATTERN.matcher(sel);
StringBuilder sb = new StringBuilder(originalLength);
int start = 0;
while (ma.find()) {
String sourceClassName = ma.group(1);
Replacement entry = potentialReplacements.get(sourceClassName);
if (entry == null) {
unknownClasses.add(sourceClassName);
continue;
} else if (entry == UNREFERENCED_EXTERNAL) {
// An @external without an accessor method. This is OK.
continue;
}
JMethod method = entry.getMethod();
String obfuscatedClassName = entry.getObfuscatedClassName();
// Consume the interstitial portion of the original selector
sb.append(sel.subSequence(start, ma.start(1)));
sb.append(obfuscatedClassName);
start = ma.end(1);
actualReplacements.put(method, obfuscatedClassName);
missingClasses.remove(method);
}
if (start != 0) {
// Consume the remainder and update the selector
sb.append(sel.subSequence(start, originalLength));
x.setSelector(sb.toString());
}
}
@Override
public void endVisit(CssStylesheet x, Context ctx) {
boolean stop = false;
// Skip names corresponding to @def entries. They too can be declared as
// String accessors.
List<JMethod> toRemove = new ArrayList<JMethod>();
for (JMethod method : missingClasses) {
if (cssDefs.contains(method.getName())) {
toRemove.add(method);
}
}
for (JMethod method : toRemove) {
missingClasses.remove(method);
}
if (!missingClasses.isEmpty()) {
stop = true;
TreeLogger errorLogger = logger.branch(TreeLogger.INFO,
"The following obfuscated style classes were missing from "
+ "the source CSS file:");
for (JMethod m : missingClasses) {
String name = m.getName();
ClassName className = m.getAnnotation(ClassName.class);
if (className != null) {
name = className.value();
}
errorLogger.log(TreeLogger.ERROR, name + ": Fix by adding ." + name
+ "{}");
}
}
if (strict && !unknownClasses.isEmpty()) {
stop = true;
TreeLogger errorLogger = logger.branch(TreeLogger.ERROR,
"The following unobfuscated classes were present in a strict CssResource:");
for (String s : unknownClasses) {
errorLogger.log(TreeLogger.ERROR, s);
}
if (errorLogger.isLoggable(TreeLogger.INFO)) {
errorLogger.log(TreeLogger.INFO, "Fix by adding String accessor "
+ "method(s) to the CssResource interface for obfuscated classes, "
+ "or using an @external declaration for unobfuscated classes.");
}
}
if (stop) {
throw new CssCompilerException("Missing a CSS replacement");
}
}
/**
* Reports the replacements that were actually performed by this visitor.
*/
public Map<JMethod, String> getReplacements() {
return actualReplacements;
}
/**
* Flatten class name lookups to speed selector rewriting.
*
* @param classReplacementsWithPrefix a map of local prefixes to the
* obfuscated names of imported methods. If a CssResource makes use
* of the {@link CssResource.Import} annotation, the keys of this map
* will correspond to the {@link CssResource.ImportedWithPrefix}
* value defined on the imported CssResource. The zero-length string
* key holds the obfuscated names for the CssResource that is being
* generated.
* @return A flattened version of the classReplacementWithPrefix map, where
* the keys are the source class name (with prefix included), and
* values have the obfuscated class name and associated JMethod.
*/
private Map<String, Replacement> computeReplacements(
Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
Set<String> externalClasses) {
Map<String, Replacement> toReturn = new HashMap<String, Replacement>();
for (String externalClass : externalClasses) {
toReturn.put(externalClass, UNREFERENCED_EXTERNAL);
}
for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
String prefix = outerEntry.getKey();
for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
JMethod method = entry.getKey();
String sourceClassName = method.getName();
String obfuscatedClassName = entry.getValue();
if (cssDefs.contains(sourceClassName)) {
continue;
}
ClassName className = method.getAnnotation(ClassName.class);
if (className != null) {
sourceClassName = className.value();
}
sourceClassName = prefix + sourceClassName;
if (externalClasses.contains(sourceClassName)) {
/*
* It simplifies the sanity-checking logic to treat external classes
* as though they were simply obfuscated to exactly the value the user
* wants.
*/
obfuscatedClassName = sourceClassName;
}
toReturn.put(sourceClassName, new Replacement(method,
obfuscatedClassName));
}
}
return Collections.unmodifiableMap(toReturn);
}
}