/* * 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); } } }