JobEvent.java

/*
 * 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.codeserver;

import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefSchema;
import com.google.gwt.dev.util.log.MetricName;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSortedMap;

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.
 *
 * <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;

  private final String inputModuleName;
  private final ImmutableSortedMap<String, String> bindings;
  private final Status status;
  private final String message;

  private final String outputModuleName;
  private final CompileDir compileDir;
  private final CompileStrategy compileStrategy;
  private final ImmutableList<String> arguments;
  private final ImmutableList<String> tags;
  private final ImmutableSortedMap<String, Long> metricMap;

  private JobEvent(Builder builder) {
    this.jobId = Preconditions.checkNotNull(builder.jobId);
    this.inputModuleName = Preconditions.checkNotNull(builder.inputModuleName);
    this.bindings = ImmutableSortedMap.copyOf(builder.bindings);
    this.status = Preconditions.checkNotNull(builder.status);
    this.message = builder.message == null ? status.defaultMessage : builder.message;

    this.arguments = ImmutableList.copyOf(builder.args);
    this.tags = ImmutableList.copyOf(builder.tags);
    this.metricMap = ImmutableSortedMap.copyOf(builder.metricMap);

    // The following fields may be null.
    this.outputModuleName = builder.outputModuleName;
    this.compileDir = builder.compileDir;
    this.compileStrategy = builder.compileStrategy;

    // Any new fields added should allow nulls for backward compatibility.
  }

  /**
   * The id of the job being compiled. Unique within the same CodeServer process.
   * This should be considered an opaque string.
   */
  public String getJobId() {
    return jobId;
  }

  /**
   * The module name sent to the GWT compiler to start the compile.
   */
  public String getInputModuleName() {
    return inputModuleName;
  }

  /**
   * The binding properties sent to the GWT compiler.
   */
  public SortedMap<String, String> getBindings() {
    // Can't return ImmutableSortedMap here because it's repackaged and this is a public API.
    return bindings;
  }

  /**
   * The last reported status of the job.
   */
  public Status getStatus() {
    return status;
  }

  /**
   * Returns a line of text describing the job's current status.
   */
  public String getMessage() {
    return message;
  }

  /**
   * Returns the module name used for the output of the GWT compiler, or null if not available.
   * (This name is used for the name of the module directory, the nocache.js file, and in all
   * JavaScript generated by the GWT compiler.)
   * Only available for completed, successful compiles.
   */
  public String getOutputModuleName() {
    return outputModuleName;
  }

  /**
   * Returns the directory where the GWT module is being compiled, or null if not available.
   * (Not available for jobs that are WAITING.)
   */
  public CompileDir getCompileDir() {
    return compileDir;
  }

  /**
   * Returns the strategy used to perform the compile or null if not available.
   * (Normally available for finished compiles.)
   */
  public CompileStrategy getCompileStrategy() {
    return compileStrategy;
  }

  /**
   * The arguments passed to Super Dev Mode at startup, or null if not available.
   */
  public List<String> getArguments() {
    return arguments;
  }

  /**
   * User-defined tags associated with this job. (Not null but may be empty.)
   */
  public List<String> getTags() { return tags; }

  /**
   * Returns the amounts of performance-related metrics. (Not null but may be empty.)
   * The keys are subject to change.
   */
  public SortedMap<String, Long> getMetricMap() {
    // can't return ImmutableSortedMap because it's repackaged
    return metricMap;
  }

  /**
   * 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 {
    WAITING("waiting", "Waiting for the compiler to start"),
    COMPILING("compiling", "Compiling"),
    SERVING("serving", "Compiled output is ready"),
    GONE("gone", "Compiled output is no longer available"),
    ERROR("error", "Compile failed with an error");

    final String jsonName;
    final String defaultMessage;

    Status(String jsonName, String defaultMessage) {
      this.jsonName = jsonName;
      this.defaultMessage = defaultMessage;
    }
  }

  /**
   * The approach taken to do the compile.
   */
  public enum CompileStrategy {
    FULL("full"), // Compiled all the source.
    INCREMENTAL("incremental"), // Only recompiled the source files that changed.
    SKIPPED("skipped"); // Did not compile anything since nothing changed

    final String jsonName;

    CompileStrategy(String jsonName) {
      this.jsonName = jsonName;
    }

    /**
     * The string to use for serialization.
     */
    public String getJsonName() {
      return jsonName;
    }
  }

  /**
   * Creates a JobEvent.
   * This is public to allow external tests of code that implements {@link JobChangeListener}.
   * Normally all JobEvents are created in the code server.
   */
  public static class Builder {
    private String jobId;

    private String inputModuleName;
    private Map<String, String> bindings = ImmutableMap.of();
    private Status status;
    private String message;

    private List<String> args = ImmutableList.of();
    private List<String> tags = ImmutableList.of();
    private Map<String, Long> metricMap = ImmutableMap.of();

    private String outputModuleName;
    private CompileDir compileDir;
    private CompileStrategy compileStrategy;

    /**
     * A unique id for this job. Required.
     */
    public void setJobId(String jobId) {
      Preconditions.checkArgument(Job.isValidJobId(jobId), "invalid job id: " + jobId);
      this.jobId = jobId;
    }

    /**
     * The name of the module as passed to the compiler. Required.
     */
    public void setInputModuleName(String inputModuleName) {
      Preconditions.checkArgument(ModuleDef.isValidModuleName(inputModuleName),
          "invalid module name: " + jobId);
      this.inputModuleName = inputModuleName;
    }

    /**
     * The bindings passed to the compiler.
     * Optional, but may not be null. (Defaults to the empty map.)
     */
    public void setBindings(Map<String, String> bindings) {
      for (String name : bindings.keySet()) {
        if (!ModuleDefSchema.isValidPropertyName(name)) {
          throw new IllegalArgumentException("invalid property name: " + name);
        }
      }
      this.bindings = bindings;
    }

    /**
     * The job's current status. Required.
     */
    public void setStatus(Status status) {
      this.status = status;
    }

    /**
     * A message to describing the job's current state.
     * It should be a single line of text.
     * Optional. If null, a default message will be used.
     */
    public void setMessage(String message) {
      if (message != null) {
        Preconditions.checkArgument(!message.contains("\n"),
            "JobEvent messages should be a single line of text");
      }
      this.message = message;
    }

    /**
     * The module name that the compiler returned (after rename).
     */
    public void setOutputModuleName(String name) {
      this.outputModuleName = name;
    }

    /**
     * The directory where the GWT compiler will write its output.
     * Optional. (Not available until the compile starts.)
     */
    public void setCompileDir(CompileDir compileDir) {
      this.compileDir = compileDir;
    }

    /**
     * The strategy used to perform the compile.
     * Optional.
     */
    public void setCompileStrategy(CompileStrategy compileStrategy) {
      this.compileStrategy = compileStrategy;
    }

    /**
     * The arguments passed to {@link Options#parseArgs} at startup.
     * Optional but may not be null. If not set, defaults to the empty list.
     */
    public void setArguments(List<String> args) {
      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);
    }

    /**
     * Sets a map containing metrics used for understanding compiler performance.
     * Optional but may not be null. If not set, defaults to the empty map.
     * Each key must be a valid identifier beginning with a capital letter.
     * (This constraint may be relaxed later.)
     */
    public void setMetricMap(Map<String, Long> nameToMetric) {
      for (String key : nameToMetric.keySet()) {
        // TODO: allow '.' in the key after making an API for that.
        Preconditions.checkArgument(MetricName.isValidKey(key),
            "invalid counter key: %s", key);
      }
      this.metricMap = nameToMetric;
    }

    public JobEvent build() {
      return new JobEvent(this);
    }
  }
}