Add property provider generators.

Patch by: jat
Review by: unnurg

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9281 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/PropertyProviderGenerator.java b/dev/core/src/com/google/gwt/core/ext/linker/PropertyProviderGenerator.java
new file mode 100644
index 0000000..fd8f1d3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/linker/PropertyProviderGenerator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2010 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.core.ext.linker;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.util.SortedSet;
+
+/**
+ * An interface for generating a property provider JS implementation, rather
+ * than having it defined in a module file.
+ * 
+ * Use it like this:
+ * <pre>
+ *   &lt;property-provider name="foo" generator="org.example.FooGenerator"/&gt;
+ * </pre>
+ * A default implementation in JS can be included inside the property-provider
+ * tag as usual, and will be used if the generator returns {@code null}.
+ */
+public interface PropertyProviderGenerator {
+
+  /**
+   * Generate a property provider.
+   *
+   * @param logger TreeLogger
+   * @param possibleValues the possible values of this property
+   * @param fallback the fallback value for this property, or null
+   * @param configProperties the configuration properties for this module
+   * @return the JS source of the property provider (the complete body of a JS
+   *     function taking no arguments, including open/close braces), or null to
+   *     use the default implementation in the property-provider tag
+   * @throws UnableToCompleteException after logging the message if processing
+   *     is unable to continue
+   */
+  String generate(TreeLogger logger, SortedSet<String> possibleValues,
+      String fallback, SortedSet<ConfigurationProperty> configProperties)
+      throws UnableToCompleteException;
+}
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/SelectionProperty.java b/dev/core/src/com/google/gwt/core/ext/linker/SelectionProperty.java
index 8940edd..6f27aba 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/SelectionProperty.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/SelectionProperty.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.core.ext.linker;
 
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
 import java.util.SortedSet;
 
 /**
@@ -27,6 +30,12 @@
  *      used in generators.
  */
 public interface SelectionProperty {
+
+  /**
+   * Returns the fallback value or an empty string if not defined.
+   */
+  String getFallbackValue();
+
   /**
    * Returns the name of the deferred binding property.
    */
@@ -40,8 +49,15 @@
   /**
    * Returns a raw function body that provides the runtime value to be used for
    * a deferred binding property.
+   * 
+   * @param logger logger to use for any warnings/errors
+   * @param configProperties set of configuration properties
+   * @throws UnableToCompleteException if execution cannot continue, after
+   *     having logged a message
    */
-  String getPropertyProvider();
+  String getPropertyProvider(TreeLogger logger,
+      SortedSet<ConfigurationProperty> configProperties)
+      throws UnableToCompleteException;
 
   /**
    * Returns <code>true</code> if the value of the SelectionProperty is always
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/PermutationsUtil.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/PermutationsUtil.java
index f69cc48..c0367fa 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/PermutationsUtil.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/PermutationsUtil.java
@@ -18,6 +18,7 @@
 
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.dev.util.StringKey;
@@ -72,9 +73,16 @@
   /**
    * Uses the internal map to insert JS to select a permutation into the
    * selection script.
+   * 
+   * @param selectionScript
+   * @param logger
+   * @param context
+   * @return the modified selectionScript buffer
+   * @throws UnableToCompleteException 
    */
   public StringBuffer addPermutationsJs(StringBuffer selectionScript,
-      TreeLogger logger, LinkerContext context) {
+      TreeLogger logger, LinkerContext context)
+      throws UnableToCompleteException {
     int startPos;
     
     PropertiesUtil.addPropertiesJs(selectionScript, logger, context);
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/PropertiesUtil.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/PropertiesUtil.java
index 3ee5355..e645566 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/PropertiesUtil.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/PropertiesUtil.java
@@ -18,21 +18,27 @@
 
 import com.google.gwt.core.ext.LinkerContext;
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
 import com.google.gwt.core.ext.linker.SelectionProperty;
 
+import java.util.SortedSet;
+
 /**
  * A utility class to fill in the properties javascript in linker templates.
  */
 public class PropertiesUtil {
   public static StringBuffer addPropertiesJs(StringBuffer selectionScript,
-      TreeLogger logger, LinkerContext context) {
+      TreeLogger logger, LinkerContext context)
+      throws UnableToCompleteException {
     int startPos;
 
     // Add property providers
     startPos = selectionScript.indexOf("// __PROPERTIES_END__");
     if (startPos != -1) {
       for (SelectionProperty p : context.getProperties()) {
-        String text = generatePropertyProvider(p);
+        String text = generatePropertyProvider(logger, p,
+            context.getConfigurationProperties());
         selectionScript.insert(startPos, text);
         startPos += text.length();
       }
@@ -40,12 +46,14 @@
     return selectionScript;
   }
   
-  private static String generatePropertyProvider(SelectionProperty prop) {
+  private static String generatePropertyProvider(TreeLogger logger,
+      SelectionProperty prop, SortedSet<ConfigurationProperty> configProps)
+      throws UnableToCompleteException {
     StringBuffer toReturn = new StringBuffer();
 
     if (prop.tryGetValue() == null && !prop.isDerived()) {
       toReturn.append("providers['" + prop.getName() + "'] = function()");
-      toReturn.append(prop.getPropertyProvider());
+      toReturn.append(prop.getPropertyProvider(logger, configProps));
       toReturn.append(";");
 
       toReturn.append("values['" + prop.getName() + "'] = {");
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSelectionProperty.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSelectionProperty.java
index 92b8670..f50ae60 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSelectionProperty.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardSelectionProperty.java
@@ -15,6 +15,10 @@
  */
 package com.google.gwt.core.ext.linker.impl;
 
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.PropertyProviderGenerator;
 import com.google.gwt.core.ext.linker.SelectionProperty;
 import com.google.gwt.dev.cfg.BindingProperty;
 
@@ -31,22 +35,29 @@
   private static final String FALLBACK_TOKEN = "/*-FALLBACK-*/";
 
   private final String activeValue;
+  private final String fallback;
   private final boolean isDerived;
   private final String name;
   private final String provider;
+  private final Class<? extends PropertyProviderGenerator> providerGenerator;
   private final SortedSet<String> values;
 
   public StandardSelectionProperty(BindingProperty p) {
     activeValue = p.getConstrainedValue();
     isDerived = p.isDerived();
     name = p.getName();
-    String fallback = p.getFallback();
+    fallback = p.getFallback();
+    providerGenerator = p.getProviderGenerator();
     provider = p.getProvider() == null ? null
         : p.getProvider().getBody().replace(FALLBACK_TOKEN, fallback);
     values = Collections.unmodifiableSortedSet(new TreeSet<String>(
         Arrays.asList(p.getDefinedValues())));
   }
 
+  public String getFallbackValue() {
+    return fallback;
+  }
+
   public String getName() {
     return name;
   }
@@ -55,8 +66,27 @@
     return values;
   }
 
-  public String getPropertyProvider() {
-    return provider;
+  public String getPropertyProvider(TreeLogger logger,
+      SortedSet<ConfigurationProperty> configProperties)
+      throws UnableToCompleteException {
+    String generatorResult = null;
+    if (providerGenerator != null) {
+      Throwable caught = null;
+      try {
+        PropertyProviderGenerator gen = providerGenerator.newInstance();
+        generatorResult = gen.generate(logger, values, fallback,
+            configProperties);
+      } catch (InstantiationException e) {
+        caught = e;
+      } catch (IllegalAccessException e) {
+        caught = e;
+      }
+      if (caught != null) {
+        logger.log(TreeLogger.WARN, "Failed to execute property provider "
+            + "generator '" + providerGenerator + "'", caught);
+      }
+    }
+    return generatorResult != null ? generatorResult : provider;
   }
 
   public boolean isDerived() {
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 5bea795..69e689c 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/BindingProperty.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.cfg;
 
+import com.google.gwt.core.ext.linker.PropertyProviderGenerator;
 import com.google.gwt.dev.util.collect.IdentityHashSet;
 import com.google.gwt.dev.util.collect.Lists;
 import com.google.gwt.dev.util.collect.Sets;
@@ -48,6 +49,7 @@
   private final Map<Condition, SortedSet<String>> conditionalValues = new LinkedHashMap<Condition, SortedSet<String>>();
   private final SortedSet<String> definedValues = new TreeSet<String>();
   private PropertyProvider provider;
+  private Class<? extends PropertyProviderGenerator> providerGenerator;
   private String fallback;
   private final ConditionAll rootCondition = new ConditionAll();
 
@@ -134,6 +136,11 @@
     return definedValues.toArray(new String[definedValues.size()]);
   }
 
+  /**
+   * Returns the fallback value for this property, or the empty string if none.
+   * 
+   * @return the fallback value
+   */
   public String getFallback() {
     return fallback;
   }
@@ -142,6 +149,13 @@
     return provider;
   }
 
+  /**
+   * @return the the provider generator class, or null if none.
+   */
+  public Class<? extends PropertyProviderGenerator> getProviderGenerator() {
+    return providerGenerator;
+  }
+
   public Set<String> getRequiredProperties() {
     Set<String> toReturn = Sets.create();
     for (Condition cond : conditionalValues.keySet()) {
@@ -168,8 +182,7 @@
 
   /**
    * Returns <code>true</code> if the value was previously provided to
-   * {@link #addDefinedValue(String)} since the last time {@link #clearValues()}
-   * was called.
+   * {@link #addDefinedValue(Condition,String)}.
    */
   public boolean isDefinedValue(String value) {
     return definedValues.contains(value);
@@ -194,7 +207,7 @@
    * the currently-defined values.
    * 
    * @throws IllegalArgumentException if any of the provided values were not
-   *           provided to {@link #addDefinedValue(String)}.
+   *     provided to {@link #addDefinedValue(Condition,String)}.
    */
   public void setAllowedValues(Condition condition, String... values) {
     SortedSet<String> temp = new TreeSet<String>(Arrays.asList(values));
@@ -228,6 +241,15 @@
   }
 
   /**
+   * Set a provider generator for this property.
+   * 
+   * @param generator
+   */
+  public void setProviderGenerator(Class<? extends PropertyProviderGenerator> generator) {
+    providerGenerator = generator;
+  }
+
+  /**
    * Create a minimal number of equivalence sets, expanding any glob patterns.
    */
   void normalizeCollapsedValues() {
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 fa76101..d5653e7 100644
--- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
+++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDefSchema.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.LinkerOrder;
+import com.google.gwt.core.ext.linker.PropertyProviderGenerator;
 import com.google.gwt.dev.js.JsParser;
 import com.google.gwt.dev.js.JsParserException;
 import com.google.gwt.dev.js.ast.JsExprStmt;
@@ -49,100 +50,151 @@
 
   private final class BodySchema extends Schema {
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __add_linker_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __clear_configuration_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __collapse_all_properties_1_value = "true";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __collapse_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __collapse_property_2_values = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_configuration_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_configuration_property_2_is_multi_valued = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_linker_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_linker_2_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __define_property_2_values = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __entry_point_1_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __extend_configuration_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __extend_configuration_property_2_value = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __extend_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __extend_property_2_values = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __generate_with_1_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __inherits_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __property_provider_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
+    protected final String __property_provider_2_generator = "";
+
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_1_path = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_2_includes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_3_excludes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_4_skips = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_5_defaultexcludes = "yes";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __public_6_casesensitive = "true";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __replace_with_1_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __script_1_src = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __servlet_1_path = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __servlet_2_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_configuration_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_configuration_property_2_value = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_property_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_property_2_value = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_property_fallback_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __set_property_fallback_2_value = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_1_path = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_2_includes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_3_excludes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_4_skips = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_5_defaultexcludes = "yes";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __source_6_casesensitive = "true";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __stylesheet_1_src = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_1_path = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_2_includes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_3_excludes = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_4_skips = "";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_5_defaultexcludes = "yes";
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __super_source_6_casesensitive = "true";
 
     /**
@@ -153,6 +205,7 @@
 
     private Schema fChild;
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __add_linker_begin(LinkerName name)
         throws UnableToCompleteException {
       if (moduleDef.getLinker(name.name) == null) {
@@ -163,6 +216,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __clear_configuration_property_begin(PropertyName name)
         throws UnableToCompleteException {
       // Don't allow configuration properties with the same name as a
@@ -189,11 +243,13 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __collapse_all_properties_begin(boolean collapse) {
       moduleDef.setCollapseAllProperties(collapse);
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __collapse_property_begin(PropertyName name,
         PropertyValueGlob[] values) throws UnableToCompleteException {
       Property prop = moduleDef.getProperties().find(name.token);
@@ -235,6 +291,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __define_configuration_property_begin(PropertyName name,
         String is_multi_valued) throws UnableToCompleteException {
       boolean isMultiValued = toPrimitiveBoolean(is_multi_valued);
@@ -303,6 +360,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __define_linker_begin(LinkerName name,
         Class<? extends Linker> linker) throws UnableToCompleteException {
       if (!Linker.class.isAssignableFrom(linker)) {
@@ -319,6 +377,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __define_property_begin(PropertyName name,
         PropertyValue[] values) throws UnableToCompleteException {
 
@@ -351,11 +410,13 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __entry_point_begin(String className) {
       moduleDef.addEntryPointTypeName(className);
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __extend_configuration_property_begin(PropertyName name,
         String value) throws UnableToCompleteException {
 
@@ -378,6 +439,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __extend_property_begin(BindingProperty property,
         PropertyValue[] values) {
       for (int i = 0; i < values.length; i++) {
@@ -388,12 +450,14 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __fail_begin() {
       RuleFail rule = new RuleFail();
       moduleDef.getRules().prepend(rule);
       return new FullConditionSchema(rule.getRootCondition());
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __generate_with_begin(Class<? extends Generator> generator)
         throws UnableToCompleteException {
       if (!Generator.class.isAssignableFrom(generator)) {
@@ -406,6 +470,7 @@
       return new FullConditionSchema(rule.getRootCondition());
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __inherits_begin(String name)
         throws UnableToCompleteException {
       TreeLogger branch = logger.branch(TreeLogger.TRACE,
@@ -414,36 +479,49 @@
       return null;
     }
 
-    @SuppressWarnings("unused")
-    protected Schema __property_provider_begin(BindingProperty property) {
-      return fChild = new ScriptSchema();
+    @SuppressWarnings("unused") // called reflectively
+    protected Schema __property_provider_begin(BindingProperty property,
+        Class<? extends PropertyProviderGenerator> generator) {
+      fChild = new ScriptSchema();
+      return fChild;
     }
 
-    protected void __property_provider_end(BindingProperty property)
+    @SuppressWarnings("unused") // called reflectively
+    protected void __property_provider_end(BindingProperty property,
+        Class<? extends PropertyProviderGenerator> generator)
         throws UnableToCompleteException {
-      ScriptSchema childSchema = ((ScriptSchema) fChild);
-      String script = childSchema.getScript();
-      if (script == null) {
-        // This is a problem.
-        //
-        logger.log(TreeLogger.ERROR,
-            "Property providers must specify a JavaScript body", null);
+      if (generator != null
+          && !PropertyProviderGenerator.class.isAssignableFrom(generator)) {
+        logger.log(TreeLogger.ERROR, "A property provider generator must "
+            + "extend " + PropertyProviderGenerator.class.getName(), null);
         throw new UnableToCompleteException();
       }
-
-      int lineNumber = childSchema.getStartLineNumber();
-      JsFunction fn = parseJsBlock(lineNumber, script);
-
-      property.setProvider(new PropertyProvider(fn.getBody().toSource()));
+      ScriptSchema childSchema = ((ScriptSchema) fChild);
+      String script = childSchema.getScript();
+      property.setProviderGenerator(generator);
+      if (script == null) {
+        if (generator == null) {
+          // This is a problem.
+          //
+          logger.log(TreeLogger.ERROR, "Property providers must specify a "
+              + "JavaScript body or a provider generator", null);
+          throw new UnableToCompleteException();
+        }
+      } else {
+        int lineNumber = childSchema.getStartLineNumber();
+        JsFunction fn = parseJsBlock(lineNumber, script);
+        property.setProvider(new PropertyProvider(fn.getBody().toSource()));
+      }
     }
 
-    @SuppressWarnings("unused")
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __public_begin(String path, String includes,
         String excludes, String skips, String defaultExcludes,
         String caseSensitive) {
       return fChild = new IncludeExcludeSchema();
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected void __public_end(String path, String includes, String excludes,
         String skips, String defaultExcludes, String caseSensitive) {
       IncludeExcludeSchema childSchema = ((IncludeExcludeSchema) fChild);
@@ -468,6 +546,7 @@
           skipList, doDefaultExcludes, doCaseSensitive);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __replace_with_begin(String className) {
       RuleReplaceWith rule = new RuleReplaceWith(className);
       moduleDef.getRules().prepend(rule);
@@ -478,11 +557,12 @@
      * @param src a partial or full url to a script file to inject
      * @return <code>null</code> since there can be no children
      */
-    @SuppressWarnings("unused")
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __script_begin(String src) {
       return fChild = new ScriptSchema();
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected void __script_end(String src) {
       ScriptSchema childSchema = (ScriptSchema) fChild;
       String js = childSchema.getScript();
@@ -495,6 +575,7 @@
       moduleDef.getScripts().append(new Script(src));
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __servlet_begin(String path, String servletClass)
         throws UnableToCompleteException {
 
@@ -511,6 +592,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __set_configuration_property_begin(PropertyName name,
         String value) throws UnableToCompleteException {
 
@@ -547,13 +629,14 @@
       return null;
     }
 
-    @SuppressWarnings("unused")
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __set_property_begin(BindingProperty prop,
         PropertyValue[] value) throws UnableToCompleteException {
       bindingPropertyCondition = new ConditionAll();
       return new PropertyConditionSchema(bindingPropertyCondition);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected void __set_property_end(BindingProperty prop,
         PropertyValue[] value) throws UnableToCompleteException {
       boolean error = false;
@@ -577,6 +660,7 @@
       prop.setAllowedValues(bindingPropertyCondition, stringValues);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __set_property_fallback_begin(BindingProperty prop,
         PropertyValue value) throws UnableToCompleteException {
       boolean error = true;
@@ -600,13 +684,14 @@
      * Indicates which subdirectories contain translatable source without
      * necessarily adding a sourcepath entry.
      */
-    @SuppressWarnings("unused")
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __source_begin(String path, String includes,
         String excludes, String skips, String defaultExcludes,
         String caseSensitive) {
       return fChild = new IncludeExcludeSchema();
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected void __source_end(String path, String includes, String excludes,
         String skips, String defaultExcludes, String caseSensitive) {
       addSourcePackage(path, includes, excludes, skips, defaultExcludes,
@@ -617,6 +702,7 @@
      * @param src a partial or full url to a stylesheet file to inject
      * @return <code>null</code> since there can be no children
      */
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __stylesheet_begin(String src) {
       moduleDef.getStyles().append(src);
       return null;
@@ -626,13 +712,14 @@
      * Like adding a translatable source package, but such that it uses the
      * module's package itself as its sourcepath root entry.
      */
-    @SuppressWarnings("unused")
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __super_source_begin(String path, String includes,
         String excludes, String skips, String defaultExcludes,
         String caseSensitive) {
       return fChild = new IncludeExcludeSchema();
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected void __super_source_end(String path, String includes,
         String excludes, String skips, String defaultExcludes,
         String caseSensitive) {
@@ -773,6 +860,10 @@
     @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
+      if (value.length() == 0) {
+        // handle optional class names
+        return null;
+      }
       try {
         ClassLoader cl = Thread.currentThread().getContextClassLoader();
         return cl.loadClass(value);
@@ -788,14 +879,17 @@
    */
   private final class FullConditionSchema extends PropertyConditionSchema {
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __when_type_assignable_1_class = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __when_type_is_1_class = null;
 
     public FullConditionSchema(CompoundCondition parentCondition) {
       super(parentCondition);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __when_type_assignable_begin(String className) {
       Condition cond = new ConditionWhenTypeAssignableTo(className);
       parentCondition.getConditions().add(cond);
@@ -804,6 +898,7 @@
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __when_type_is_begin(String className) {
       Condition cond = new ConditionWhenTypeIs(className);
       parentCondition.getConditions().add(cond);
@@ -820,10 +915,13 @@
 
   private static final class IncludeExcludeSchema extends Schema {
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __exclude_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __include_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __skip_1_name = null;
 
     /**
@@ -853,21 +951,24 @@
       return skips;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __exclude_begin(String name) {
       excludes.add(name);
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __include_begin(String name) {
       includes.add(name);
       return null;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __skip_begin(String name) {
       skips.add(name);
       return null;
     }
-}
+  }
 
   private static class LinkerName {
     public final String name;
@@ -882,6 +983,7 @@
    */
   private final class LinkerNameAttrCvt extends AttributeConverter {
 
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       // Ensure the value is a valid Java identifier
@@ -912,6 +1014,7 @@
    */
   private final class NullableNameAttrCvt extends AttributeConverter {
 
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       if (value == null || value.length() == 0) {
@@ -945,6 +1048,7 @@
       this.concreteType = concreteType;
     }
 
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       // Find the named property.
@@ -974,10 +1078,13 @@
    * A limited number of conditional predicates based only on properties.
    */
   private class PropertyConditionSchema extends Schema {
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __when_linker_added_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __when_property_is_1_name = null;
 
+    @SuppressWarnings("unused") // referenced reflectively
     protected final String __when_property_is_2_value = null;
 
     protected final CompoundCondition parentCondition;
@@ -986,24 +1093,28 @@
       this.parentCondition = parentCondition;
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __all_begin() {
       CompoundCondition cond = new ConditionAll();
       parentCondition.getConditions().add(cond);
       return subSchema(cond);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __any_begin() {
       CompoundCondition cond = new ConditionAny();
       parentCondition.getConditions().add(cond);
       return subSchema(cond);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __none_begin() {
       CompoundCondition cond = new ConditionNone();
       parentCondition.getConditions().add(cond);
       return subSchema(cond);
     }
 
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __when_linker_added_begin(LinkerName linkerName) {
       Condition cond = new ConditionWhenLinkerAdded(linkerName.name);
       parentCondition.getConditions().add(cond);
@@ -1017,6 +1128,7 @@
      * module writers. It prevents them from trying to create property providers
      * for unknown properties.
      */
+    @SuppressWarnings("unused") // called reflectively
     protected Schema __when_property_is_begin(BindingProperty prop,
         PropertyValue value) {
       Condition cond = new ConditionWhenPropertyIs(prop.getName(), value.token);
@@ -1044,6 +1156,7 @@
    */
   private final class PropertyNameAttrCvt extends AttributeConverter {
 
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       // Ensure each part of the name is valid.
@@ -1075,6 +1188,7 @@
    * Converts a comma-separated string into an array of property value tokens.
    */
   private final class PropertyValueArrayAttrCvt extends AttributeConverter {
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       String[] tokens = value.split(",");
@@ -1095,6 +1209,7 @@
    * Converts a string into a property value, validating it in the process.
    */
   private final class PropertyValueAttrCvt extends AttributeConverter {
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
 
@@ -1124,6 +1239,7 @@
    * Converts a comma-separated string into an array of property value tokens.
    */
   private final class PropertyValueGlobArrayAttrCvt extends AttributeConverter {
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
       String[] tokens = value.split(",");
@@ -1144,6 +1260,7 @@
    * Converts a string into a property value glob, validating it in the process.
    */
   private final class PropertyValueGlobAttrCvt extends AttributeConverter {
+    @Override
     public Object convertToArg(Schema schema, int line, String elem,
         String attr, String value) throws UnableToCompleteException {
 
@@ -1169,6 +1286,7 @@
     public ScriptSchema() {
     }
 
+    @SuppressWarnings("unused") // called reflectively
     public void __text(String text) {
       if (script == null) {
         script = new StringBuffer();
diff --git a/dev/core/test/com/google/gwt/core/ext/linker/impl/StandardSelectionPropertyTest.java b/dev/core/test/com/google/gwt/core/ext/linker/impl/StandardSelectionPropertyTest.java
index 19a4cb9..1c646fa 100644
--- a/dev/core/test/com/google/gwt/core/ext/linker/impl/StandardSelectionPropertyTest.java
+++ b/dev/core/test/com/google/gwt/core/ext/linker/impl/StandardSelectionPropertyTest.java
@@ -15,16 +15,37 @@
  */
 package com.google.gwt.core.ext.linker.impl;
 
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.PropertyProviderGenerator;
 import com.google.gwt.dev.cfg.BindingProperty;
 import com.google.gwt.dev.cfg.PropertyProvider;
+import com.google.gwt.dev.shell.FailErrorLogger;
 
 import junit.framework.TestCase;
 
+import java.util.SortedSet;
+import java.util.TreeSet;
+
 /**
  * Tests for {@link StandardSelectionProperty}.
  */
 public class StandardSelectionPropertyTest extends TestCase {
 
+  /**
+   * Test property provider generator.
+   */
+  public static class MyProviderGenerator
+      implements PropertyProviderGenerator {
+
+    public String generate(TreeLogger logger, SortedSet<String> possibleValues,
+        String fallback, SortedSet<ConfigurationProperty> configProperties)
+        throws UnableToCompleteException {
+      return "good " + fallback;
+    }
+  }
+
   private static final String FBV = "FBV";
 
   private static final String PROVIDER_MULTIFALLBACK =
@@ -36,30 +57,49 @@
 
   private static final String PROVIDER_NOFALLBACK = "provider text without fallback";
 
-  public void testNoFallback() {
+  private static final TreeLogger logger = new FailErrorLogger();
+
+  private static final SortedSet<ConfigurationProperty> configProperties = new TreeSet<ConfigurationProperty>();
+
+  public void testNoFallback() throws UnableToCompleteException {
     BindingProperty bp = new BindingProperty("doesNotUseFallback");
     PropertyProvider provider = new PropertyProvider(PROVIDER_NOFALLBACK);
     bp.setProvider(provider);
     StandardSelectionProperty property = new StandardSelectionProperty(bp);
-    assertEquals(PROVIDER_NOFALLBACK, property.getPropertyProvider());
+    assertEquals(PROVIDER_NOFALLBACK, property.getPropertyProvider(logger,
+        configProperties));
 
     provider = new PropertyProvider(PROVIDER_MULTIFALLBACK);
     bp.setProvider(provider);
     property = new StandardSelectionProperty(bp);
-    assertEquals(PROVIDER_MULTIFALLBACK_EMPTY, property.getPropertyProvider());
+    assertEquals(PROVIDER_MULTIFALLBACK_EMPTY, property.getPropertyProvider(
+        logger, configProperties));
   }
 
-  public void testWithFallback() {
+  public void testPropertyProviderGenerator() throws UnableToCompleteException {
+    BindingProperty bp = new BindingProperty("providerGenerator");
+    bp.setFallback(FBV);
+    PropertyProvider provider = new PropertyProvider("bad");
+    bp.setProvider(provider);
+    bp.setProviderGenerator(MyProviderGenerator.class);
+    StandardSelectionProperty property = new StandardSelectionProperty(bp);
+    assertEquals("good " + FBV, property.getPropertyProvider(logger,
+        configProperties));
+  }
+
+  public void testWithFallback() throws UnableToCompleteException {
     BindingProperty bp = new BindingProperty("doesUseFallback");
     bp.setFallback(FBV);
     PropertyProvider provider = new PropertyProvider(PROVIDER_NOFALLBACK);
     bp.setProvider(provider);
     StandardSelectionProperty property = new StandardSelectionProperty(bp);
-    assertEquals(PROVIDER_NOFALLBACK, property.getPropertyProvider());
+    assertEquals(PROVIDER_NOFALLBACK, property.getPropertyProvider(logger,
+        configProperties));
 
     provider = new PropertyProvider(PROVIDER_MULTIFALLBACK);
     bp.setProvider(provider);
     property = new StandardSelectionProperty(bp);
-    assertEquals(PROVIDER_MULTIFALLBACK_FBV, property.getPropertyProvider());
+    assertEquals(PROVIDER_MULTIFALLBACK_FBV, property.getPropertyProvider(
+        logger, configProperties));
   }
 }
diff --git a/distro-source/core/src/gwt-module.dtd b/distro-source/core/src/gwt-module.dtd
index fd59093..10c6614 100644
--- a/distro-source/core/src/gwt-module.dtd
+++ b/distro-source/core/src/gwt-module.dtd
@@ -32,28 +32,28 @@
 <!ELEMENT source (include | exclude)*>
 <!ATTLIST source
 	path CDATA #REQUIRED
-		includes CDATA #IMPLIED
-		excludes CDATA #IMPLIED
-		defaultexcludes (yes | no) "yes"
-		casesensitive (true | false) "true"
+	includes CDATA #IMPLIED
+	excludes CDATA #IMPLIED
+	defaultexcludes (yes | no) "yes"
+	casesensitive (true | false) "true"
 >
 <!-- Specify the public resource path, relative to the classpath location of the module descriptor -->
 <!ELEMENT public (include | exclude)*>
 <!ATTLIST public
 	path CDATA #REQUIRED
-		includes CDATA #IMPLIED
-		excludes CDATA #IMPLIED
-		defaultexcludes (yes | no) "yes"
-		casesensitive (true | false) "true"
+	includes CDATA #IMPLIED
+	excludes CDATA #IMPLIED
+	defaultexcludes (yes | no) "yes"
+	casesensitive (true | false) "true"
 >
 <!-- Specify a source path that rebases subpackages into the root namespace -->
 <!ELEMENT super-source (include | exclude)*>
 <!ATTLIST super-source
 	path CDATA #REQUIRED
-		includes CDATA #IMPLIED
-		excludes CDATA #IMPLIED
-		defaultexcludes (yes | no) "yes"
-		casesensitive (true | false) "true"
+	includes CDATA #IMPLIED
+	excludes CDATA #IMPLIED
+	defaultexcludes (yes | no) "yes"
+	casesensitive (true | false) "true"
 >
 <!ELEMENT include EMPTY>
 <!ATTLIST include
@@ -160,6 +160,7 @@
 <!ELEMENT property-provider (#PCDATA)>
 <!ATTLIST property-provider
 	name CDATA #REQUIRED
+	generator CDATA #IMPLIED
 >
 <!-- All possible predicates -->
 <!ENTITY % predicates "when-property-is | when-type-assignable | when-type-is | all | any | none">
diff --git a/tools/api-checker/config/gwt20_21userApi.conf b/tools/api-checker/config/gwt20_21userApi.conf
index b79ca67..216ac23 100644
--- a/tools/api-checker/config/gwt20_21userApi.conf
+++ b/tools/api-checker/config/gwt20_21userApi.conf
@@ -10,7 +10,6 @@
 :com/google/gwt/benchmarks/BenchmarkShell.java\
 :com/google/gwt/benchmarks/client/Benchmark.java\
 :com/google/gwt/core/ext/**\
-:com/google/gwt/core/linker/**\
 :com/google/gwt/dev/*.java\
 :com/google/gwt/dev/asm/**\
 :com/google/gwt/dev/cfg/**\
@@ -28,9 +27,9 @@
 :com/google/gwt/resources/css/**\
 :com/google/gwt/resources/ext/**\
 :com/google/gwt/resources/rg/**\
-:com/google/gwt/user/linker/**\
 :com/google/gwt/util/**\
 :com/google/gwt/soyc/**\
+:**/linker/**\
 :**/rebind/**\
 :**/remote/**\
 :**/server/**\
@@ -42,7 +41,6 @@
 :com/google/gwt/regexp/shared/**\
 :com/google/gwt/rpc/client/impl/ClientWriterFactory.java\
 :com/google/gwt/rpc/client/impl/EscapeUtil.java\
-:com/google/gwt/rpc/linker/*.java\
 :com/google/gwt/uibinder/attributeparsers/*.java\
 :com/google/gwt/uibinder/elementparsers/*.java\
 :com/google/gwt/uibinder/testing/*.java\
@@ -64,6 +62,7 @@
 excludedFiles_new user/src/com/google/gwt/benchmarks/BenchmarkReport.java\
 :user/src/com/google/gwt/benchmarks/BenchmarkShell.java\
 :user/src/com/google/gwt/benchmarks/client/Benchmark.java\
+:**/linker/**\
 :**/rebind/**\
 :**/server/**\
 :**/tools/**\
@@ -74,17 +73,14 @@
 :user/src/com/google/gwt/junit/client/GWTTestCase.java\
 :user/src/com/google/gwt/junit/client/impl/GWTRunner.java\
 :user/src/com/google/gwt/junit/remote\
-:user/src/com/google/gwt/precompress/linker\
 :user/src/com/google/gwt/resources/css\
 :user/src/com/google/gwt/resources/ext\
 :user/src/com/google/gwt/resources/rg\
 :user/src/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java\
 :user/src/com/google/gwt/rpc/client/impl/ClientWriterFactory.java\
 :user/src/com/google/gwt/rpc/client/impl/EscapeUtil.java\
-:user/src/com/google/gwt/rpc/linker\
 :user/src/com/google/gwt/safehtml/shared/SafeHtmlHostedModeUtils.java\
 :user/src/com/google/gwt/user/client/rpc/core/java/util/LinkedHashMap_CustomFieldSerializer.java\
-:user/src/com/google/gwt/user/linker\
 :user/src/com/google/gwt/uibinder/attributeparsers\
 :user/src/com/google/gwt/uibinder/elementparsers\
 :user/src/com/google/gwt/uibinder/testing\
diff --git a/user/src/com/google/gwt/i18n/I18N.gwt.xml b/user/src/com/google/gwt/i18n/I18N.gwt.xml
index 00d90ca..a22c58f 100644
--- a/user/src/com/google/gwt/i18n/I18N.gwt.xml
+++ b/user/src/com/google/gwt/i18n/I18N.gwt.xml
@@ -23,60 +23,56 @@
   <!-- 'default' is always defined.                                    -->
   <define-property name="locale" values="default" />
 
-  <property-provider name="locale">
-    <![CDATA[
-      try {
-      var locale;
-      var defaultLocale = "/*-FALLBACK-*/" || 'default';
+  <!--
+   - Configuration property defining the query parameter to use for the locale.
+   - Valid values are any legal URL query parameter name. 
+   -->
+  <define-configuration-property name="locale.queryparam"
+      is-multi-valued="false"/>
+  <set-configuration-property name="locale.queryparam" value="locale"/>
 
-      // Look for the locale as a url argument
-      if (locale == null) {
-        var args = location.search;
-        var startLang = args.indexOf("locale=");
-        if (startLang >= 0) {
-          var language = args.substring(startLang);
-          var begin = language.indexOf("=") + 1;
-          var end = language.indexOf("&");
-          if (end == -1) {
-            end = language.length;
-          }
-          locale = language.substring(begin, end);
-        }
-      }
+  <!--
+   - Configuration property defining the cookie to use for the locale.
+   - Valid values are any legal cookie name. 
+   -->
+  <define-configuration-property name="locale.cookie" is-multi-valued="false"/>
+  <set-configuration-property name="locale.cookie" value=""/>
 
-      if (locale == null) {
-        // Look for the locale on the web page
-        locale = __gwt_getMetaProperty("locale")
-      }
+  <!--
+   - Configuration property controlling whether to use user agent info for
+   - the user's locale.
+   - Valid values are (case insensitive): y/yes/n/no/true/false/on/off (others
+   - are treated as no).
+   -->
+  <define-configuration-property name="locale.useragent"
+      is-multi-valued="false"/>
+  <set-configuration-property name="locale.useragent" value="N"/>
 
-      if (locale == null) {
-        // Look for an override computed by other means in the selection script
-        locale = $wnd['__gwt_Locale'];
-      } else {
-        $wnd['__gwt_Locale'] = locale || defaultLocale;
-      }
+  <!--
+   - Configuration controlling whether to look for locale information in meta
+   - tags embedded by the server.
+   - Valid values are (case insensitive): y/yes/n/no/true/false/on/off (others
+   - are treated as no).
+   -->
+  <define-configuration-property name="locale.usemeta"
+      is-multi-valued="false"/>
+  <set-configuration-property name="locale.usemeta" value="Y"/>
 
-      if (locale == null) {
-        return defaultLocale;
-      }
+  <!--
+   - Configuration property defining the order to search for a locale.
+   - Valid values are comma-separated lists of the following values:
+   -   * queryparam
+   -   * meta
+   -   * cookie
+   -   * useragent
+   -->
+  <define-configuration-property name="locale.searchorder"
+      is-multi-valued="false"/>
+  <set-configuration-property name="locale.searchorder"
+      value="queryparam,cookie,meta,useragent"/>
 
-      while (!__gwt_isKnownPropertyValue("locale",  locale)) {
-        var lastIndex = locale.lastIndexOf("_");
-        if (lastIndex == -1) {
-          locale = defaultLocale;
-          break;
-        } else {
-          locale = locale.substring(0,lastIndex);
-        }
-      }
-
-      return locale;
-    } catch(e){
-      alert("Unexpected exception in locale detection, using default: " + e);
-      return "default";
-    }
-  ]]>
-  </property-provider>
+  <property-provider name="locale"
+      generator="com.google.gwt.i18n.linker.LocalePropertyProviderGenerator"/>
 
   <generate-with class="com.google.gwt.i18n.rebind.LocalizableGenerator">
     <when-type-assignable class="com.google.gwt.i18n.client.Localizable" />
@@ -99,7 +95,6 @@
       reference the compile-time locale set in the "locale" property.
    -->
   <define-configuration-property name="runtime.locales" is-multi-valued="true"/>
-  <set-configuration-property name="runtime.locales" value=""/>
 
   <!--
       A "real" locale to be served by default (i.e. if the browser either
diff --git a/user/src/com/google/gwt/i18n/client/LocaleInfo.java b/user/src/com/google/gwt/i18n/client/LocaleInfo.java
index ff188cf..6505de8 100644
--- a/user/src/com/google/gwt/i18n/client/LocaleInfo.java
+++ b/user/src/com/google/gwt/i18n/client/LocaleInfo.java
@@ -69,6 +69,16 @@
   }
 
   /**
+   * Returns the name of the cookie used by GWT to get the locale, or
+   * null if this app was compiled to not use a cookie.
+   * 
+   * @return cookie name or null
+   */
+  public static String getLocaleCookieName() {
+    return instance.infoImpl.getLocaleCookieName();
+  }
+
+  /**
    * Returns the display name of the requested locale in its native locale, if
    * possible. If no native localization is available, the English name will
    * be returned, or as a last resort just the locale name will be returned.  If
@@ -86,6 +96,16 @@
      */
     return instance.infoImpl.getLocaleNativeDisplayName(localeName);
   }
+ 
+  /**
+   * Returns the name of the query parameter used by GWT to get the locale, or
+   * null if this app was compiled to not use a query parameter.
+   * 
+   * @return query parameter name or null
+   */
+  public static String getLocaleQueryParamName() {
+    return instance.infoImpl.getLocaleQueryParamName();
+  }
 
   /**
    * Returns true if any locale supported by this build of the app is RTL.
diff --git a/user/src/com/google/gwt/i18n/client/impl/LocaleInfoImpl.java b/user/src/com/google/gwt/i18n/client/impl/LocaleInfoImpl.java
index 92d5ee8..ce83e1b 100644
--- a/user/src/com/google/gwt/i18n/client/impl/LocaleInfoImpl.java
+++ b/user/src/com/google/gwt/i18n/client/impl/LocaleInfoImpl.java
@@ -62,6 +62,13 @@
   }
 
   /**
+   * @return the cookie name used for the GWT locale, or null if none.
+   */
+  public String getLocaleCookieName() {
+    return null;
+  }
+
+  /**
    * Returns the current locale name, such as "default, "en_US", etc.
    */
   public String getLocaleName() {
@@ -82,6 +89,13 @@
   }
 
   /**
+   * @return the query parameter name used for the GWT locale, or null if none.
+   */
+  public String getLocaleQueryParamName() {
+    return null;
+  }
+
+  /**
    * @return an implementation of {@link LocalizedNames} for this locale.
    */
   public LocalizedNames getLocalizedNames() {
diff --git a/user/src/com/google/gwt/i18n/linker/LocalePropertyProviderGenerator.java b/user/src/com/google/gwt/i18n/linker/LocalePropertyProviderGenerator.java
new file mode 100644
index 0000000..ecbf23f
--- /dev/null
+++ b/user/src/com/google/gwt/i18n/linker/LocalePropertyProviderGenerator.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2010 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.i18n.linker;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.linker.ConfigurationProperty;
+import com.google.gwt.core.ext.linker.PropertyProviderGenerator;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+import java.util.SortedSet;
+import java.util.regex.Pattern;
+
+/**
+ * Generates a property provider implementation for the "locale" property.
+ */
+public class LocalePropertyProviderGenerator implements PropertyProviderGenerator {
+
+  public static final String LOCALE_QUERYPARAM = "locale.queryparam";
+  
+  public static final String LOCALE_COOKIE = "locale.cookie";
+  
+  public static final String LOCALE_SEARCHORDER = "locale.searchorder";
+
+  public static final String LOCALE_USEMETA = "locale.usemeta";
+
+  public static final String LOCALE_USERAGENT = "locale.useragent";
+
+  protected static final Pattern COOKIE_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_]*$");
+
+  protected static final Pattern QUERYPARAM_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_]*$");
+
+  /**
+   * Return true when the supplied value represents a true/yes/on value.
+   * 
+   * @param value
+   * @return true if the string represents true/yes/on
+   */
+  protected static boolean isTrue(String value) {
+    return value != null && ("yes".equalsIgnoreCase(value)
+        || "y".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value)
+        || "on".equalsIgnoreCase(value));
+  }
+
+  public String generate(TreeLogger logger, SortedSet<String> possibleValues,
+      String fallback, SortedSet<ConfigurationProperty> configProperties)
+      throws UnableToCompleteException {
+    // get relevant config property values
+    String localeQueryParam = null;
+    String localeCookie = null;
+    boolean localeUserAgent = false;
+    boolean localeUseMeta = false;
+    String localeSearchOrder = "queryparam,cookie,meta,useragent";
+    for (ConfigurationProperty configProp : configProperties) {
+      String name = configProp.getName();
+      if (LOCALE_QUERYPARAM.equals(name)) {
+        localeQueryParam = configProp.getValues().get(0);
+        if (localeQueryParam != null && localeQueryParam.length() != 0
+            && !validateQueryParam(localeQueryParam)) {
+          logger.log(TreeLogger.WARN, "Ignoring invalid value of '"
+              + localeQueryParam + "' from '" + LOCALE_QUERYPARAM
+              + "', not a valid query parameter name");
+          localeQueryParam = null;
+        }
+      } else if (LOCALE_COOKIE.equals(name)) {
+          localeCookie = configProp.getValues().get(0);
+          if (localeCookie != null && localeCookie.length() != 0
+              && !validateCookieName(localeCookie)) {
+            logger.log(TreeLogger.WARN, "Ignoring invalid value of '"
+                + localeCookie + "' from '" + LOCALE_COOKIE
+                + "', not a valid cookie name");
+            localeCookie = null;
+          }
+      } else if (LOCALE_USEMETA.equals(name)) {
+        localeUseMeta = isTrue(configProp.getValues().get(0));
+      } else if (LOCALE_USERAGENT.equals(name)) {
+        localeUserAgent = isTrue(configProp.getValues().get(0));
+      } else if (LOCALE_SEARCHORDER.equals(name)) {
+        localeSearchOrder = configProp.getValues().get(0);
+      }
+    }
+    // provide a default for the search order
+    localeSearchOrder = localeSearchOrder.trim();
+    if (localeSearchOrder == null || localeSearchOrder.length() == 0) {
+      localeSearchOrder = "queryparam,cookie,meta,useragent";
+    }
+
+    if (fallback == null) {
+      // TODO(jat): define this in a common place
+      fallback = "default";
+    }
+
+    // build property provider body
+    StringSourceWriter body = new StringSourceWriter();
+    body.println("{");
+    body.indent();
+    body.println("var locale = null;");
+    body.println("var rtlocale = '" + fallback + "';");
+    body.println("try {");
+    for (String method : localeSearchOrder.split(",")) {
+      if ("queryparam".equals(method)) {
+        if (localeQueryParam != null && localeQueryParam.length() > 0) {
+          body.println("if (!locale) {");
+          body.indent();
+          generateQueryParamLookup(logger, body, localeQueryParam);
+          body.outdent();
+          body.println("}");
+        }
+      } else if ("cookie".equals(method)) {
+        if (localeCookie != null && localeCookie.length() > 0) {
+          body.println("if (!locale) {");
+          body.indent();
+          generateCookieLookup(logger, body, localeCookie);
+          body.outdent();
+          body.println("}");
+        }
+      } else if ("meta".equals(method)) {
+        if (localeUseMeta) {
+          body.println("if (!locale) {");
+          body.indent();
+          generateMetaLookup(logger, body);
+          body.outdent();
+          body.println("}");
+        }
+      } else if ("useragent".equals(method)) {
+        if (localeUserAgent) {
+          body.println("if (!locale) {");
+          body.indent();
+          generateUserAgentLookup(logger, body);
+          body.outdent();
+          body.println("}");
+        }
+      } else {
+        logger.log(TreeLogger.WARN, "Ignoring unknown locale lookup method \""
+            + method + "\"");
+        body.println("// ignoring invalid lookup method '" + method + "'");
+      }
+    }
+    body.println("if (!locale) {");
+    body.indent();
+    body.println("locale = $wnd['__gwt_Locale'];");
+    body.outdent();
+    body.println("}");
+    body.println("if (locale) {");
+    body.indent();
+    body.println("rtlocale = locale;");
+    body.outdent();
+    body.println("}");
+    generateInheritanceLookup(logger, body);
+    body.outdent();
+    body.println("} catch (e) {");
+    body.indent();
+    body.println("alert(\"Unexpected exception in locale detection, using "
+        + "default: \" + e);\n");
+    body.outdent();
+    body.println("}");
+    body.println("$wnd['__gwt_Locale'] = rtlocale;");
+    body.println("return locale || \"" + fallback + "\";");
+    body.outdent();
+    body.println("}");
+    return body.toString();
+  }
+
+  /**
+   * Generate JS code that looks up the locale value from a cookie.
+   *
+   * @param logger logger to use
+   * @param body
+   * @param cookieName
+   * @throws UnableToCompleteException
+   */
+  protected void generateCookieLookup(TreeLogger logger, SourceWriter body,
+      String cookieName) throws UnableToCompleteException  {
+    body.println("var cookies = $doc.cookie;");
+    body.println("var idx = cookies.indexOf(\"" + cookieName + "=\");");
+    body.println("if (idx >= 0) {");
+    body.indent();
+    body.println("var end = cookies.indexOf(';', idx);");
+    body.println("if (end < 0) {");
+    body.indent();
+    body.println("end = cookies.length();");
+    body.outdent();
+    body.println("}");
+    body.println("locale = cookies.substring(idx + " + (cookieName.length() + 1)
+        + ", end);");
+    body.outdent();
+    body.println("}");
+  }
+
+  /**
+   * Generate JS code that takes the value of the "locale" variable and finds
+   * parent locales until the value is a supported locale or the default locale.
+   * 
+   * @param logger logger to use
+   * @param body
+   * @throws UnableToCompleteException
+   */
+  protected void generateInheritanceLookup(TreeLogger logger, SourceWriter body)
+      throws UnableToCompleteException  {
+    body.println("while (locale && !__gwt_isKnownPropertyValue(\"locale\", locale)) {");
+    body.indent();
+    body.println("var lastIndex = locale.lastIndexOf(\"_\");");
+    body.println("if (lastIndex < 0) {");
+    body.indent();
+    body.println("locale = null;");
+    body.println("break;");
+    body.outdent();
+    body.println("}");
+    body.println("locale = locale.substring(0, lastIndex);");
+    body.outdent();
+    body.println("}");
+  }
+
+  /**
+   * Generate JS code to fetch the locale from a meta property.
+   *
+   * @param logger logger to use
+   * @param body
+   * @throws UnableToCompleteException
+   */
+  protected void generateMetaLookup(TreeLogger logger, SourceWriter body)
+      throws UnableToCompleteException  {
+    // TODO(jat): do we want to allow customizing the meta property name?
+    body.println("locale = __gwt_getMetaProperty(\"locale\");");
+  }
+
+  /**
+   * Generate JS code to get the locale from a query parameter.
+   *
+   * @param logger logger to use
+   * @param body where to append JS output
+   * @param queryParam the query parameter to use
+   * @throws UnableToCompleteException
+   */
+  protected void generateQueryParamLookup(TreeLogger logger, SourceWriter body,
+      String queryParam) throws UnableToCompleteException  {
+    body.println("var queryParam = location.search;");
+    body.println("var qpStart = queryParam.indexOf(\"" + queryParam + "=\");");
+    body.println("if (qpStart >= 0) {");
+    body.indent();
+    body.println("var value = queryParam.substring(qpStart + "
+        + (queryParam.length() + 1) + ");");
+    body.println("var end = queryParam.indexOf(\"&\", qpStart);");
+    body.println("if (end < 0) {");
+    body.indent();
+    body.println("end = queryParam.length();");
+    body.outdent();
+    body.println("}");
+    body.println("locale = queryParam.substring(qpStart + "
+        + (queryParam.length() + 1) + ", end);");
+    body.outdent();
+    body.println("}");
+  }
+
+  /**
+   * Generate JS code to fetch the locale from the user agent's compile-time
+   * locale.
+   *
+   * @param logger logger to use
+   * @param body
+   * @throws UnableToCompleteException
+   */
+  protected void generateUserAgentLookup(TreeLogger logger, SourceWriter body)
+      throws UnableToCompleteException {
+    body.println("var language = navigator.browserLanguage ? "
+        + "navigator.browserLanguage : navigator.language;");
+    body.println("if (language) {");
+    body.indent();
+    body.println("locale = language.replace(/-/g, \"_\");");
+    body.outdent();
+    body.println();
+  }
+
+  /**
+   * Validate that a name is a valid cookie name.
+   * 
+   * @param cookieName
+   * @return true if cookieName is an acceptable cookie name
+   */
+  protected boolean validateCookieName(String cookieName) {
+    return COOKIE_PATTERN.matcher(cookieName).matches();
+  }
+
+  /**
+   * Validate that a value is a valid query parameter name.
+   * 
+   * @param queryParam
+   * @return true if queryParam is a valid query parameter name. 
+   */
+  protected boolean validateQueryParam(String queryParam) {
+    return QUERYPARAM_PATTERN.matcher(queryParam).matches();
+  }
+}
diff --git a/user/src/com/google/gwt/i18n/rebind/LocaleInfoGenerator.java b/user/src/com/google/gwt/i18n/rebind/LocaleInfoGenerator.java
index a899a96..f681eb7 100644
--- a/user/src/com/google/gwt/i18n/rebind/LocaleInfoGenerator.java
+++ b/user/src/com/google/gwt/i18n/rebind/LocaleInfoGenerator.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.ConfigurationProperty;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.GeneratorContext;
 import com.google.gwt.core.ext.PropertyOracle;
@@ -26,6 +28,7 @@
 import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.i18n.client.impl.LocaleInfoImpl;
+import com.google.gwt.i18n.linker.LocalePropertyProviderGenerator;
 import com.google.gwt.i18n.server.GwtLocaleImpl;
 import com.google.gwt.i18n.shared.GwtLocale;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
@@ -83,6 +86,24 @@
   }
 
   /**
+   * Generates source representing a string constant that might be null (and
+   * empty strings are treated as null as well).
+   * 
+   * @param value
+   * @return source representation of value
+   */
+  private static String possiblyNullStringConstant(String value) {
+    if (value == null || value.length() == 0) {
+      return "null";
+    }
+    return "\"" + quoteQuotes(value) + "\"";
+  }
+
+  private static String quoteQuotes(String val) {
+    return val.replace("\"", "\\\"");
+  }
+
+  /**
    * Generate an implementation for the given type.
    * 
    * @param logger error logger
@@ -99,6 +120,25 @@
     PropertyOracle propertyOracle = context.getPropertyOracle();
     LocaleUtils localeUtils = LocaleUtils.getInstance(logger, propertyOracle,
         context);
+    String cookieName = null;
+    String queryParamName = null;
+    ConfigurationProperty prop;
+    try {
+      prop = propertyOracle.getConfigurationProperty(
+          LocalePropertyProviderGenerator.LOCALE_COOKIE);
+      cookieName = prop.getValues().get(0);
+    } catch (BadPropertyValueException e) {
+    } catch (IndexOutOfBoundsException e) {
+      // ignore, leaving the value as null
+    }
+    try {
+      prop = propertyOracle.getConfigurationProperty(
+          LocalePropertyProviderGenerator.LOCALE_QUERYPARAM);
+      queryParamName = prop.getValues().get(0);
+    } catch (BadPropertyValueException e) {
+    } catch (IndexOutOfBoundsException e) {
+      // ignore, leaving the value as null
+    }
 
     JClassType targetClass;
     try {
@@ -177,6 +217,12 @@
       writer.println("}");
       writer.println();
       writer.println("@Override");
+      writer.println("public String getLocaleCookieName() {");
+      writer.println("  return " + possiblyNullStringConstant(cookieName)
+          + ";");
+      writer.println("}");
+      writer.println();
+      writer.println("@Override");
       writer.println("public String getLocaleNativeDisplayName(String localeName) {");
       writer.println("  if (GWT.isScript()) {");
       writer.println("    if (nativeDisplayNamesNative == null) {");
@@ -210,6 +256,12 @@
       writer.println("}");
       writer.println();
       writer.println("@Override");
+      writer.println("public String getLocaleQueryParamName() {");
+      writer.println("  return " + possiblyNullStringConstant(queryParamName)
+          + ";");
+      writer.println("}");
+      writer.println();
+      writer.println("@Override");
       writer.println("public boolean hasAnyRTL() {");
       writer.println("  return " + hasAnyRtl + ";");
       writer.println("}");
@@ -379,8 +431,4 @@
     }
     locales.add(locale);
   }
-  
-  private String quoteQuotes(String val) {
-    return val.replace("\"", "\\\"");
-  }
 }
diff --git a/user/test/com/google/gwt/i18n/I18NTest_ar.gwt.xml b/user/test/com/google/gwt/i18n/I18NTest_ar.gwt.xml
index e00f9d8..cfc54ae 100644
--- a/user/test/com/google/gwt/i18n/I18NTest_ar.gwt.xml
+++ b/user/test/com/google/gwt/i18n/I18NTest_ar.gwt.xml
@@ -20,4 +20,7 @@
 	<source path="client"/>
 	<extend-property name="locale" values="ar"/>
 	<set-property name = "locale" value = "ar"/>
+
+  <set-configuration-property name="locale.cookie" value="LOCALE"/>
+  <set-configuration-property name="locale.queryparam" value="arlocale"/>
 </module>
diff --git a/user/test/com/google/gwt/i18n/client/LocaleInfoTest.java b/user/test/com/google/gwt/i18n/client/LocaleInfoTest.java
index 52aa6db..c4bae15 100644
--- a/user/test/com/google/gwt/i18n/client/LocaleInfoTest.java
+++ b/user/test/com/google/gwt/i18n/client/LocaleInfoTest.java
@@ -38,6 +38,17 @@
     assertEquals("piglatin_UK_WINDOWS", locale);
   }
 
+  public void testLocaleCookieName() {
+    String cookieName = LocaleInfo.getLocaleCookieName();
+    assertNull("Default locale cooke name should be null", cookieName);
+  }
+
+  public void testLocaleQueryParam() {
+    String queryParam = LocaleInfo.getLocaleQueryParamName();
+    assertEquals("Default locale query param should be 'locale'", "locale",
+        queryParam);
+  }
+
   public void testNativeDisplayNames() {
     // en isn't in the property set for this module so should return null
     String displayName = LocaleInfo.getLocaleNativeDisplayName("en");
diff --git a/user/test/com/google/gwt/i18n/client/LocaleInfo_ar_Test.java b/user/test/com/google/gwt/i18n/client/LocaleInfo_ar_Test.java
index 6acd948..3b2a15e 100644
--- a/user/test/com/google/gwt/i18n/client/LocaleInfo_ar_Test.java
+++ b/user/test/com/google/gwt/i18n/client/LocaleInfo_ar_Test.java
@@ -30,11 +30,6 @@
     return "com.google.gwt.i18n.I18NTest_ar";
   }
 
-  public void testCurrentLocale() {
-    String locale = LocaleInfo.getCurrentLocale().getLocaleName();
-    assertEquals("ar", locale);
-  }
-
   public void testAvailableLocales() {
     String[] locales = LocaleInfo.getAvailableLocaleNames();
     ArrayList<String> localeList = new ArrayList<String>();
@@ -43,6 +38,23 @@
     assertTrue(localeList.contains("default"));
   }
 
+  public void testCurrentLocale() {
+    String locale = LocaleInfo.getCurrentLocale().getLocaleName();
+    assertEquals("ar", locale);
+  }
+
+  public void testLocaleCookieName() {
+    String cookieName = LocaleInfo.getLocaleCookieName();
+    assertEquals("I18N/ar locale cooke name should be 'LOCALE'", "LOCALE",
+        cookieName);
+  }
+
+  public void testLocaleQueryParam() {
+    String queryParam = LocaleInfo.getLocaleQueryParamName();
+    assertEquals("I18N/ar locale query param should be 'arlocale'", "arlocale",
+        queryParam);
+  }
+
   public void testNativeDisplayNames() {
     // verify ar is known
     String displayName = LocaleInfo.getLocaleNativeDisplayName("ar");