/* * 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); } }