Super Dev Mode: add user-defined tags to JobEvent

Tags are search terms that can be added by startup code
that embeds the code server. They're restricted to
short strings that will work well in a search query.
For example, we could use tags to mark compiles
launched by GWTTestCase, when we add that.

Also, automatically add tags to track whether incremental
compiles and precompiles are turned on or off.

Rationale for using tags to track incremental compiles:

CompileStrategy doesn't quite do this because the first compile
is a "full" compile and we may add an option to trigger a full
compile manually. We shouldn't change this because it will
mess up the average times for incremental compiles.

JobEvent.getArguments() could be made to work, but tags seem
a bit cleaner, since they won't change if we rename flags or
add synonyms. We can also drop a tag without changing the
JobEvent API, so it's better for temporary terms used to track
migrations.

Change-Id: I2c0661105f697ec8e628e733b868a590d35f4c2a
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
index b94c0c5..9af7cda 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Job.java
@@ -28,6 +28,7 @@
 import com.google.gwt.thirdparty.guava.common.util.concurrent.SettableFuture;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -70,6 +71,7 @@
   // Miscellaneous
 
   private final ImmutableList<String> args;
+  private final Set<String> tags;
 
   /**
    * The id to report to the recompile listener.
@@ -98,6 +100,7 @@
     this.recompileListener = Preconditions.checkNotNull(options.getRecompileListener());
     this.jobChangeListener = Preconditions.checkNotNull(options.getJobChangeListener());
     this.args = Preconditions.checkNotNull(options.getArgs());
+    this.tags = Preconditions.checkNotNull(options.getTags());
     this.logSupplier = new LogSupplier(parentLogger, id);
   }
 
@@ -284,6 +287,7 @@
     out.setCompileDir(compileDir);
     out.setCompileStrategy(compileStrategy);
     out.setArguments(args);
+    out.setTags(tags);
     return out.build();
   }
 
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/JobEvent.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/JobEvent.java
index ee26865..e4582a6 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/JobEvent.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/JobEvent.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
+import java.util.regex.Pattern;
 
 /**
  * The status of a compile job submitted to Super Dev Mode.
@@ -32,6 +33,7 @@
  * <p>JobEvent objects are deeply immutable, though they describe a Job that changes.
  */
 public final class JobEvent {
+  private static final Pattern VALID_TAG = Pattern.compile("\\S{1,100}");
 
   private final String jobId;
 
@@ -43,6 +45,7 @@
   private final CompileDir compileDir;
   private final CompileStrategy compileStrategy;
   private final ImmutableList<String> arguments;
+  private final ImmutableList<String> tags;
 
   private JobEvent(Builder builder) {
     this.jobId = Preconditions.checkNotNull(builder.jobId);
@@ -55,6 +58,7 @@
     this.compileDir = builder.compileDir;
     this.compileStrategy = builder.compileStrategy;
     this.arguments = ImmutableList.copyOf(builder.args);
+    this.tags = ImmutableList.copyOf(builder.tags);
 
     // Any new fields added should allow nulls for backward compatibility.
   }
@@ -120,6 +124,34 @@
   }
 
   /**
+   * User-defined tags associated with this job. (Not null but may be empty.)
+   */
+  public ImmutableList<String> getTags() { return tags; }
+
+  /**
+   * If all the given tags are valid, returns a list containing the tags.
+   * @throws java.lang.IllegalArgumentException if any tag is invalid.
+   */
+  static ImmutableList<String> checkTags(Iterable<String> tags) {
+    ImmutableList.Builder<String> builder = ImmutableList.builder();
+    for (String tag : tags) {
+      if (!isValidTag(tag)) {
+        throw new IllegalArgumentException("invalid tag: " + tag);
+      }
+      builder.add(tag);
+    }
+    return builder.build();
+  }
+
+  /**
+   * Returns true if the tag is valid.
+   * Tags must not be null, contain whitespace, or be more than 100 characters.
+   */
+  private static boolean isValidTag(String candidate) {
+    return candidate != null && VALID_TAG.matcher(candidate).matches();
+  }
+
+  /**
    * Defines the lifecycle of a job.
    */
   public enum Status {
@@ -155,7 +187,7 @@
     /**
      * The string to use for serialization.
      */
-    String getJsonName() {
+    public String getJsonName() {
       return jsonName;
     }
   }
@@ -175,6 +207,7 @@
     private CompileDir compileDir;
     private CompileStrategy compileStrategy;
     private List<String> args = ImmutableList.of();
+    private List<String> tags = ImmutableList.of();
 
     /**
      * A unique id for this job. Required.
@@ -250,6 +283,14 @@
       this.args = Preconditions.checkNotNull(args);
     }
 
+    /**
+     * User-defined tags passed to {@link Options#addTags}.
+     * Optional but may not be null. If not set, defaults to the empty list.
+     */
+    public void setTags(Iterable<String> tags) {
+      this.tags = checkTags(tags);
+    }
+
     public JobEvent build() {
       return new JobEvent(this);
     }
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 42732ee..3910845 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Options.java
@@ -28,6 +28,7 @@
 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.util.tools.ArgHandler;
 import com.google.gwt.util.tools.ArgHandlerDir;
 import com.google.gwt.util.tools.ArgHandlerExtra;
@@ -40,7 +41,9 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Defines the command-line options for the {@link CodeServer CodeServer's} main() method.
@@ -49,6 +52,7 @@
  */
 public class Options {
   private ImmutableList<String> args;
+  private Set<String> tags = new LinkedHashSet<String>();
 
   private boolean compilePerFile = false;
   private boolean noPrecompile = false;
@@ -103,10 +107,36 @@
       noPrecompile = true;
     }
 
+    // Set some tags automatically for migration tracking.
+    if (shouldCompilePerFile()) {
+      addTags("incremental_on");
+    } else {
+      addTags("incremental_off");
+    }
+
+    if (getNoPrecompile()) {
+      addTags("precompile_off");
+    } else {
+      addTags("precompile_on");
+    }
+
     return true;
   }
 
   /**
+   * Adds some user-defined tags that will be passed through to {@link JobEvent#getTags}.
+   *
+   * <p>A tag may not be null, contain whitespace, or be more than 100 characters.
+   * If a tag was already added, it won't be added again.
+   *
+   * <p>This method may be called more than once, but compile jobs that are already running
+   * will not have the new tags.
+   */
+  public synchronized void addTags(String... tags) {
+    this.tags.addAll(JobEvent.checkTags(Arrays.asList(tags)));
+  }
+
+  /**
    * Returns the arguments passed to {@link #parseArgs}.
    */
   ImmutableList<String> getArgs() {
@@ -114,6 +144,13 @@
   }
 
   /**
+   * Returns the tags passed to {@link #addTags}.
+   */
+  synchronized Set<String> getTags() {
+    return ImmutableSet.copyOf(tags);
+  }
+
+  /**
    * A Java application that embeds Super Dev Mode can use this hook to find out
    * when compiles start and end.
    *