property fall back value evaluation scheme - enable fall back bindings.

Review at http://gwt-code-reviews.appspot.com/1369807


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9824 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/DefaultSelectionProperty.java b/dev/core/src/com/google/gwt/core/ext/DefaultSelectionProperty.java
index 7d7535b..dc4e804 100644
--- a/dev/core/src/com/google/gwt/core/ext/DefaultSelectionProperty.java
+++ b/dev/core/src/com/google/gwt/core/ext/DefaultSelectionProperty.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.core.ext;
 
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.SortedSet;
 
 /**
@@ -27,6 +30,7 @@
   private final String fallbackValue;
   private final String name;
   private final SortedSet<String> possibleValues;
+  private final Map<String, ? extends List<? extends Set<String>>> fallbackValueMap;
 
   /**
    * Construct a selection property.
@@ -40,6 +44,23 @@
    */
   public DefaultSelectionProperty(String currentValue, String fallbackValue,
       String name, SortedSet<String> possibleValues) {
+    this(currentValue, fallbackValue, name, possibleValues, null);
+  }
+
+  /**
+   * Construct a selection property.
+   *  
+   * @param currentValue current value of this property, must not be null
+   * @param fallbackValue the fallback value to use, must not be null
+   * @param name the name of this property, must not be null
+   * @param possibleValues the set of possible values, must not be null and
+   *     will be returned to callers, so a copy should be passed into this
+   *     ctor if the caller will use this set later
+   * @param map the map propertyValue to fallback values
+   */
+  public DefaultSelectionProperty(String currentValue, String fallbackValue,
+      String name, SortedSet<String> possibleValues,
+      Map<String, ? extends List<? extends Set<String>>> fallbackValueMap) {
     assert currentValue != null;
     assert fallbackValue != null;
     assert name != null;
@@ -48,6 +69,7 @@
     this.fallbackValue = fallbackValue;
     this.name = name;
     this.possibleValues = possibleValues;
+    this.fallbackValueMap = fallbackValueMap;
   }
 
   @Override
@@ -76,6 +98,10 @@
     return fallbackValue;
   }
 
+  public List<? extends Set<String>> getFallbackValues(String value) {
+    return (null != fallbackValueMap) ? fallbackValueMap.get(value) : null;
+  }
+
   public String getName() {
     return name;
   }
diff --git a/dev/core/src/com/google/gwt/core/ext/SelectionProperty.java b/dev/core/src/com/google/gwt/core/ext/SelectionProperty.java
index e781f76..e286d75 100644
--- a/dev/core/src/com/google/gwt/core/ext/SelectionProperty.java
+++ b/dev/core/src/com/google/gwt/core/ext/SelectionProperty.java
@@ -16,6 +16,8 @@
 
 package com.google.gwt.core.ext;
 
+import java.util.List;
+import java.util.Set;
 import java.util.SortedSet;
 
 /**
@@ -47,6 +49,14 @@
   String getFallbackValue(); 
 
   /**
+   * Returns the list of fall back values for a given value. 
+   * @param value the property value
+   * @return the fall back list of values by increasing order
+   *         of preference.
+   */
+  List<? extends Set<String>> getFallbackValues(String value);
+
+  /**
    * Returns the possible values for the property in sorted order.
    * 
    * @return a SortedSet of Strings containing the possible property values.
diff --git a/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java b/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
index 69e689c..36cb777 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
@@ -28,11 +28,13 @@
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.Map.Entry;
 import java.util.regex.Pattern;
 
 /**
@@ -51,6 +53,8 @@
   private PropertyProvider provider;
   private Class<? extends PropertyProviderGenerator> providerGenerator;
   private String fallback;
+  private HashMap<String,LinkedList<LinkedHashSet<String>>> fallbackValueMap;
+  private HashMap<String,LinkedList<String>> fallbackValues = new HashMap<String,LinkedList<String>>();
   private final ConditionAll rootCondition = new ConditionAll();
 
   {
@@ -95,6 +99,20 @@
   }
 
   /**
+   * Adds fall back value to given property name. 
+   * @param value the property value.
+   * @param fallbackValue the fall back value for given property value.
+   */
+  public void addFallbackValue(String value, String fallbackValue) {
+    LinkedList<String> values = fallbackValues.get(fallbackValue);
+    if (values == null) {
+      values = new LinkedList<String>();
+      fallbackValues.put(fallbackValue, values);
+    }
+    values.addFirst(value);
+  }
+
+  /**
    * Returns the set of allowed values in sorted order when a certain condition
    * is satisfied.
    */
@@ -145,6 +163,43 @@
     return fallback;
   }
 
+  /**
+   * Returns the map of values to fall back values. the list of fall
+   * back values is in decreasing order of preference.
+   * @return map of property value to fall back values.
+   */
+  public Map<String,? extends List<? extends Set<String>>> getFallbackValuesMap() {
+    if (fallbackValueMap == null) {
+      HashMap<String,LinkedList<LinkedHashSet<String>>> valuesMap = new HashMap<String,LinkedList<LinkedHashSet<String>>>();
+      // compute closure of fall back values preserving order
+      for (Entry<String, LinkedList<String>> e : fallbackValues.entrySet()) {
+        String from = e.getKey();
+        LinkedList<LinkedHashSet<String>> alternates = new LinkedList<LinkedHashSet<String>>();
+        valuesMap.put(from, alternates);
+        LinkedList<String> childList = fallbackValues.get(from);
+        LinkedHashSet<String> children = new LinkedHashSet<String>();
+        children.addAll(childList);
+        while (children != null && children.size() > 0) 
+        {
+          alternates.add(children);
+          LinkedHashSet<String> newChildren = new LinkedHashSet<String>();
+          for (String child : children) {
+            childList = fallbackValues.get(child);
+            if (null == childList) {
+              continue;
+            }
+            for (String val : childList) {
+              newChildren.add(val);
+            }
+          }
+          children = newChildren;
+        }
+      }
+      fallbackValueMap = valuesMap;
+    }
+    return fallbackValueMap;
+  }
+  
   public PropertyProvider getProvider() {
     return provider;
   }
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ConditionWhenPropertyIs.java b/dev/core/src/com/google/gwt/dev/cfg/ConditionWhenPropertyIs.java
index a03daf9..bef8e5b 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ConditionWhenPropertyIs.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ConditionWhenPropertyIs.java
@@ -23,6 +23,7 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.util.collect.Sets;
 
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -69,6 +70,31 @@
       if (testValue.equals(value)) {
         return true;
       } else {
+        // no exact match was found, see if any fall back 
+        // value would satisfy the condition
+        try {
+          SelectionProperty prop = propertyOracle.getSelectionProperty(logger,
+              propName);
+          List<? extends Set<String>> fallbackValues = prop.getFallbackValues(value);
+          if (fallbackValues != null && fallbackValues.size() > 0) {
+            logger.log(TreeLogger.DEBUG, "Property value '" + value + "'" +
+                " is the fallback of '" + fallbackValues.toString() + "'", null);
+            int cost = -1;
+            for (Set<String> values : fallbackValues) {
+              for (String fallbackValue : values) {
+                if (testValue.equals(fallbackValue)) {
+                  query.setFallbackEvaluationCost(cost);
+                  return false;
+                }
+              }
+              cost--;
+            }
+          }
+        }
+        catch (BadPropertyValueException e) {
+          // do nothing - currently, only selection
+          // properties support fall back values
+        }
         return false;
       }
     } catch (BadPropertyValueException e) {
diff --git a/dev/core/src/com/google/gwt/dev/cfg/DeferredBindingQuery.java b/dev/core/src/com/google/gwt/dev/cfg/DeferredBindingQuery.java
index 503ca94..42b49d3 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/DeferredBindingQuery.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/DeferredBindingQuery.java
@@ -26,6 +26,7 @@
  * against such a query.
  */
 public class DeferredBindingQuery {
+  private int fallbackEvalCost = Integer.MAX_VALUE;
   private final Set<String> linkerNames;
   private final PropertyOracle propertyOracle;
   private final String testType;
@@ -54,6 +55,10 @@
     this.testType = testType;
   }
 
+  public int getFallbackEvaluationCost() {
+    return fallbackEvalCost;
+  }
+  
   public Set<String> getLinkerNames() {
     return linkerNames;
   }
@@ -69,4 +74,8 @@
   public TypeOracle getTypeOracle() {
     return typeOracle;
   }
+
+  public void setFallbackEvaluationCost(int cost) {
+    fallbackEvalCost = cost;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
index d5653e7..9d829ca 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -98,6 +98,9 @@
     @SuppressWarnings("unused") // referenced reflectively
     protected final String __extend_property_2_values = null;
 
+    @SuppressWarnings("unused") // referenced reflectively ($ means optional)
+    protected final String __extend_property_3_fallback_value$ = null;
+
     @SuppressWarnings("unused") // referenced reflectively
     protected final String __generate_with_1_class = null;
 
@@ -441,11 +444,25 @@
 
     @SuppressWarnings("unused") // called reflectively
     protected Schema __extend_property_begin(BindingProperty property,
-        PropertyValue[] values) {
+        PropertyValue[] values, PropertyFallbackValue fallbackValue) 
+        throws UnableToCompleteException {
       for (int i = 0; i < values.length; i++) {
         property.addDefinedValue(property.getRootCondition(), values[i].token);
       }
 
+      // validate fallback-property value if present
+      if (fallbackValue != null) {
+        if (!property.isDefinedValue(fallbackValue.token)) {
+        logger.log(TreeLogger.ERROR, "The property " + property.name
+            + " fallback-value was not defined");
+        throw new UnableToCompleteException(); 
+        }
+        // add fallback map to the property
+        for (int i = 0; i < values.length; i++) {
+          property.addFallbackValue(values[i].token, fallbackValue.token);
+        }
+      }
+     
       // No children.
       return null;
     }
@@ -1184,6 +1201,14 @@
     }
   }
 
+  private static class PropertyFallbackValue {
+    public final String token;
+
+    public PropertyFallbackValue(String token) {
+      this.token = token;
+    }
+  }
+  
   /**
    * Converts a comma-separated string into an array of property value tokens.
    */
@@ -1236,6 +1261,24 @@
   }
 
   /**
+   * Converts a string into a property fallback value, validating it in the process.
+   */
+  private final class PropertyFallbackValueAttrCvt extends AttributeConverter {
+    @Override
+    public Object convertToArg(Schema schema, int line, String elem,
+        String attr, String value) throws UnableToCompleteException {
+      String token = value.trim();
+      if (Util.isValidJavaIdent(token)) {
+        return new PropertyFallbackValue(token);
+      } else {
+        Messages.PROPERTY_VALUE_INVALID.log(logger, token, null);
+        throw new UnableToCompleteException();
+      }
+    }
+  }
+
+  
+  /**
    * Converts a comma-separated string into an array of property value tokens.
    */
   private final class PropertyValueGlobArrayAttrCvt extends AttributeConverter {
@@ -1351,6 +1394,7 @@
   private final PropertyNameAttrCvt propNameAttrCvt = new PropertyNameAttrCvt();
   private final PropertyValueArrayAttrCvt propValueArrayAttrCvt = new PropertyValueArrayAttrCvt();
   private final PropertyValueAttrCvt propValueAttrCvt = new PropertyValueAttrCvt();
+  private final PropertyFallbackValueAttrCvt propFallbackValueAttrCvt = new PropertyFallbackValueAttrCvt();
   private final PropertyValueGlobArrayAttrCvt propValueGlobArrayAttrCvt = new PropertyValueGlobArrayAttrCvt();
   private final PropertyValueGlobAttrCvt propValueGlobAttrCvt = new PropertyValueGlobAttrCvt();
 
@@ -1371,6 +1415,7 @@
     registerAttributeConverter(ConfigurationProperty.class,
         configurationPropAttrCvt);
     registerAttributeConverter(PropertyValue.class, propValueAttrCvt);
+    registerAttributeConverter(PropertyFallbackValue.class, propFallbackValueAttrCvt);
     registerAttributeConverter(PropertyValue[].class, propValueArrayAttrCvt);
     registerAttributeConverter(PropertyValueGlob.class, propValueGlobAttrCvt);
     registerAttributeConverter(PropertyValueGlob[].class,
diff --git a/dev/core/src/com/google/gwt/dev/cfg/Rule.java b/dev/core/src/com/google/gwt/dev/cfg/Rule.java
index a24e1ca..7ebe9f0 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/Rule.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/Rule.java
@@ -24,19 +24,35 @@
  * Abstract base class for various kinds of deferred binding rules.
  */
 public abstract class Rule {
-
+  
+  private int fallbackEvalCost = Integer.MAX_VALUE;
   private final ConditionAll rootCondition = new ConditionAll();
 
+  /**
+   * Returns the cost of evaluation fallback binding values.
+   * when isApplicable() is true, this value is meaningless
+   * when isApplicable() is false, [1,Integer.MAX_VALUE-1] means 
+   * a relative scale of cost, where cost c is better than cost c+k
+   * Integer.MAX_VALUE implies no match.
+   * @return cost of evaluating fallback values.
+   */
+  public int getFallbackEvaluationCost() {
+    return fallbackEvalCost;
+  }
+  
   public ConditionAll getRootCondition() {
     return rootCondition;
   }
-
+  
   public boolean isApplicable(TreeLogger logger,
       StandardGeneratorContext context, String typeName)
       throws UnableToCompleteException {
-    return rootCondition.isTrue(logger, new DeferredBindingQuery(
+    DeferredBindingQuery query = new DeferredBindingQuery(
         context.getPropertyOracle(), context.getActiveLinkerNames(),
-        context.getTypeOracle(), typeName));
+        context.getTypeOracle(), typeName);
+    boolean result = rootCondition.isTrue(logger, query);
+    fallbackEvalCost = query.getFallbackEvaluationCost();
+    return result;
   }
 
   public abstract RebindResult realize(TreeLogger logger,
diff --git a/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java b/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
index b7d2e7b..47906e4 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/StaticPropertyOracle.java
@@ -22,6 +22,9 @@
 import com.google.gwt.core.ext.TreeLogger;
 
 import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.TreeSet;
 
 /**
@@ -152,7 +155,7 @@
           possibleValues.add(v);
         }
         return new DefaultSelectionProperty(value, prop.getFallback(), name,
-            possibleValues);
+            possibleValues, (Map<String, List<Set<String>>>) prop.getFallbackValuesMap());
       }
     }
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/ModuleSpacePropertyOracle.java b/dev/core/src/com/google/gwt/dev/shell/ModuleSpacePropertyOracle.java
index 819c468..494d549 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ModuleSpacePropertyOracle.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ModuleSpacePropertyOracle.java
@@ -135,7 +135,8 @@
       for (String v : cprop.getDefinedValues()) {
         possibleValues.add(v);
       }
-      return new DefaultSelectionProperty(value, fallback, name, possibleValues);
+      return new DefaultSelectionProperty(value, fallback, name, possibleValues, 
+          cprop.getFallbackValuesMap());
     } else {
       throw new BadPropertyValueException(propertyName);
     }
diff --git a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
index ee7e9f8..2daa472 100644
--- a/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
+++ b/dev/core/src/com/google/gwt/dev/shell/StandardRebindOracle.java
@@ -117,6 +117,8 @@
         return null;
       }
 
+      Rule minCostRuleSoFar = null;
+
       for (Iterator<Rule> iter = rules.iterator(); iter.hasNext();) {
         Rule rule = iter.next();
 
@@ -132,7 +134,7 @@
           if (!usedRules.contains(rule)) {
             usedRules.add(rule);
             Messages.TRACE_RULE_MATCHED.log(logger, null);
-
+            
             return rule;
           } else {
             // We are skipping this rule because it has already been used
@@ -141,11 +143,35 @@
           }
         } else {
           Messages.TRACE_RULE_DID_NOT_MATCH.log(logger, null);
+
+          // keep track of fallback partial matches
+          if (minCostRuleSoFar == null) {
+            minCostRuleSoFar = rule;
+          }
+          assert rule.getFallbackEvaluationCost() != 0;
+          // if we found a better match, keep that as the best candidate so far
+          if (rule.getFallbackEvaluationCost() <= minCostRuleSoFar.getFallbackEvaluationCost()) {
+            logger.log(TreeLogger.DEBUG, "Found better fallback match for " + rule);
+            minCostRuleSoFar = rule;
+          }
         }
       }
 
+      // if we reach this point, it means we did not find an exact match
+      // and we may have a partial match based on fall back values
+      assert minCostRuleSoFar != null;
+      if (minCostRuleSoFar.getFallbackEvaluationCost() < Integer.MAX_VALUE) {
+        logger.log(TreeLogger.WARN, "Could not find an exact match rule. Using 'closest' rule " + 
+            minCostRuleSoFar + " based on fall back values. You may need to implement a specific " +
+            "binding in case the fall back behavior does not replace the missing binding");
+        if (!usedRules.contains(minCostRuleSoFar)) {
+          usedRules.add(minCostRuleSoFar);
+          logger.log(TreeLogger.DEBUG, "No exact match was found, using closest match rule " + minCostRuleSoFar);
+          return minCostRuleSoFar;
+        }
+      }
+      
       // No matching rule for this type.
-      //
       return null;
     }
 
@@ -161,14 +187,14 @@
       if (!genCtx.isGeneratorResultCachingEnabled()) {
         return resultTypeName;
       }
-      
+
       RebindStatus status = newResult.getResultStatus();
       switch (status) {
         
         case USE_EXISTING:
           // in this case, no newly generated or cached types are needed
           break; 
-          
+
         case USE_ALL_NEW_WITH_NO_CACHING:
           /*
            * in this case, new artifacts have been generated, but no need to
@@ -176,7 +202,7 @@
            * advantage of caching).
            */
           break;
-          
+
         case USE_ALL_NEW:
           // use all new results, add a new cache entry
           cachedResult = new CachedRebindResult(newResult.getReturnedTypeName(),
@@ -184,18 +210,18 @@
               System.currentTimeMillis(), newResult.getClientDataMap());
           rebindCachePut(rule, typeName, cachedResult);
           break;
-          
+
         case USE_ALL_CACHED:
           // use all cached results
           assert (cachedResult != null);
-          
+
           genCtx.commitArtifactsFromCache(logger);
           genCtx.addGeneratedUnitsFromCache();
-          
+
           // use cached type name
           resultTypeName = cachedResult.getReturnedTypeName();
           break;
-          
+
         case USE_PARTIAL_CACHED:
           /*
            * Add cached generated units marked for reuse to the context.  
@@ -203,7 +229,7 @@
            * as GeneratedUnits.
            */
           genCtx.addGeneratedUnitsMarkedForReuseFromCache();
-          
+
           /*
            * Create a new cache entry using the composite set of new and 
            * reused cached results currently in genCtx.
@@ -267,14 +293,14 @@
   public void setRebindCache(RebindCache cache) {
     this.rebindCache = cache;
   }
-  
+
   private CachedRebindResult rebindCacheGet(Rule rule, String typeName) {
     if (rebindCache != null) {
       return rebindCache.get(rule, typeName);
     }
     return null;
   }
-  
+
   private void rebindCachePut(Rule rule, String typeName, CachedRebindResult result) {
     if (rebindCache != null) {
       rebindCache.put(rule, typeName, result);
diff --git a/dev/core/src/com/google/gwt/dev/util/xml/HandlerArgs.java b/dev/core/src/com/google/gwt/dev/util/xml/HandlerArgs.java
index 9c564b3..b58614a 100644
--- a/dev/core/src/com/google/gwt/dev/util/xml/HandlerArgs.java
+++ b/dev/core/src/com/google/gwt/dev/util/xml/HandlerArgs.java
@@ -63,6 +63,8 @@
       AttributeConverter converter = schema.getAttributeConverter(handlerParams[i].getParamType());
       return converter.convertToArg(schema, lineNumber, elemName, attrNames[i],
           value);
+    } else if (handlerParams[i].isOptional()) {
+        return null;
     } else {
       return new NullPointerException("Argument " + i + " was null");
     }
@@ -77,7 +79,7 @@
   }
 
   public boolean isArgSet(int i) {
-    if (argValues[i] != null) {
+    if (argValues[i] != null || handlerParams[i].isOptional()) {
       return true;
     } else {
       return false;
diff --git a/dev/core/src/com/google/gwt/dev/util/xml/HandlerParam.java b/dev/core/src/com/google/gwt/dev/util/xml/HandlerParam.java
index 6d4f539..a89418e 100644
--- a/dev/core/src/com/google/gwt/dev/util/xml/HandlerParam.java
+++ b/dev/core/src/com/google/gwt/dev/util/xml/HandlerParam.java
@@ -81,11 +81,17 @@
   private final Class<?> paramType;
 
   private final Field metaField;
+  
+  private final boolean isOptional;
 
   private final String normalizedAttrName;
 
   private HandlerParam(Class<?> paramType, Field metaField,
       String normalizedAttrName) {
+    this.isOptional = normalizedAttrName.endsWith("$");
+    if (isOptional) {
+      normalizedAttrName = normalizedAttrName.substring(0, normalizedAttrName.length() - 1);
+    }
     this.paramType = paramType;
     this.metaField = metaField;
     this.normalizedAttrName = normalizedAttrName;
@@ -116,4 +122,8 @@
   public Class<?> getParamType() {
     return paramType;
   }
+  
+  public boolean isOptional() {
+    return isOptional;
+  }
 }
diff --git a/dev/core/test/com/google/gwt/dev/js/JsCoerceIntShiftTest.java b/dev/core/test/com/google/gwt/dev/js/JsCoerceIntShiftTest.java
index 1ae331d..3a30878 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsCoerceIntShiftTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsCoerceIntShiftTest.java
@@ -52,7 +52,8 @@
       } else {
         SortedSet<String> valueSet = new TreeSet<String>();
         valueSet.add(value);
-        return new DefaultSelectionProperty(value, value, value, valueSet);
+        return new DefaultSelectionProperty(value, value, value, valueSet, 
+            null /* fallbackValueMap */);
       }
     }
 
diff --git a/user/src/com/google/gwt/user/UserAgent.gwt.xml b/user/src/com/google/gwt/user/UserAgent.gwt.xml
index f60f650..261e194 100644
--- a/user/src/com/google/gwt/user/UserAgent.gwt.xml
+++ b/user/src/com/google/gwt/user/UserAgent.gwt.xml
@@ -19,8 +19,11 @@
 <module>
 
   <!-- Browser-sensitive code should use the 'user.agent' property -->
-  <define-property name="user.agent" values="ie6,ie8,gecko1_8,safari,opera"/>
-
+  <define-property name="user.agent" values="ie6" />
+  <extend-property name="user.agent" values="ie8" />
+  <extend-property name="user.agent" values="gecko1_8" />
+  <extend-property name="user.agent" values="safari" />
+  <extend-property name="user.agent" values="opera" />
   <property-provider name="user.agent" generator="com.google.gwt.user.rebind.UserAgentPropertyGenerator"/>
 
   <!-- Set to false to avoid runtime warnings for mismatched runtime and -->
diff --git a/user/src/com/google/gwt/user/rebind/UserAgentGenerator.java b/user/src/com/google/gwt/user/rebind/UserAgentGenerator.java
index f0f3e8b..5f164d4 100644
--- a/user/src/com/google/gwt/user/rebind/UserAgentGenerator.java
+++ b/user/src/com/google/gwt/user/rebind/UserAgentGenerator.java
@@ -80,10 +80,11 @@
     }
 
     String userAgentValue;
+    SelectionProperty selectionProperty;
     try {
-      SelectionProperty property = propertyOracle.getSelectionProperty(logger,
+      selectionProperty = propertyOracle.getSelectionProperty(logger,
           PROPERTY_USER_AGENT);
-      userAgentValue = property.getCurrentValue();
+      userAgentValue = selectionProperty.getCurrentValue();
     } catch (BadPropertyValueException e) {
       logger.log(TreeLogger.ERROR, "Unable to find value for '"
           + PROPERTY_USER_AGENT + "'", e);
@@ -113,7 +114,8 @@
       sw.println();
       sw.println("public native String getRuntimeValue() /*-{");
       sw.indent();
-      UserAgentPropertyGenerator.writeUserAgentPropertyJavaScript(sw);
+      UserAgentPropertyGenerator.writeUserAgentPropertyJavaScript(sw, 
+          selectionProperty.getPossibleValues());
       sw.outdent();
       sw.println("}-*/;");
       sw.println();
diff --git a/user/src/com/google/gwt/user/rebind/UserAgentPropertyGenerator.java b/user/src/com/google/gwt/user/rebind/UserAgentPropertyGenerator.java
index 03c8df0..8c70403 100644
--- a/user/src/com/google/gwt/user/rebind/UserAgentPropertyGenerator.java
+++ b/user/src/com/google/gwt/user/rebind/UserAgentPropertyGenerator.java
@@ -40,6 +40,51 @@
       "ie6", "ie8", "gecko1_8", "safari", "opera"});
 
   /**
+   * List of predicates to identify user agent.
+   * The order of evaluation is from top to bottom, i.e., the first matching
+   * predicate will have the associated ua token returned.
+   * ua is defined in an outer scope and is therefore visible in
+   * the predicate javascript fragment.
+   */
+  private static UserAgentPropertyGeneratorPredicate[] predicates =
+    new UserAgentPropertyGeneratorPredicate[] {
+
+      // opera
+      new UserAgentPropertyGeneratorPredicate("opera")
+      .getPredicateBlock()
+        .println("return (ua.indexOf('opera') != -1);")
+      .returns("'opera'"),
+
+      // webkit family
+      new UserAgentPropertyGeneratorPredicate("safari")
+      .getPredicateBlock()
+        .println("return (ua.indexOf('webkit') != -1);")
+      .returns("'safari'"),
+
+      // IE8
+      new UserAgentPropertyGeneratorPredicate("ie8")
+      .getPredicateBlock()
+        .println("return (ua.indexOf('msie') != -1 && ($doc.documentMode >= 8));")
+      .returns("'ie8'"),
+
+      // IE6
+      new UserAgentPropertyGeneratorPredicate("ie6")
+      .getPredicateBlock()
+        .println("var result = /msie ([0-9]+)\\.([0-9]+)/.exec(ua);")
+        .println("if (result && result.length == 3)")
+        .indent()
+          .println("return (makeVersion(result) >= 6000);")
+        .outdent()
+      .returns("'ie6'"),
+
+      // gecko family
+      new UserAgentPropertyGeneratorPredicate("gecko1_8")
+      .getPredicateBlock()
+        .println("return (ua.indexOf('gecko') != -1);")
+      .returns("'gecko1_8'"),
+  };
+
+  /**
    * Writes out the JavaScript function body for determining the value of the
    * <code>user.agent</code> selection property. This method is used to create
    * the selection script and by {@link UserAgentGenerator} to assert at runtime
@@ -47,48 +92,29 @@
    * <code>user.agent</code> values listed here should be kept in sync with
    * {@link #VALID_VALUES} and <code>UserAgent.gwt.xml</code>.
    */
-  static void writeUserAgentPropertyJavaScript(SourceWriter body) {
+  static void writeUserAgentPropertyJavaScript(SourceWriter body, 
+      SortedSet<String> possibleValues) {
+
+    // write preamble
     body.println("var ua = navigator.userAgent.toLowerCase();");
     body.println("var makeVersion = function(result) {");
     body.indent();
     body.println("return (parseInt(result[1]) * 1000) + parseInt(result[2]);");
     body.outdent();
     body.println("};");
-    body.println("if (ua.indexOf('opera') != -1) {");
-    body.indent();
-    body.println("return 'opera';");
-    body.outdent();
-    body.println("} else if (ua.indexOf('webkit') != -1) {");
-    body.indent();
-    body.println("return 'safari';");
-    body.outdent();
-    body.println("} else if (ua.indexOf('msie') != -1) {");
-    body.indent();
-    body.println("if ($doc.documentMode >= 8) {");
-    body.indent();
-    body.println("return 'ie8';");
-    body.outdent();
-    body.println("} else {");
-    body.indent();
-    body.println("var result = /msie ([0-9]+)\\.([0-9]+)/.exec(ua);");
-    body.println("if (result && result.length == 3) {");
-    body.indent();
-    body.println("var v = makeVersion(result);");
-    body.println("if (v >= 6000) {");
-    body.indent();
-    body.println("return 'ie6';");
-    body.outdent();
-    body.println("}");
-    body.outdent();
-    body.println("}");
-    body.outdent();
-    body.println("}");
-    body.outdent();
-    body.println("} else if (ua.indexOf('gecko') != -1) {");
-    body.indent();
-    body.println("return 'gecko1_8';");
-    body.outdent();
-    body.println("}");
+
+    // write only selected user agents 
+    for (int i = 0; i < predicates.length; i++) {
+      if (possibleValues.contains(predicates[i].getUserAgent())) {
+        body.println("if ((function() { ");
+        body.indent();
+        body.print(predicates[i].toString());
+        body.outdent();
+        body.println("})()) return " + predicates[i].getReturnValue() + ";");
+      }
+    }
+    
+    // default return
     body.println("return 'unknown';");
   }
 
@@ -105,14 +131,17 @@
             + "\" value=\"false\"/> to suppress this warning message.");
       }
     }
-
+    // make sure that the # of ua in VALID_VALUES
+    // is the same of predicates. maybe should iterate
+    // to make sure each one has a match.
+    assert predicates.length == VALID_VALUES.size();
     StringSourceWriter body = new StringSourceWriter();
     body.println("{");
     body.indent();
-    writeUserAgentPropertyJavaScript(body);
+    writeUserAgentPropertyJavaScript(body, possibleValues);
     body.outdent();
     body.println("}");
 
     return body.toString();
   }
-}
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/rebind/UserAgentPropertyGeneratorPredicate.java b/user/src/com/google/gwt/user/rebind/UserAgentPropertyGeneratorPredicate.java
new file mode 100644
index 0000000..66cb73d
--- /dev/null
+++ b/user/src/com/google/gwt/user/rebind/UserAgentPropertyGeneratorPredicate.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2011 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.user.rebind;
+
+/**
+ * Represents a predicate expressed in Javascript
+ * returns the given user agent if predicate evaluates
+ * to true.
+ */
+public class UserAgentPropertyGeneratorPredicate {
+  
+  private final StringSourceWriter predicateWriter = new StringSourceWriter();
+  private String userAgent;
+  private String returnValue;
+  
+  public UserAgentPropertyGeneratorPredicate(String userAgent) {
+    assert userAgent != null;
+    assert userAgent.length() > 0;
+    this.userAgent = userAgent;
+  }
+
+  public UserAgentPropertyGeneratorPredicate getPredicateBlock() {
+    return this;
+  }
+
+  public String getReturnValue() {
+    return returnValue;
+  }
+  
+  public String getUserAgent() {
+    return userAgent;
+  }
+  
+  public UserAgentPropertyGeneratorPredicate indent() {
+    predicateWriter.indent();
+    return this;
+  }
+
+  public UserAgentPropertyGeneratorPredicate outdent() {
+    predicateWriter.outdent();
+    return this;
+  }
+  
+  public UserAgentPropertyGeneratorPredicate println(String s) {
+    predicateWriter.println(s);
+    return this;
+  }
+
+  public UserAgentPropertyGeneratorPredicate returns(String s) {
+    returnValue = s;
+    return this;
+  }
+  
+  @Override
+  public String toString() {
+    return predicateWriter.toString();
+  }
+}