Allow property values to be restricted via command line flag.

The command line argument should be like this:
 [-setProperty name=value,value...]

-setProperty flag can be used multiple times to set
different properties.

If a property is set multiple times, the last one overwrites
the others.

Change-Id: I9df49c81790a4eb77f3698b7c8cdcafe6dcb5460
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
index 217113a..6e7c460 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/CompilerOptionsImpl.java
@@ -25,6 +25,8 @@
 import com.google.gwt.dev.util.arg.OptionOptimize;
 import com.google.gwt.dev.util.arg.SourceLevel;
 import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
+import com.google.gwt.thirdparty.guava.common.collect.LinkedListMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 
 import java.io.File;
@@ -45,6 +47,7 @@
   private final boolean strictSourceResources;
   private final OptionJsInteropMode.Mode jsInteropMode;
   private final OptionMethodNameDisplayMode.Mode methodNameDisplayMode;
+  private final ListMultimap<String, String> properties;
 
   CompilerOptionsImpl(CompileDir compileDir, String moduleName, Options options) {
     this.compileDir = compileDir;
@@ -57,6 +60,7 @@
     this.logLevel = options.getLogLevel();
     this.jsInteropMode = options.getJsInteropMode();
     this.methodNameDisplayMode = options.getMethodNameDisplayMode();
+    this.properties = LinkedListMultimap.create(options.getProperties());
   }
 
   @Override
@@ -109,11 +113,6 @@
     return ImmutableList.of();
   }
 
-  @Override
-  public OptionMethodNameDisplayMode.Mode getMethodNameDisplayMode() {
-    return methodNameDisplayMode;
-  }
-
   /**
    * Number of threads to use to compile permutations.
    */
@@ -133,6 +132,11 @@
   }
 
   @Override
+  public OptionMethodNameDisplayMode.Mode getMethodNameDisplayMode() {
+    return methodNameDisplayMode;
+  }
+
+  @Override
   public File getMissingDepsFile() {
     return null; // Don't record and save missing dependency information to a file.
   }
@@ -163,6 +167,11 @@
   }
 
   @Override
+  public ListMultimap<String, String> getProperties() {
+    return properties;
+  }
+
+  @Override
   public File getSaveSourceOutput() {
     return null;
   }
@@ -218,6 +227,11 @@
   }
 
   @Override
+  public boolean isIncrementalCompileEnabled() {
+    return incremental;
+  }
+
+  @Override
   public  boolean isJsonSoycEnabled() {
     return false;
   }
@@ -274,11 +288,6 @@
   }
 
   @Override
-  public boolean isIncrementalCompileEnabled() {
-    return incremental;
-  }
-
-  @Override
   public boolean shouldInlineLiteralParameters() {
     return false;
   }
@@ -319,12 +328,12 @@
   }
 
   @Override
-  public boolean warnOverlappingSource() {
+  public boolean warnMissingDeps() {
     return false;
   }
 
   @Override
-  public boolean warnMissingDeps() {
+  public boolean warnOverlappingSource() {
     return false;
   }
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
index 2c6e8dd..66e5348 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
@@ -23,15 +23,19 @@
 import com.google.gwt.dev.util.arg.ArgHandlerJsInteropMode;
 import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
 import com.google.gwt.dev.util.arg.ArgHandlerMethodNameDisplayMode;
+import com.google.gwt.dev.util.arg.ArgHandlerSetProperties;
 import com.google.gwt.dev.util.arg.ArgHandlerSourceLevel;
 import com.google.gwt.dev.util.arg.OptionIncrementalCompile;
 import com.google.gwt.dev.util.arg.OptionJsInteropMode;
 import com.google.gwt.dev.util.arg.OptionLogLevel;
 import com.google.gwt.dev.util.arg.OptionMethodNameDisplayMode;
+import com.google.gwt.dev.util.arg.OptionSetProperties;
 import com.google.gwt.dev.util.arg.OptionSourceLevel;
 import com.google.gwt.dev.util.arg.SourceLevel;
 import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
 import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
+import com.google.gwt.thirdparty.guava.common.collect.LinkedListMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
 import com.google.gwt.util.tools.ArgHandler;
 import com.google.gwt.util.tools.ArgHandlerDir;
 import com.google.gwt.util.tools.ArgHandlerExtra;
@@ -82,6 +86,8 @@
   private OptionMethodNameDisplayMode.Mode methodNameDisplayMode =
       OptionMethodNameDisplayMode.Mode.NONE;
 
+  private final ListMultimap<String, String> properties = LinkedListMultimap.create();
+
   /**
    * Sets each option to the appropriate value, based on command-line arguments.
    * If there is an error, prints error messages and/or usage to System.err.
@@ -299,6 +305,10 @@
     return jsInteropMode;
   }
 
+  ListMultimap<String, String> getProperties() {
+    return properties;
+  }
+
   private class ArgProcessor extends ArgProcessorBase {
 
     public ArgProcessor() {
@@ -314,6 +324,19 @@
       registerHandler(new StrictResourcesFlag());
       registerHandler(new WorkDirFlag());
       registerHandler(new LauncherDir());
+      registerHandler(new ArgHandlerSetProperties(new OptionSetProperties() {
+
+          @Override
+        public void setPropertyValues(String name, Iterable<String> values) {
+          properties.replaceValues(name, values);
+        }
+
+          @Override
+        public ListMultimap<String, String> getProperties() {
+          return properties;
+        }
+
+      }));
       registerHandler(new ArgHandlerIncrementalCompile(new OptionIncrementalCompile() {
         @Override
         public boolean isIncrementalCompileEnabled() {
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index 21fceb4..0e86063 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -297,6 +297,10 @@
     compilerContext = compilerContextBuilder.options(loadOptions).build();
 
     ModuleDef module = loadModule(compileLogger);
+    if (!Compiler.maybeRestrictProperties(compileLogger, module,
+        loadOptions.getProperties())) {
+      return false;
+    }
 
     // We need to generate the stub before restricting permutations
     String recompileJs = generateModuleRecompileJs(module, compileLogger);
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
index 312881a..9576991 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/UnmodifiableCompilerOptions.java
@@ -299,4 +299,9 @@
   public void setMethodNameDisplayMode(OptionMethodNameDisplayMode.Mode methodNameDisplayMode) {
     throw new UnsupportedOperationException();
   }
+
+  @Override
+  public void setPropertyValues(String name, Iterable<String> value) {
+    throw new UnsupportedOperationException();
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/Compiler.java b/dev/core/src/com/google/gwt/dev/Compiler.java
index d519851..238aa8d 100644
--- a/dev/core/src/com/google/gwt/dev/Compiler.java
+++ b/dev/core/src/com/google/gwt/dev/Compiler.java
@@ -19,6 +19,8 @@
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.ArtifactSet;
 import com.google.gwt.dev.CompileTaskRunner.CompileTask;
+import com.google.gwt.dev.cfg.BindingProperty;
+import com.google.gwt.dev.cfg.ConfigurationProperty;
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.cfg.ModuleDefLoader;
 import com.google.gwt.dev.javac.UnitCache;
@@ -37,18 +39,22 @@
 import com.google.gwt.dev.util.arg.ArgHandlerLocalWorkers;
 import com.google.gwt.dev.util.arg.ArgHandlerMethodNameDisplayMode;
 import com.google.gwt.dev.util.arg.ArgHandlerSaveSourceOutput;
+import com.google.gwt.dev.util.arg.ArgHandlerSetProperties;
 import com.google.gwt.dev.util.arg.ArgHandlerWarDir;
 import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional;
 import com.google.gwt.dev.util.arg.OptionOptimize;
 import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
 import com.google.gwt.util.tools.Utility;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map.Entry;
 import java.util.concurrent.FutureTask;
 
 /**
@@ -71,6 +77,7 @@
       registerHandler(new ArgHandlerExtraDir(options));
       registerHandler(new ArgHandlerSaveSourceOutput(options));
       registerHandler(new ArgHandlerMethodNameDisplayMode(options));
+      registerHandler(new ArgHandlerSetProperties(options));
     }
 
     @Override
@@ -153,7 +160,12 @@
     ModuleDef[] modules = new ModuleDef[options.getModuleNames().size()];
     int i = 0;
     for (String moduleName : options.getModuleNames()) {
-      modules[i++] = ModuleDefLoader.loadFromClassPath(logger, compilerContext, moduleName, true);
+      ModuleDef module =
+          ModuleDefLoader.loadFromClassPath(logger, compilerContext, moduleName, true);
+      modules[i++] = module;
+      if (!maybeRestrictProperties(logger, module, options.getProperties())) {
+        return false;
+      }
     }
     return run(logger, modules);
   }
@@ -264,4 +276,43 @@
     }
     return true;
   }
+
+  public static boolean maybeRestrictProperties(TreeLogger logger, ModuleDef module,
+      ListMultimap<String, String> properties) {
+    try {
+      for (Entry<String, Collection<String>> property : properties.asMap().entrySet()) {
+        String propertyName = property.getKey();
+        Collection<String> propertyValues = property.getValue();
+        BindingProperty bindingProp = module.getProperties().findBindingProp(propertyName);
+        ConfigurationProperty configProp = module.getProperties().findConfigProp(propertyName);
+        if (bindingProp != null) {
+          bindingProp.setValues(bindingProp.getRootCondition(),
+              propertyValues.toArray(new String[propertyValues.size()]));
+        } else if (configProp != null) {
+          if (configProp.allowsMultipleValues()) {
+            configProp.clear();
+            for (String propertyValue : propertyValues) {
+              configProp.addValue(propertyValue);
+            }
+          } else {
+            String firstValue = propertyValues.iterator().next();
+            if (propertyValues.size() > 1) {
+              logger.log(TreeLogger.ERROR,
+                  "Attemp to set multiple values to a single-valued configuration property '"
+                  + propertyName + "'.");
+              return false;
+            }
+            configProp.setValue(firstValue);
+          }
+        } else {
+          logger.log(TreeLogger.ERROR, "Unknown property: " + propertyName);
+          return false;
+        }
+      }
+    } catch (IllegalArgumentException e) {
+      logger.log(TreeLogger.ERROR, e.getMessage());
+      return false;
+    }
+    return true;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/DevMode.java b/dev/core/src/com/google/gwt/dev/DevMode.java
index f55e144..f2aeea7 100644
--- a/dev/core/src/com/google/gwt/dev/DevMode.java
+++ b/dev/core/src/com/google/gwt/dev/DevMode.java
@@ -40,6 +40,7 @@
 import com.google.gwt.dev.util.arg.ArgHandlerMethodNameDisplayMode;
 import com.google.gwt.dev.util.arg.ArgHandlerModuleName;
 import com.google.gwt.dev.util.arg.ArgHandlerModulePathPrefix;
+import com.google.gwt.dev.util.arg.ArgHandlerSetProperties;
 import com.google.gwt.dev.util.arg.ArgHandlerSourceLevel;
 import com.google.gwt.dev.util.arg.ArgHandlerWarDir;
 import com.google.gwt.dev.util.arg.ArgHandlerWorkDirOptional;
@@ -253,6 +254,7 @@
           return super.getPurpose() + " to host";
         }
       });
+      registerHandler(new ArgHandlerSetProperties(options));
     }
 
     @Override
diff --git a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
index 8475edf..f4e990a 100644
--- a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
+++ b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptions.java
@@ -22,6 +22,7 @@
 import com.google.gwt.dev.util.arg.OptionMaxPermsPerPrecompile;
 import com.google.gwt.dev.util.arg.OptionMissingDepsFile;
 import com.google.gwt.dev.util.arg.OptionSaveSource;
+import com.google.gwt.dev.util.arg.OptionSetProperties;
 import com.google.gwt.dev.util.arg.OptionSourceMapFilePrefix;
 import com.google.gwt.dev.util.arg.OptionValidateOnly;
 import com.google.gwt.dev.util.arg.OptionWarnMissingDeps;
@@ -33,5 +34,6 @@
 public interface PrecompileTaskOptions extends JJSOptions, CompileTaskOptions, OptionGenDir,
     OptionSaveSource, OptionSourceMapFilePrefix, OptionValidateOnly, OptionDisableUpdateCheck,
     OptionEnableGeneratingOnShards, OptionMaxPermsPerPrecompile, OptionMissingDepsFile,
-    OptionWarnOverlappingSource, OptionWarnMissingDeps, PrecompilationResult {
+    OptionWarnOverlappingSource, OptionWarnMissingDeps, PrecompilationResult,
+    OptionSetProperties {
 }
diff --git a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
index c73a08c..a958425 100644
--- a/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
+++ b/dev/core/src/com/google/gwt/dev/PrecompileTaskOptionsImpl.java
@@ -23,6 +23,8 @@
 import com.google.gwt.dev.util.arg.OptionJsInteropMode;
 import com.google.gwt.dev.util.arg.OptionMethodNameDisplayMode;
 import com.google.gwt.dev.util.arg.SourceLevel;
+import com.google.gwt.thirdparty.guava.common.collect.LinkedListMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
 
 import java.io.File;
 
@@ -42,6 +44,7 @@
   private boolean validateOnly;
   private boolean warnOverlappingSource;
   private boolean warnMissingDeps;
+  private final ListMultimap<String, String> properties = LinkedListMultimap.create();
 
   public PrecompileTaskOptionsImpl() {
   }
@@ -77,6 +80,7 @@
     setMissingDepsFile(other.getMissingDepsFile());
     setValidateOnly(other.isValidateOnly());
     setEnabledGeneratingOnShards(other.isEnabledGeneratingOnShards());
+    properties.putAll(other.getProperties());
   }
 
   @Override
@@ -104,6 +108,10 @@
     return genDir;
   }
 
+  @Override public OptionJsInteropMode.Mode getJsInteropMode() {
+    return jjsOptions.getJsInteropMode();
+  }
+
   @Override
   public int getMaxPermsPerPrecompile() {
     return maxPermsPerPrecompile;
@@ -135,8 +143,12 @@
   }
 
   @Override
-  public SourceLevel getSourceLevel()
-  {
+  public ListMultimap<String, String> getProperties() {
+    return properties;
+  }
+
+  @Override
+  public SourceLevel getSourceLevel() {
     return jjsOptions.getSourceLevel();
   }
 
@@ -180,6 +192,11 @@
   }
 
   @Override
+  public boolean isIncrementalCompileEnabled() {
+    return jjsOptions.isIncrementalCompileEnabled();
+  }
+
+  @Override
   public boolean isJsonSoycEnabled() {
     return jjsOptions.isJsonSoycEnabled();
   }
@@ -250,11 +267,6 @@
   }
 
   @Override
-  public void setIncrementalCompileEnabled(boolean enabled) {
-    jjsOptions.setIncrementalCompileEnabled(enabled);
-  }
-
-  @Override
   public void setCompilerMetricsEnabled(boolean enabled) {
     jjsOptions.setCompilerMetricsEnabled(enabled);
   }
@@ -300,10 +312,19 @@
   }
 
   @Override
+  public void setIncrementalCompileEnabled(boolean enabled) {
+    jjsOptions.setIncrementalCompileEnabled(enabled);
+  }
+
+  @Override
   public void setInlineLiteralParameters(boolean enabled) {
     jjsOptions.setInlineLiteralParameters(enabled);
   }
 
+  @Override public void setJsInteropMode(OptionJsInteropMode.Mode mode) {
+    jjsOptions.setJsInteropMode(mode);
+  }
+
   @Override
   public void setJsonSoycEnabled(boolean enabled) {
     jjsOptions.setJsonSoycEnabled(enabled);
@@ -360,6 +381,11 @@
   }
 
   @Override
+  public void setPropertyValues(String name, Iterable<String> values) {
+    properties.replaceValues(name, values);
+  }
+
+  @Override
   public void setRunAsyncEnabled(boolean enabled) {
     jjsOptions.setRunAsyncEnabled(enabled);
   }
@@ -410,13 +436,13 @@
   }
 
   @Override
-  public void setWarnOverlappingSource(boolean warnOverlappingSource) {
-    this.warnOverlappingSource = warnOverlappingSource;
+  public void setWarnMissingDeps(boolean showMissingDepsWarnings) {
+    this.warnMissingDeps = showMissingDepsWarnings;
   }
 
   @Override
-  public void setWarnMissingDeps(boolean showMissingDepsWarnings) {
-    this.warnMissingDeps = showMissingDepsWarnings;
+  public void setWarnOverlappingSource(boolean warnOverlappingSource) {
+    this.warnOverlappingSource = warnOverlappingSource;
   }
 
   @Override
@@ -430,11 +456,6 @@
   }
 
   @Override
-  public boolean isIncrementalCompileEnabled() {
-    return jjsOptions.isIncrementalCompileEnabled();
-  }
-
-  @Override
   public boolean shouldInlineLiteralParameters() {
     return jjsOptions.shouldInlineLiteralParameters();
   }
@@ -448,7 +469,6 @@
   public boolean shouldOptimizeDataflow() {
     return jjsOptions.shouldOptimizeDataflow();
   }
-
   @Override
   public boolean shouldOrdinalizeEnums() {
     return jjsOptions.shouldOrdinalizeEnums();
@@ -468,21 +488,14 @@
   public boolean useDetailedTypeIds() {
     return jjsOptions.useDetailedTypeIds();
   }
-  @Override
-  public boolean warnOverlappingSource() {
-    return warnOverlappingSource;
-  }
 
   @Override
   public boolean warnMissingDeps() {
     return warnMissingDeps;
   }
 
-  @Override public OptionJsInteropMode.Mode getJsInteropMode() {
-    return jjsOptions.getJsInteropMode();
-  }
-
-  @Override public void setJsInteropMode(OptionJsInteropMode.Mode mode) {
-    jjsOptions.setJsInteropMode(mode);
+  @Override
+  public boolean warnOverlappingSource() {
+    return warnOverlappingSource;
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/SuperDevListener.java b/dev/core/src/com/google/gwt/dev/shell/SuperDevListener.java
index 27614e4..d266e2a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/SuperDevListener.java
+++ b/dev/core/src/com/google/gwt/dev/shell/SuperDevListener.java
@@ -24,6 +24,8 @@
 import com.google.gwt.dev.cfg.ModuleDef;
 import com.google.gwt.dev.util.arg.OptionJsInteropMode;
 import com.google.gwt.dev.util.arg.OptionMethodNameDisplayMode;
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.util.concurrent.SettableFuture;
 
 import java.io.File;
@@ -207,9 +209,26 @@
       args.add("-XmethodNameDisplayMode");
       args.add(options.getMethodNameDisplayMode().name());
     }
+    if (options.getProperties().size() > 0) {
+      args.addAll(makeSetPropertyArgs(options.getProperties()));
+    }
     for (String mod : options.getModuleNames()) {
       args.add(mod);
     }
     return args;
   }
+
+  private static List<String> makeSetPropertyArgs(
+      ListMultimap<String, String> properties) {
+    List<String> propertyArgs = Lists.newArrayList();
+    for (String propertyName : properties.keySet()) {
+      propertyArgs.add("-setProperty");
+      StringBuilder nameValues = new StringBuilder(propertyName + "=");
+      for (String propertyValue : properties.get(propertyName)) {
+        nameValues.append(propertyValue + ",");
+      }
+      propertyArgs.add(nameValues.substring(0, nameValues.length() - 1));
+    }
+    return propertyArgs;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSetProperties.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSetProperties.java
new file mode 100644
index 0000000..3f8bb1f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSetProperties.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2014 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.dev.util.arg;
+
+import com.google.gwt.thirdparty.guava.common.base.Splitter;
+import com.google.gwt.util.tools.ArgHandlerString;
+
+import java.util.List;
+
+/**
+ * An argument handler to parse the -setProperty argument. Set the allowed values of a given
+ * property to the given values. The format is like: [-setProperty name=value,value,...,value].
+ * -setProperty flag can be used multiple times to set different properties. If a property is set
+ * multiple times, the last one over writes the others.
+ */
+public class ArgHandlerSetProperties extends ArgHandlerString {
+
+  private final OptionSetProperties options;
+
+  public ArgHandlerSetProperties(OptionSetProperties options) {
+    this.options = options;
+  }
+
+  @Override
+  public boolean setString(String str) {
+    assert (str != null);
+    List<String> nameValuePair = Splitter.on("=").trimResults().omitEmptyStrings().splitToList(str);
+    if (nameValuePair.size() != 2) {
+      return false;
+    }
+    String name = nameValuePair.get(0);
+    String valuesList = nameValuePair.get(1);
+    Iterable<String> values = Splitter.on(",").trimResults().omitEmptyStrings().split(valuesList);
+    options.setPropertyValues(name, values);
+    return true;
+  }
+
+  @Override
+  public String getPurpose() {
+    return "Set the values of a property in the form of propertyName=value1[,value2...].";
+  }
+
+  @Override
+  public String getTag() {
+    return "-setProperty";
+  }
+
+  @Override
+  public String[] getTagArgs() {
+    return new String[] {"name=value,value..."};
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/OptionSetProperties.java b/dev/core/src/com/google/gwt/dev/util/arg/OptionSetProperties.java
new file mode 100644
index 0000000..4ef915e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/arg/OptionSetProperties.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014 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.dev.util.arg;
+
+import com.google.gwt.thirdparty.guava.common.collect.ListMultimap;
+
+/**
+ * Option to set property values.
+ */
+public interface OptionSetProperties {
+
+  /**
+   * Set the value of property {@code name} to be {@code values}.
+   */
+  void setPropertyValues(String name, Iterable<String> values);
+
+  /**
+   * Return all properties ({name->values} pairs).
+   */
+  ListMultimap<String, String> getProperties();
+
+}
diff --git a/dev/core/test/com/google/gwt/dev/ArgHandlerSetPropertiesTest.java b/dev/core/test/com/google/gwt/dev/ArgHandlerSetPropertiesTest.java
new file mode 100644
index 0000000..f55c2c5
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/ArgHandlerSetPropertiesTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2014 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.dev;
+
+import java.util.Collection;
+
+/**
+ * Test for {@ArgHandlerSetProperties}.
+ */
+public class ArgHandlerSetPropertiesTest extends ArgProcessorTestBase {
+  private final Compiler.ArgProcessor argProcessor;
+  private final CompilerOptionsImpl options = new CompilerOptionsImpl();
+
+  public ArgHandlerSetPropertiesTest() {
+    argProcessor = new Compiler.ArgProcessor(options);
+  }
+
+  public void testSinglePropertySingleValue() {
+    assertProcessSuccess(argProcessor,
+        new String[] {"-setProperty", "locale=zh", "my.Module"});
+    assertEquals("{locale=[zh]}", options.getProperties().toString());
+  }
+
+  public void testMultiplePropertiesSingleValue() {
+    assertProcessSuccess(argProcessor, new String[] {
+        "-setProperty", "locale=zh",
+        "-setProperty", "user.agent=safari",
+        "-setProperty", "stackTraces=false", "my.Module"});
+    assertEquals(3, options.getProperties().keySet().size());
+    assertEquals("[zh]", options.getProperties().get("locale").toString());
+    assertEquals("[safari]", options.getProperties().get("user.agent").toString());
+    assertEquals("[false]", options.getProperties().get("stackTraces").toString());
+  }
+
+  public void testSinglePropertyMultipleValues() {
+    assertProcessSuccess(argProcessor,
+        new String[] {"-setProperty", "locale=zh,en", "my.Module"});
+    assertEquals(1, options.getProperties().keySet().size());
+    Collection<String> locales = options.getProperties().get("locale");
+    assertEquals(2, locales.size());
+    assertTrue(locales.contains("zh") && locales.contains("en"));
+  }
+
+  public void testMultiplePropertiesMultipleValues() {
+    assertProcessSuccess(argProcessor, new String[] {
+        "-setProperty", "locale=zh,en",
+        "-setProperty", "user.agent=safari,opera",
+        "-setProperty", "stackTraces=true,false",
+        "my.Module"});
+    assertEquals(3, options.getProperties().keySet().size());
+
+    Collection<String> locales = options.getProperties().get("locale");
+    Collection<String> userAgents = options.getProperties().get("user.agent");
+    Collection<String> stackTraces = options.getProperties().get("stackTraces");
+    assertEquals(2, locales.size());
+    assertTrue(locales.contains("zh") && locales.contains("en"));
+    assertEquals(2, userAgents.size());
+    assertTrue(userAgents.contains("opera") && userAgents.contains("safari"));
+    assertEquals(2, stackTraces.size());
+    assertTrue(stackTraces.contains("false") && stackTraces.contains("true"));
+  }
+
+  public void testSetOnePropertyMultipleTimes() {
+    assertProcessSuccess(argProcessor, new String[] {
+        "-setProperty", "locale = zh",
+        "-setProperty", "locale = en, fr",
+        "my.Module"
+    });
+    assertEquals(1, options.getProperties().keySet().size());
+    Collection<String> locales = options.getProperties().get("locale");
+    assertEquals(2, locales.size());
+    assertTrue(locales.contains("en") && locales.contains("fr"));
+  }
+}