Package com.google.gwt.libideas.resources.rg

Source Code of com.google.gwt.libideas.resources.rg.CssResourceGenerator$RtlVisitor

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

Related Classes of com.google.gwt.libideas.resources.rg.CssResourceGenerator$RtlVisitor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.