/*
* Copyright 2008 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.libideas.resources.rg;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.DefaultTextOutput;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dom.client.Element;
import com.google.gwt.libideas.resources.client.CssResource;
import com.google.gwt.libideas.resources.client.DataResource;
import com.google.gwt.libideas.resources.client.ImageResource;
import com.google.gwt.libideas.resources.client.CssResource.ClassName;
import com.google.gwt.libideas.resources.client.CssResource.Import;
import com.google.gwt.libideas.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.libideas.resources.client.CssResource.Shared;
import com.google.gwt.libideas.resources.client.CssResource.Strict;
import com.google.gwt.libideas.resources.client.ImageResource.ImageOptions;
import com.google.gwt.libideas.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.libideas.resources.css.CssGenerationVisitor;
import com.google.gwt.libideas.resources.css.GenerateCssAst;
import com.google.gwt.libideas.resources.css.ast.Context;
import com.google.gwt.libideas.resources.css.ast.CssCompilerException;
import com.google.gwt.libideas.resources.css.ast.CssDef;
import com.google.gwt.libideas.resources.css.ast.CssEval;
import com.google.gwt.libideas.resources.css.ast.CssIf;
import com.google.gwt.libideas.resources.css.ast.CssMediaRule;
import com.google.gwt.libideas.resources.css.ast.CssModVisitor;
import com.google.gwt.libideas.resources.css.ast.CssNoFlip;
import com.google.gwt.libideas.resources.css.ast.CssNode;
import com.google.gwt.libideas.resources.css.ast.CssProperty;
import com.google.gwt.libideas.resources.css.ast.CssRule;
import com.google.gwt.libideas.resources.css.ast.CssSelector;
import com.google.gwt.libideas.resources.css.ast.CssSprite;
import com.google.gwt.libideas.resources.css.ast.CssStylesheet;
import com.google.gwt.libideas.resources.css.ast.CssUrl;
import com.google.gwt.libideas.resources.css.ast.CssVisitor;
import com.google.gwt.libideas.resources.css.ast.HasNodes;
import com.google.gwt.libideas.resources.css.ast.CssProperty.DotPathValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.ExpressionValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.IdentValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.ListValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.NumberValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.StringValue;
import com.google.gwt.libideas.resources.css.ast.CssProperty.Value;
import com.google.gwt.libideas.resources.ext.ResourceBundleRequirements;
import com.google.gwt.libideas.resources.ext.ResourceContext;
import com.google.gwt.libideas.resources.ext.ResourceGeneratorUtil;
import com.google.gwt.libideas.resources.rebind.StringSourceWriter;
import com.google.gwt.user.rebind.SourceWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.Adler32;
/**
* Provides implementations of CSSResources.
*/
public class CssResourceGenerator extends AbstractResourceGenerator {
static class ClassRenamer extends CssVisitor {
private final Map<String, Map<JMethod, String>> classReplacementsWithPrefix;
private final Pattern classSelectorPattern = Pattern.compile("\\.([^ :>+#.]*)");
private final TreeLogger logger;
private final Set<JMethod> missingClasses;
private final Set<String> replacedClasses = new HashSet<String>();
private final boolean strict;
private final Set<String> unknownClasses = new HashSet<String>();
public ClassRenamer(TreeLogger logger,
Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
boolean strict) {
this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
this.classReplacementsWithPrefix = classReplacementsWithPrefix;
this.strict = strict;
// Require a definition for all classes in the default namespace
assert classReplacementsWithPrefix.containsKey("");
missingClasses = new HashSet<JMethod>(
classReplacementsWithPrefix.get("").keySet());
}
@Override
public void endVisit(CssSelector x, Context ctx) {
String sel = x.getSelector();
// TODO This would be simplified by having a class hierarchy for selectors
for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
String prefix = outerEntry.getKey();
for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
String name = entry.getKey().getName();
ClassName className = entry.getKey().getAnnotation(ClassName.class);
if (className != null) {
name = className.value();
}
name = prefix + name;
Pattern p = Pattern.compile("(.*)\\.(" + Pattern.quote(name)
+ ")([ :>+#.].*|$)");
Matcher m = p.matcher(sel);
if (m.find()) {
sel = m.group(1) + "." + entry.getValue() + m.group(3);
missingClasses.remove(entry.getKey());
if (strict) {
replacedClasses.add(entry.getValue());
}
}
}
}
sel = sel.trim();
if (strict) {
Matcher m = classSelectorPattern.matcher(sel);
while (m.find()) {
String classSelector = m.group(1);
if (!replacedClasses.contains(classSelector)) {
unknownClasses.add(classSelector);
}
}
}
x.setSelector(sel);
}
@Override
public void endVisit(CssStylesheet x, Context ctx) {
boolean stop = false;
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 (stop) {
throw new CssCompilerException("Missing a CSS replacement");
}
}
}
/**
* This delegate class bypasses traversal of a node, instead traversing the
* node's children. Any modifications made to the node list of the
* CollapsedNode will be reflected in the original node.
*/
static class CollapsedNode extends CssNode implements HasNodes {
private final List<CssNode> nodes;
public CollapsedNode(HasNodes parent) {
this(parent.getNodes());
}
public CollapsedNode(List<CssNode> nodes) {
this.nodes = nodes;
}
public List<CssNode> getNodes() {
return nodes;
}
public void traverse(CssVisitor visitor, Context context) {
visitor.acceptWithInsertRemove(getNodes());
}
}
/**
* Statically evaluates {@literal @if} rules.
*/
static class IfEvaluator extends CssModVisitor {
private final TreeLogger logger;
private final PropertyOracle oracle;
public IfEvaluator(TreeLogger logger, PropertyOracle oracle) {
this.logger = logger.branch(TreeLogger.DEBUG,
"Replacing property-based @if blocks");
this.oracle = oracle;
}
@Override
public void endVisit(CssIf x, Context ctx) {
if (x.getExpression() != null) {
// This gets taken care of by the runtime substitution visitor
} else {
try {
String propertyName = x.getPropertyName();
String propValue = oracle.getPropertyValue(logger, propertyName);
/*
* If the deferred binding property's value is in the list of values
* in the @if rule, move the rules into the @if's context.
*/
if (Arrays.asList(x.getPropertyValues()).contains(propValue)
^ x.isNegated()) {
for (CssNode n : x.getNodes()) {
ctx.insertBefore(n);
}
} else {
// Otherwise, move the else block into the if statement's position
for (CssNode n : x.getElseNodes()) {
ctx.insertBefore(n);
}
}
// Always delete @if rules that we can statically evaluate
ctx.removeMe();
} catch (BadPropertyValueException e) {
logger.log(TreeLogger.ERROR, "Unable to evaluate @if block", e);
throw new CssCompilerException("Unable to parse CSS", e);
}
}
}
}
static class JClassOrderComparator implements Comparator<JClassType> {
public int compare(JClassType o1, JClassType o2) {
return o1.getQualifiedSourceName().compareTo(o2.getQualifiedSourceName());
}
}
/**
* Merges rules that have matching selectors.
*/
static class MergeIdenticalSelectorsVisitor extends CssModVisitor {
private final Map<String, CssRule> canonicalRules = new HashMap<String, CssRule>();
private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
@Override
public boolean visit(CssIf x, Context ctx) {
visitInNewContext(x.getNodes());
visitInNewContext(x.getElseNodes());
return false;
}
@Override
public boolean visit(CssMediaRule x, Context ctx) {
visitInNewContext(x.getNodes());
return false;
}
@Override
public boolean visit(CssRule x, Context ctx) {
// Assumed to run immediately after SplitRulesVisitor
assert x.getSelectors().size() == 1;
CssSelector sel = x.getSelectors().get(0);
if (canonicalRules.containsKey(sel.getSelector())) {
CssRule canonical = canonicalRules.get(sel.getSelector());
// Check everything between the canonical rule and this rule for common
// properties. If there are common properties, it would be unsafe to
// promote the rule.
boolean hasCommon = false;
int index = rulesInOrder.indexOf(canonical) + 1;
assert index != 0;
for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
&& !hasCommon;) {
hasCommon = haveCommonProperties(i.next(), x);
}
if (!hasCommon) {
// It's safe to promote the rule
canonical.getProperties().addAll(x.getProperties());
ctx.removeMe();
return false;
}
}
canonicalRules.put(sel.getSelector(), x);
rulesInOrder.add(x);
return false;
}
private void visitInNewContext(List<CssNode> nodes) {
MergeIdenticalSelectorsVisitor v = new MergeIdenticalSelectorsVisitor();
v.accept(nodes);
rulesInOrder.addAll(v.rulesInOrder);
}
}
/**
* Merges rules that have identical content.
*/
static class MergeRulesByContentVisitor extends CssModVisitor {
private Map<String, CssRule> rulesByContents = new HashMap<String, CssRule>();
private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
@Override
public boolean visit(CssIf x, Context ctx) {
visitInNewContext(x.getNodes());
visitInNewContext(x.getElseNodes());
return false;
}
@Override
public boolean visit(CssMediaRule x, Context ctx) {
visitInNewContext(x.getNodes());
return false;
}
@Override
public boolean visit(CssRule x, Context ctx) {
StringBuilder b = new StringBuilder();
for (CssProperty p : x.getProperties()) {
b.append(p.getName()).append(":").append(p.getValues().getExpression());
}
String content = b.toString();
CssRule canonical = rulesByContents.get(content);
// Check everything between the canonical rule and this rule for common
// properties. If there are common properties, it would be unsafe to
// promote the rule.
if (canonical != null) {
boolean hasCommon = false;
int index = rulesInOrder.indexOf(canonical) + 1;
assert index != 0;
for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
&& !hasCommon;) {
hasCommon = haveCommonProperties(i.next(), x);
}
if (!hasCommon) {
canonical.getSelectors().addAll(x.getSelectors());
ctx.removeMe();
return false;
}
}
rulesByContents.put(content, x);
rulesInOrder.add(x);
return false;
}
private void visitInNewContext(List<CssNode> nodes) {
MergeRulesByContentVisitor v = new MergeRulesByContentVisitor();
v.accept(nodes);
rulesInOrder.addAll(v.rulesInOrder);
}
}
static class RequirementsCollector extends CssVisitor {
private final TreeLogger logger;
private final ResourceBundleRequirements requirements;
public RequirementsCollector(TreeLogger logger,
ResourceBundleRequirements requirements) {
this.logger = logger.branch(TreeLogger.DEBUG,
"Scanning CSS for requirements");
this.requirements = requirements;
}
@Override
public void endVisit(CssIf x, Context ctx) {
String propertyName = x.getPropertyName();
if (propertyName != null) {
try {
requirements.addPermutationAxis(propertyName);
} catch (BadPropertyValueException e) {
logger.log(TreeLogger.ERROR, "Unknown deferred-binding property "
+ propertyName, e);
throw new CssCompilerException("Unknown deferred-binding property", e);
}
}
}
}
static class RtlVisitor extends CssModVisitor {
/**
* Records if we're currently visiting a CssRule whose only selector is
* "body".
*/
private boolean inBodyRule;
@Override
public void endVisit(CssProperty x, Context ctx) {
String name = x.getName();
List<Value> values = x.getValues().getValues();
if (name.equalsIgnoreCase("left")) {
x.setName("right");
} else if (name.equalsIgnoreCase("right")) {
x.setName("left");
} else if (name.endsWith("-left")) {
int len = name.length();
x.setName(name.substring(0, len - 4) + "right");
} else if (name.endsWith("-right")) {
int len = name.length();
x.setName(name.substring(0, len - 5) + "left");
} else if (name.contains("-right-")) {
x.setName(name.replace("-right-", "-left-"));
} else if (name.contains("-left-")) {
x.setName(name.replace("-left-", "-right-"));
} else {
invokePropertyHandler(x.getName(), values);
}
}
@Override
public boolean visit(CssNoFlip x, Context ctx) {
return false;
}
@Override
public boolean visit(CssRule x, Context ctx) {
inBodyRule = x.getSelectors().size() == 1
&& x.getSelectors().get(0).getSelector().equals("body");
return true;
}
void propertyHandlerBackground(List<Value> values) {
/*
* The first numeric value will be treated as the left position only if we
* havn't seen any value that could potentially be the left value.
*/
boolean seenLeft = false;
for (ListIterator<Value> it = values.listIterator(); it.hasNext();) {
Value v = it.next();
Value maybeFlipped = flipLeftRightIdentValue(v);
NumberValue nv = v.isNumberValue();
if (v != maybeFlipped) {
it.set(maybeFlipped);
seenLeft = true;
} else if (isIdent(v, "center")) {
seenLeft = true;
} else if (!seenLeft && (nv != null)) {
seenLeft = true;
if ("%".equals(nv.getUnits())) {
float position = 100f - nv.getValue();
it.set(new NumberValue(position, "%"));
break;
}
}
}
}
void propertyHandlerBackgroundPosition(List<Value> values) {
propertyHandlerBackground(values);
}
Value propertyHandlerBackgroundPositionX(Value v) {
ArrayList<Value> list = new ArrayList<Value>(1);
list.add(v);
propertyHandlerBackground(list);
return list.get(0);
}
/**
* Note there should be no propertyHandlerBorder(). The CSS spec states that
* the border property must set all values at once.
*/
void propertyHandlerBorderColor(List<Value> values) {
swapFour(values);
}
void propertyHandlerBorderStyle(List<Value> values) {
swapFour(values);
}
void propertyHandlerBorderWidth(List<Value> values) {
swapFour(values);
}
Value propertyHandlerClear(Value v) {
return propertyHandlerFloat(v);
}
Value propertyHandlerCursor(Value v) {
IdentValue identValue = v.isIdentValue();
if (identValue == null) {
return v;
}
String ident = identValue.getIdent().toLowerCase();
if (!ident.endsWith("-resize")) {
return v;
}
StringBuffer newIdent = new StringBuffer();
if (ident.length() == 9) {
if (ident.charAt(0) == 'n') {
newIdent.append('n');
ident = ident.substring(1);
} else if (ident.charAt(0) == 's') {
newIdent.append('s');
ident = ident.substring(1);
} else {
return v;
}
}
if (ident.length() == 8) {
if (ident.charAt(0) == 'e') {
newIdent.append("w-resize");
} else if (ident.charAt(0) == 'w') {
newIdent.append("e-resize");
} else {
return v;
}
return new IdentValue(newIdent.toString());
} else {
return v;
}
}
Value propertyHandlerDirection(Value v) {
if (inBodyRule) {
if (isIdent(v, "ltr")) {
return new IdentValue("rtl");
} else if (isIdent(v, "rtl")) {
return new IdentValue("ltr");
}
}
return v;
}
Value propertyHandlerFloat(Value v) {
return flipLeftRightIdentValue(v);
}
void propertyHandlerMargin(List<Value> values) {
swapFour(values);
}
void propertyHandlerPadding(List<Value> values) {
swapFour(values);
}
Value propertyHandlerPageBreakAfter(Value v) {
return flipLeftRightIdentValue(v);
}
Value propertyHandlerPageBreakBefore(Value v) {
return flipLeftRightIdentValue(v);
}
Value propertyHandlerTextAlign(Value v) {
return flipLeftRightIdentValue(v);
}
private Value flipLeftRightIdentValue(Value v) {
if (isIdent(v, "right")) {
return new IdentValue("left");
} else if (isIdent(v, "left")) {
return new IdentValue("right");
}
return v;
}
/**
* Reflectively invokes a propertyHandler method for the named property.
* Dashed names are transformed into camel-case names; only letters
* following a dash will be capitalized when looking for a method to prevent
* <code>fooBar<code> and <code>foo-bar</code> from colliding.
*/
private void invokePropertyHandler(String name, List<Value> values) {
// See if we have a property-handler function
try {
String[] parts = name.toLowerCase().split("-");
StringBuffer methodName = new StringBuffer("propertyHandler");
for (String part : parts) {
methodName.append(Character.toUpperCase(part.charAt(0)));
methodName.append(part, 1, part.length());
}
try {
// Single-arg for simplicity
Method m = getClass().getDeclaredMethod(methodName.toString(),
Value.class);
assert Value.class.isAssignableFrom(m.getReturnType());
Value newValue = (Value) m.invoke(this, values.get(0));
values.set(0, newValue);
} catch (NoSuchMethodException e) {
// OK
}
try {
// Or the whole List for completeness
Method m = getClass().getDeclaredMethod(methodName.toString(),
List.class);
m.invoke(this, values);
} catch (NoSuchMethodException e) {
// OK
}
} catch (SecurityException e) {
throw new CssCompilerException(
"Unable to invoke property handler function for " + name, e);
} catch (IllegalArgumentException e) {
throw new CssCompilerException(
"Unable to invoke property handler function for " + name, e);
} catch (IllegalAccessException e) {
throw new CssCompilerException(
"Unable to invoke property handler function for " + name, e);
} catch (InvocationTargetException e) {
throw new CssCompilerException(
"Unable to invoke property handler function for " + name, e);
}
}
private boolean isIdent(Value value, String query) {
IdentValue v = value.isIdentValue();
return v != null && v.getIdent().equalsIgnoreCase(query);
}
/**
* Swaps the second and fourth values in a list of four values.
*/
private void swapFour(List<Value> values) {
if (values.size() == 4) {
Collections.swap(values, 1, 3);
}
}
}
/**
* Splits rules with compound selectors into multiple rules.
*/
static class SplitRulesVisitor extends CssModVisitor {
@Override
public void endVisit(CssRule x, Context ctx) {
if (x.getSelectors().size() == 1) {
return;
}
for (CssSelector sel : x.getSelectors()) {
CssRule newRule = new CssRule();
newRule.getSelectors().add(sel);
newRule.getProperties().addAll(x.getProperties());
ctx.insertBefore(newRule);
}
ctx.removeMe();
return;
}
}
/**
* Replaces CssSprite nodes with CssRule nodes that will display the sprited
* image. The real trick with spriting the images is to reuse the
* ImageResource processing framework by requiring the sprite to be defined in
* terms of an ImageResource.
*/
static class Spriter extends CssModVisitor {
private final ResourceContext context;
private final TreeLogger logger;
public Spriter(TreeLogger logger, ResourceContext context) {
this.logger = logger.branch(TreeLogger.DEBUG,
"Creating image sprite classes");
this.context = context;
}
@Override
public void endVisit(CssSprite x, Context ctx) {
JClassType bundleType = context.getResourceBundleType();
String functionName = x.getResourceFunction();
if (functionName == null) {
logger.log(TreeLogger.ERROR, "The @sprite rule " + x.getSelectors()
+ " must specify the " + CssSprite.IMAGE_PROPERTY_NAME
+ " property");
throw new CssCompilerException("No image property specified");
}
// Find the image accessor method
JMethod imageMethod = null;
JMethod[] allMethods = bundleType.getOverridableMethods();
for (int i = 0; imageMethod == null && i < allMethods.length; i++) {
JMethod candidate = allMethods[i];
// If the function name matches and takes no parameters
if (candidate.getName().equals(functionName)
&& candidate.getParameters().length == 0) {
// We have a match
imageMethod = candidate;
}
}
// Method unable to be located
if (imageMethod == null) {
logger.log(TreeLogger.ERROR, "Unable to find ImageResource method "
+ functionName + " in " + bundleType.getQualifiedSourceName());
throw new CssCompilerException("Cannot find image function");
}
JClassType imageResourceType = context.getGeneratorContext().getTypeOracle().findType(
ImageResource.class.getName());
assert imageResourceType != null;
if (!imageResourceType.isAssignableFrom(imageMethod.getReturnType().isClassOrInterface())) {
logger.log(TreeLogger.ERROR, "The return type of " + functionName
+ " is not assignable to "
+ imageResourceType.getSimpleSourceName());
throw new CssCompilerException("Incorrect return type for "
+ CssSprite.IMAGE_PROPERTY_NAME + " method");
}
ImageOptions options = imageMethod.getAnnotation(ImageOptions.class);
RepeatStyle repeatStyle;
if (options != null) {
repeatStyle = options.repeatStyle();
} else {
repeatStyle = RepeatStyle.None;
}
String instance = "(" + context.getImplementationSimpleSourceName()
+ ".this." + functionName + "())";
CssRule replacement = new CssRule();
replacement.getSelectors().addAll(x.getSelectors());
List<CssProperty> properties = replacement.getProperties();
if (repeatStyle == RepeatStyle.None
|| repeatStyle == RepeatStyle.Horizontal) {
properties.add(new CssProperty("height", new ExpressionValue(instance
+ ".getHeight() + \"px\""), false));
}
if (repeatStyle == RepeatStyle.None
|| repeatStyle == RepeatStyle.Vertical) {
properties.add(new CssProperty("width", new ExpressionValue(instance
+ ".getWidth() + \"px\""), false));
}
properties.add(new CssProperty("overflow", new StringValue("hidden"),
false));
String repeatText;
switch (repeatStyle) {
case None:
repeatText = " no-repeat";
break;
case Horizontal:
repeatText = " repeat-x";
break;
case Vertical:
repeatText = " repeat-y";
break;
case Both:
repeatText = " repeat";
break;
default:
throw new RuntimeException("Unknown repeatStyle " + repeatStyle);
}
String backgroundExpression = "\"url(\\\"\" + " + instance
+ ".getURL() + \"\\\") -\" + " + instance
+ ".getLeft() + \"px -\" + " + instance + ".getTop() + \"px "
+ repeatText + "\"";
properties.add(new CssProperty("background", new ExpressionValue(
backgroundExpression), false));
// Retain any user-specified properties
properties.addAll(x.getProperties());
ctx.replaceMe(replacement);
}
}
static class SubstitutionCollector extends CssVisitor {
private final Map<String, CssDef> substitutions = new HashMap<String, CssDef>();
@Override
public void endVisit(CssDef x, Context ctx) {
substitutions.put(x.getKey(), x);
}
@Override
public void endVisit(CssEval x, Context ctx) {
substitutions.put(x.getKey(), x);
}
@Override
public void endVisit(CssUrl x, Context ctx) {
substitutions.put(x.getKey(), x);
}
}
/**
* Substitute symbolic replacements into string values.
*/
static class SubstitutionReplacer extends CssVisitor {
private final ResourceContext context;
private final TreeLogger logger;
private final Map<String, CssDef> substitutions;
public SubstitutionReplacer(TreeLogger logger, ResourceContext context,
Map<String, CssDef> substitutions) {
this.context = context;
this.logger = logger;
this.substitutions = substitutions;
}
@Override
public void endVisit(CssProperty x, Context ctx) {
if (x.getValues() == null) {
// Nothing to do
return;
}
List<Value> values = new ArrayList<Value>(x.getValues().getValues());
for (ListIterator<Value> i = values.listIterator(); i.hasNext();) {
IdentValue v = i.next().isIdentValue();
if (v == null) {
// Don't try to substitute into anything other than idents
continue;
}
String value = v.getIdent();
CssDef def = substitutions.get(value);
if (def == null) {
continue;
} else if (def instanceof CssUrl) {
assert def.getValues().size() == 1;
assert def.getValues().get(0).isIdentValue() != null;
String functionName = def.getValues().get(0).isIdentValue().getIdent();
// Find the method
JMethod method = context.getResourceBundleType().findMethod(
functionName, new JType[0]);
if (method == null) {
logger.log(TreeLogger.ERROR, "Unable to find DataResource method "
+ functionName + " in "
+ context.getResourceBundleType().getQualifiedSourceName());
throw new CssCompilerException("Cannot find data function");
}
String instance = "((" + DataResource.class.getName() + ")("
+ context.getImplementationSimpleSourceName() + ".this."
+ functionName + "()))";
StringBuilder expression = new StringBuilder();
expression.append("\"url('\" + ");
expression.append(instance).append(".getUrl()");
expression.append(" + \"')\"");
i.set(new ExpressionValue(expression.toString()));
} else {
i.remove();
for (Value defValue : def.getValues()) {
i.add(defValue);
}
}
}
x.setValue(new ListValue(values));
}
}
/**
* 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.
*/
private 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'};
/**
* This value is used by {@link #concatOp} to help create a more balanced AST
* tree by producing parenthetical expressions.
*/
private static final int CONCAT_EXPRESSION_LIMIT = 20;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
System.out.println(makeIdent(i));
}
}
static boolean haveCommonProperties(CssRule a, CssRule b) {
if (a.getProperties().size() == 0 || b.getProperties().size() == 0) {
return false;
}
SortedSet<String> aProperties = new TreeSet<String>();
SortedSet<String> bProperties = new TreeSet<String>();
for (CssProperty p : a.getProperties()) {
aProperties.add(p.getName());
}
for (CssProperty p : b.getProperties()) {
bProperties.add(p.getName());
}
Iterator<String> ai = aProperties.iterator();
Iterator<String> bi = bProperties.iterator();
String aName = ai.next();
String bName = bi.next();
for (;;) {
int comp = aName.compareToIgnoreCase(bName);
if (comp == 0) {
return true;
} else if (comp > 0) {
if (aName.startsWith(bName + "-")) {
return true;
}
if (!bi.hasNext()) {
break;
}
bName = bi.next();
} else {
if (bName.startsWith(aName + "-")) {
return true;
}
if (!ai.hasNext()) {
break;
}
aName = ai.next();
}
}
return false;
}
/**
* Create a Java expression that evaluates to a string representation of the
* given node. Visible only for testing.
*/
static <T extends CssNode & HasNodes> String makeExpression(
TreeLogger logger, ResourceContext context, JClassType cssResourceType,
T node, boolean prettyOutput) throws UnableToCompleteException {
// Generate the CSS template
DefaultTextOutput out = new DefaultTextOutput(!prettyOutput);
CssGenerationVisitor v = new CssGenerationVisitor(out);
v.accept(node);
// Generate the final Java expression
String template = out.toString();
StringBuilder b = new StringBuilder();
int start = 0;
/*
* Very large concatenation expressions using '+' cause the GWT compiler to
* overflow the stack due to deep AST nesting. The workaround for now is to
* force it to be more balanced using intermediate concatenation groupings.
*
* This variable is used to track the number of subexpressions within the
* current parenthetical expression.
*/
int numExpressions = 0;
b.append('(');
for (Map.Entry<Integer, List<CssNode>> entry : v.getSubstitutionPositions().entrySet()) {
// Add the static section between start and the substitution point
b.append('"');
b.append(Generator.escape(template.substring(start, entry.getKey())));
b.append('\"');
numExpressions = concatOp(numExpressions, b);
// Add the nodes at the substitution point
for (CssNode x : entry.getValue()) {
TreeLogger loopLogger = logger.branch(TreeLogger.DEBUG,
"Performing substitution in node " + x.toString());
if (x instanceof CssIf) {
CssIf asIf = (CssIf) x;
// Generate the sub-expressions
String expression = makeExpression(loopLogger, context,
cssResourceType, new CollapsedNode(asIf), prettyOutput);
String elseExpression;
if (asIf.getElseNodes().isEmpty()) {
// We'll treat an empty else block as an empty string
elseExpression = "\"\"";
} else {
elseExpression = makeExpression(loopLogger, context,
cssResourceType, new CollapsedNode(asIf.getElseNodes()),
prettyOutput);
}
// ((expr) ? "CSS" : "elseCSS") +
b.append("((" + asIf.getExpression() + ") ? " + expression + " : "
+ elseExpression + ") ");
numExpressions = concatOp(numExpressions, b);
} else if (x instanceof CssProperty) {
CssProperty property = (CssProperty) x;
validateValue(loopLogger, context.getResourceBundleType(),
property.getValues());
// (expr) +
b.append("(" + property.getValues().getExpression() + ") ");
numExpressions = concatOp(numExpressions, b);
} else {
// This indicates that some magic node is slipping by our visitors
loopLogger.log(TreeLogger.ERROR, "Unhandled substitution "
+ x.getClass());
throw new UnableToCompleteException();
}
}
start = entry.getKey();
}
// Add the remaining parts of the template
b.append('"');
b.append(Generator.escape(template.substring(start)));
b.append('"');
b.append(')');
return b.toString();
}
/**
* Check if number of concat expressions currently exceeds limit and either
* append '+' if the limit isn't reached or ') + (' if it is.
*
* @return numExpressions + 1 or 0 if limit was exceeded.
*/
private static int concatOp(int numExpressions, StringBuilder b) {
/*
* TODO: Fix the compiler to better handle arbitrarily long concatenation
* expressions.
*/
if (numExpressions >= CONCAT_EXPRESSION_LIMIT) {
b.append(") + (");
return 0;
}
b.append(" + ");
return numExpressions + 1;
}
private static String makeIdent(long id) {
assert id >= 0;
StringBuilder b = new StringBuilder();
// Use only guaranteed-alpha characters for the first character
b.append(BASE32_CHARS[(int) (id & 0xf)]);
id >>= 4;
while (id != 0) {
b.append(BASE32_CHARS[(int) (id & 0x1f)]);
id >>= 5;
}
return b.toString();
}
/**
* This function validates any context-sensitive Values.
*/
private static void validateValue(TreeLogger logger,
JClassType resourceBundleType, Value value)
throws UnableToCompleteException {
ListValue list = value.isListValue();
if (list != null) {
for (Value v : list.getValues()) {
validateValue(logger, resourceBundleType, v);
}
return;
}
DotPathValue dot = value.isDotPathValue();
if (dot != null) {
String[] elements = dot.getPath().split("\\.");
if (elements.length == 0) {
logger.log(TreeLogger.ERROR, "value() functions must specify a path");
throw new UnableToCompleteException();
}
JType currentType = resourceBundleType;
for (Iterator<String> i = Arrays.asList(elements).iterator(); i.hasNext();) {
String pathElement = i.next();
JClassType referenceType = currentType.isClassOrInterface();
if (referenceType == null) {
logger.log(TreeLogger.ERROR, "Cannot resolve member " + pathElement
+ " on non-reference type "
+ currentType.getQualifiedSourceName());
throw new UnableToCompleteException();
}
try {
JMethod m = referenceType.getMethod(pathElement, new JType[0]);
currentType = m.getReturnType();
} catch (NotFoundException e) {
logger.log(TreeLogger.ERROR, "Could not find no-arg method named "
+ pathElement + " in type "
+ currentType.getQualifiedSourceName());
throw new UnableToCompleteException();
}
}
return;
}
}
private String classPrefix;
private JClassType cssResourceType;
private JClassType elementType;
private boolean enableMerge;
private boolean prettyOutput;
private Map<JClassType, Map<JMethod, String>> replacementsByClassAndMethod;
private Map<JMethod, String> replacementsForSharedMethods;
private Map<JMethod, CssStylesheet> stylesheetMap;
private JClassType stringType;
@Override
public String createAssignment(TreeLogger logger, ResourceContext context,
JMethod method) throws UnableToCompleteException {
SourceWriter sw = new StringSourceWriter();
// Write the expression to create the subtype.
sw.println("new " + method.getReturnType().getQualifiedSourceName()
+ "() {");
sw.indent();
JClassType cssResourceSubtype = method.getReturnType().isInterface();
assert cssResourceSubtype != null;
Map<String, Map<JMethod, String>> replacementsWithPrefix = new HashMap<String, Map<JMethod, String>>();
replacementsWithPrefix.put("",
computeReplacementsForType(cssResourceSubtype));
Import imp = method.getAnnotation(Import.class);
if (imp != null) {
boolean fail = false;
for (Class<? extends CssResource> clazz : imp.value()) {
JClassType importType = context.getGeneratorContext().getTypeOracle().findType(
clazz.getName().replace('$', '.'));
String prefix = importType.getSimpleSourceName();
ImportedWithPrefix exp = importType.getAnnotation(ImportedWithPrefix.class);
if (exp != null) {
prefix = exp.value();
}
assert importType != null;
if (replacementsWithPrefix.put(prefix + "-",
computeReplacementsForType(importType)) != null) {
logger.log(TreeLogger.ERROR,
"Multiple imports that would use the prefix " + prefix);
fail = true;
}
}
if (fail) {
throw new UnableToCompleteException();
}
}
/*
* getOverridableMethods is used to handle CssResources extending
* non-CssResource types. See the discussion in computeReplacementsForType.
*/
for (JMethod toImplement : cssResourceSubtype.getOverridableMethods()) {
String name = toImplement.getName();
if ("getName".equals(name) || "getText".equals(name)) {
continue;
}
if (toImplement.getReturnType().equals(stringType)
&& toImplement.getParameters().length == 0) {
writeClassAssignment(sw, toImplement, replacementsWithPrefix.get(""));
} else if (toImplement.getReturnType().isPrimitive() != null
&& toImplement.getParameters().length == 0) {
writeDefAssignment(logger, sw, toImplement, stylesheetMap.get(method));
} else {
logger.log(TreeLogger.ERROR, "Don't know how to implement method "
+ toImplement.getName());
throw new UnableToCompleteException();
}
}
sw.println("public String getText() {");
sw.indent();
boolean strict = method.getAnnotation(Strict.class) != null;
if (!strict) {
/*
* The developer may choose to force strict behavior onto the system. If
* the method does already have the @Strict annotation, print a warning.
*/
try {
PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle();
String propertyValue = propertyOracle.getPropertyValue(logger,
"CssResource.forceStrict");
if (Boolean.valueOf(propertyValue)) {
logger.log(TreeLogger.WARN, "CssResource.forceStrict is true, but "
+ method.getName() + "() is missing the @Strict annotation.");
strict = true;
}
} catch (BadPropertyValueException e) {
// Ignore
}
}
String cssExpression = makeExpression(logger, context, cssResourceSubtype,
stylesheetMap.get(method), replacementsWithPrefix, strict);
sw.println("return " + cssExpression + ";");
sw.outdent();
sw.println("}");
sw.println("public String getName() {");
sw.indent();
sw.println("return \"" + method.getName() + "\";");
sw.outdent();
sw.println("}");
sw.outdent();
sw.println("}");
return sw.toString();
}
@Override
public void init(TreeLogger logger, ResourceContext context)
throws UnableToCompleteException {
try {
PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle();
String style = propertyOracle.getPropertyValue(logger,
"CssResource.style").toLowerCase();
prettyOutput = style.equals("pretty");
String merge = propertyOracle.getPropertyValue(logger,
"CssResource.enableMerge").toLowerCase();
enableMerge = merge.equals("true");
classPrefix = propertyOracle.getPropertyValue(logger,
"CssResource.globalPrefix");
} catch (BadPropertyValueException e) {
logger.log(TreeLogger.WARN, "Unable to query module property", e);
throw new UnableToCompleteException();
}
if ("default".equals(classPrefix)) {
// Compute it later in computeObfuscatedNames();
classPrefix = null;
} else if ("empty".equals(classPrefix)) {
classPrefix = "";
}
// Find all of the types that we care about in the type system
TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
cssResourceType = typeOracle.findType(CssResource.class.getName());
assert cssResourceType != null;
elementType = typeOracle.findType(Element.class.getName());
assert elementType != null;
stringType = typeOracle.findType(String.class.getName());
assert stringType != null;
replacementsByClassAndMethod = new IdentityHashMap<JClassType, Map<JMethod, String>>();
replacementsForSharedMethods = new IdentityHashMap<JMethod, String>();
stylesheetMap = new IdentityHashMap<JMethod, CssStylesheet>();
computeObfuscatedNames(logger);
}
@Override
public void prepare(TreeLogger logger, ResourceContext context,
ResourceBundleRequirements requirements, JMethod method)
throws UnableToCompleteException {
URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
method);
if (method.getReturnType().isInterface() == null) {
logger.log(TreeLogger.ERROR, "Return type must be an interface");
throw new UnableToCompleteException();
}
// Create the AST and do a quick scan for requirements
CssStylesheet sheet = GenerateCssAst.exec(logger, resources);
stylesheetMap.put(method, sheet);
(new RequirementsCollector(logger, requirements)).accept(sheet);
}
/**
* Each distinct type of CssResource has a unique collection of values that it
* will return, excepting for those methods that are defined within an
* interface that is tagged with {@code @Shared}.
*/
private void computeObfuscatedNames(TreeLogger logger) {
logger = logger.branch(TreeLogger.DEBUG, "Computing CSS class replacements");
SortedSet<JClassType> cssResourceSubtypes = computeOperableTypes(logger);
if (classPrefix == null) {
Adler32 checksum = new Adler32();
for (JClassType type : cssResourceSubtypes) {
checksum.update(Util.getBytes(type.getQualifiedSourceName()));
}
classPrefix = "G"
+ Long.toString(checksum.getValue(), Character.MAX_RADIX);
}
int count = 0;
for (JClassType type : cssResourceSubtypes) {
Map<JMethod, String> replacements = new IdentityHashMap<JMethod, String>();
replacementsByClassAndMethod.put(type, replacements);
for (JMethod method : type.getOverridableMethods()) {
String name = method.getName();
if ("getName".equals(name) || "getText".equals(name)
|| !stringType.equals(method.getReturnType())) {
continue;
}
// The user provided the class name to use
ClassName classNameOverride = method.getAnnotation(ClassName.class);
if (classNameOverride != null) {
name = classNameOverride.value();
}
String obfuscatedClassName;
if (prettyOutput) {
obfuscatedClassName = classPrefix + "-"
+ type.getQualifiedSourceName().replaceAll("[.$]", "-") + "-"
+ name;
} else {
obfuscatedClassName = classPrefix + makeIdent(count++);
}
replacements.put(method, obfuscatedClassName);
if (method.getEnclosingType() == type) {
Shared shared = type.getAnnotation(Shared.class);
if (shared != null) {
replacementsForSharedMethods.put(method, obfuscatedClassName);
}
}
logger.log(TreeLogger.SPAM, "Mapped " + type.getQualifiedSourceName()
+ "." + name + " to " + obfuscatedClassName);
}
}
}
/**
* Returns all interfaces derived from CssResource, sorted by qualified name.
* <p>
* We'll ignore concrete implementations of CssResource, which include types
* previously-generated by CssResourceGenerator and user-provided
* implementations of CssResource, which aren't valid for use with
* CssResourceGenerator anyway. By ignoring newly-generated CssResource types,
* we'll ensure a stable ordering, regardless of the actual execution order
* used by the Generator framework.
* <p>
* It is still possible that additional pure-interfaces could be introduced by
* other generators, which would change the result of this computation, but
* there is presently no way to determine when, or by what means, a type was
* added to the TypeOracle.
*/
private SortedSet<JClassType> computeOperableTypes(TreeLogger logger) {
logger = logger.branch(TreeLogger.DEBUG,
"Finding operable CssResource subtypes");
SortedSet<JClassType> toReturn = new TreeSet<JClassType>(
new JClassOrderComparator());
JClassType[] cssResourceSubtypes = cssResourceType.getSubtypes();
for (JClassType type : cssResourceSubtypes) {
if (type.isInterface() != null) {
logger.log(TreeLogger.SPAM, "Added " + type.getQualifiedSourceName());
toReturn.add(type);
} else {
logger.log(TreeLogger.SPAM, "Ignored " + type.getQualifiedSourceName());
}
}
return toReturn;
}
/**
* Compute the mapping of original class names to obfuscated type names for a
* given subtype of CssResource. Mappings are inherited from the type's
* supertypes.
*/
private Map<JMethod, String> computeReplacementsForType(JClassType type) {
Map<JMethod, String> toReturn = new IdentityHashMap<JMethod, String>();
/*
* We check to see if the type is derived from CssResource so that we can
* handle the case of a CssResource type being derived from a
* non-CssResource base type. This basically collapses the non-CssResource
* base types into their least-derived CssResource subtypes.
*/
if (type == null || !derivedFromCssResource(type)) {
return toReturn;
}
if (replacementsByClassAndMethod.containsKey(type)) {
toReturn.putAll(replacementsByClassAndMethod.get(type));
}
/*
* Replacements for methods defined in shared types will override any
* locally-computed values.
*/
for (JMethod method : type.getOverridableMethods()) {
if (replacementsForSharedMethods.containsKey(method)) {
assert toReturn.containsKey(method);
toReturn.put(method, replacementsForSharedMethods.get(method));
}
}
return toReturn;
}
/**
* Determine if a type is derived from CssResource.
*/
private boolean derivedFromCssResource(JClassType type) {
List<JClassType> superInterfaces = Arrays.asList(type.getImplementedInterfaces());
if (superInterfaces.contains(cssResourceType)) {
return true;
}
JClassType superClass = type.getSuperclass();
if (superClass != null) {
if (derivedFromCssResource(superClass)) {
return true;
}
}
for (JClassType superInterface : superInterfaces) {
if (derivedFromCssResource(superInterface)) {
return true;
}
}
return false;
}
/**
* Create a Java expression that evaluates to the string representation of the
* stylesheet resource.
*/
private String makeExpression(TreeLogger logger, ResourceContext context,
JClassType cssResourceType, CssStylesheet sheet,
Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
boolean strict) throws UnableToCompleteException {
try {
// Create CSS sprites
(new Spriter(logger, context)).accept(sheet);
// Perform @def and @eval substitutions
SubstitutionCollector collector = new SubstitutionCollector();
collector.accept(sheet);
(new SubstitutionReplacer(logger, context, collector.substitutions)).accept(sheet);
// Evaluate @if statements based on deferred binding properties
(new IfEvaluator(logger,
context.getGeneratorContext().getPropertyOracle())).accept(sheet);
// Rename css .class selectors
(new ClassRenamer(logger, classReplacementsWithPrefix, strict)).accept(sheet);
// Combine rules with identical selectors
if (enableMerge) {
// TODO This is an off-switch while this is being developed; remove
(new SplitRulesVisitor()).accept(sheet);
(new MergeIdenticalSelectorsVisitor()).accept(sheet);
(new MergeRulesByContentVisitor()).accept(sheet);
}
String standard = makeExpression(logger, context, cssResourceType, sheet,
prettyOutput);
(new RtlVisitor()).accept(sheet);
String reversed = makeExpression(logger, context, cssResourceType, sheet,
prettyOutput);
return "com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ? ("
+ reversed + ") : (" + standard + ")";
} catch (CssCompilerException e) {
// Take this as a sign that one of the visitors was unhappy, but only
// log the stack trace if there's a causal (i.e. unknown) exception.
logger.log(TreeLogger.ERROR, "Unable to process CSS",
e.getCause() == null ? null : e);
throw new UnableToCompleteException();
}
}
/**
* Write the CssResource accessor method for simple String return values.
*/
private void writeClassAssignment(SourceWriter sw, JMethod toImplement,
Map<JMethod, String> classReplacements) {
String replacement = classReplacements.get(toImplement);
assert replacement != null;
sw.println(toImplement.getReadableDeclaration(false, true, true, true, true)
+ "{");
sw.indent();
sw.println("return \"" + replacement + "\";");
sw.outdent();
sw.println("}");
}
private void writeDefAssignment(TreeLogger logger, SourceWriter sw,
JMethod toImplement, CssStylesheet cssStylesheet)
throws UnableToCompleteException {
SubstitutionCollector collector = new SubstitutionCollector();
collector.accept(cssStylesheet);
String name = toImplement.getName();
// TODO: Annotation for override
CssDef def = collector.substitutions.get(name);
if (def == null) {
logger.log(TreeLogger.ERROR, "No @def rule for name " + name);
throw new UnableToCompleteException();
}
// TODO: Allow returning an array of values
if (def.getValues().size() != 1) {
logger.log(TreeLogger.ERROR, "@def rule " + name
+ " must define exactly one value");
throw new UnableToCompleteException();
}
NumberValue numberValue = def.getValues().get(0).isNumberValue();
if (numberValue == null) {
logger.log(TreeLogger.ERROR, "The define named " + name
+ " does not define a numeric value");
throw new UnableToCompleteException();
}
JPrimitiveType returnType = toImplement.getReturnType().isPrimitive();
assert returnType != null;
sw.print(toImplement.getReadableDeclaration(false, false, false, false,
true));
sw.println(" {");
sw.indent();
if (returnType == JPrimitiveType.INT || returnType == JPrimitiveType.LONG) {
sw.println("return " + Math.round(numberValue.getValue()) + ";");
} else if (returnType == JPrimitiveType.FLOAT) {
sw.println("return " + numberValue.getValue() + "F;");
} else if (returnType == JPrimitiveType.DOUBLE) {
sw.println("return " + numberValue.getValue() + ";");
} else {
logger.log(TreeLogger.ERROR, returnType.getQualifiedSourceName()
+ " is not a valid return type for @def accessors");
throw new UnableToCompleteException();
}
sw.outdent();
sw.println("}");
numberValue.getValue();
}
}