Adds --artifact option to workspace generator for generating workspace and
build file rules from specific maven artifact coordinates.

RELNOTES: Added --artifact to workspace generator for generating workspace and
build file rules from artifact coodrinates.

--
MOS_MIGRATED_REVID=115001893
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java
index e06f067..84bbc0b 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java
@@ -23,6 +23,7 @@
 import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.UnixFileSystem;
+import com.google.devtools.build.workspace.maven.Resolver;
 import com.google.devtools.common.options.OptionsParser;
 
 import java.io.File;
@@ -41,14 +42,16 @@
 
   private final EventHandler handler;
   private final FileSystem fileSystem;
-  private final com.google.devtools.build.workspace.maven.Resolver resolver;
+  private final Resolver resolver;
   private final Path outputDir;
 
   public static void main(String[] args) {
     OptionsParser parser = OptionsParser.newOptionsParser(GenerateWorkspaceOptions.class);
     parser.parseAndExitUponError(args);
     GenerateWorkspaceOptions options = parser.getOptions(GenerateWorkspaceOptions.class);
-    if (options.mavenProjects.isEmpty() && options.bazelProjects.isEmpty()) {
+    if (options.mavenProjects.isEmpty()
+        && options.bazelProjects.isEmpty()
+        && options.artifacts.isEmpty()) {
       printUsage(parser);
       return;
     }
@@ -56,6 +59,7 @@
     GenerateWorkspace workspaceFileGenerator = new GenerateWorkspace(options.outputDir);
     workspaceFileGenerator.generateFromWorkspace(options.bazelProjects);
     workspaceFileGenerator.generateFromPom(options.mavenProjects);
+    workspaceFileGenerator.generateFromArtifacts(options.artifacts);
     if (!workspaceFileGenerator.hasErrors()) {
       workspaceFileGenerator.writeResults();
     }
@@ -66,11 +70,11 @@
   }
 
   private static void printUsage(OptionsParser parser) {
-    System.out.println("Usage: generate_workspace (-b PATH|-m PATH)+ [-o PATH]\n\n"
+    System.out.println("Usage: generate_workspace (-b PATH|-m PATH|-a coord)+ [-o PATH]\n\n"
         + "Generates a WORKSPACE file from the given projects and a BUILD file with a rule that "
-        + "contains all of the transitive dependencies. At least one bazel_project or "
-        + "maven_project must be specified. If output_dir is not specified, the generated files "
-        + "will be written to a temporary directory.\n");
+        + "contains all of the transitive dependencies. At least one bazel_project, "
+        + "maven_project, or artifact coordinate must be specified. If output_dir is not "
+        + "specified, the generated files will be written to a temporary directory.\n");
     System.out.println(parser.describeOptions(Collections.<String, String>emptyMap(),
         OptionsParser.HelpVerbosity.LONG));
   }
@@ -78,7 +82,7 @@
   private GenerateWorkspace(String outputDir) {
     this.handler = new EventHandler();
     this.fileSystem = getFileSystem();
-    this.resolver = new com.google.devtools.build.workspace.maven.Resolver(handler);
+    this.resolver = new Resolver(handler);
     if (outputDir.isEmpty()) {
       this.outputDir = fileSystem.getPath(Files.createTempDir().toString());
     } else {
@@ -93,7 +97,7 @@
 
   private void generateFromWorkspace(List<String> projects) {
     for (String project : projects) {
-      Resolver workspaceResolver = new Resolver(resolver, handler);
+      WorkspaceResolver workspaceResolver = new WorkspaceResolver(resolver, handler);
       Path projectPath = fileSystem.getPath(getAbsolute(project));
       Package externalPackage = workspaceResolver.parse(projectPath.getRelative("WORKSPACE"));
       workspaceResolver.resolveTransitiveDependencies(externalPackage);
@@ -106,6 +110,12 @@
     }
   }
 
+  private void generateFromArtifacts(List<String> artifacts) {
+    for (String artifactCoord : artifacts) {
+      resolver.resolveArtifact(artifactCoord);
+    }
+  }
+
   private String getAbsolute(String path) {
     return Paths.get(System.getProperty("user.dir")).resolve(path).toString();
   }
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java
index 935df95..7e9011b 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java
@@ -36,6 +36,7 @@
       abbrev = 'b',
       help = "Directory contains a Bazel project.",
       allowMultiple = true,
+      category = "input",
       defaultValue = ""
   )
   public List<String> bazelProjects;
@@ -45,15 +46,27 @@
       abbrev = 'm',
       help = "Directory containing a Maven project.",
       allowMultiple = true,
+      category = "input",
       defaultValue = ""
   )
   public List<String> mavenProjects;
 
   @Option(
+      name = "artifact",
+      abbrev = 'a',
+      help = "Maven artifact coordinates (e.g. groupId:artifactId:version).",
+      allowMultiple = true,
+      category = "input",
+      defaultValue = ""
+  )
+  public List<String> artifacts;
+  
+  @Option(
       name = "output_dir",
       abbrev = 'o',
       help = "Output directory to store the WORKSPACE and BUILD files. If unspecified, a temporary"
           + " directory is used.",
+      category = "output",
       defaultValue = ""
   )
   public String outputDir;
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/Resolver.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/WorkspaceResolver.java
similarity index 93%
rename from src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/Resolver.java
rename to src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/WorkspaceResolver.java
index 2d95c4b..1adadb8 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/Resolver.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/WorkspaceResolver.java
@@ -37,6 +37,8 @@
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.workspace.maven.DefaultModelResolver;
+import com.google.devtools.build.workspace.maven.Resolver;
+import com.google.devtools.build.workspace.maven.Resolver.InvalidArtifactCoordinateException;
 import com.google.devtools.build.workspace.maven.Rule;
 
 import org.apache.maven.model.building.ModelSource;
@@ -48,14 +50,14 @@
 /**
  * Finds the transitive dependencies of a WORKSPACE file.
  */
-public class Resolver {
+public class WorkspaceResolver {
 
   private final RuleClassProvider ruleClassProvider;
   private final ImmutableList<EnvironmentExtension> environmentExtensions;
   private final EventHandler handler;
   private final com.google.devtools.build.workspace.maven.Resolver resolver;
 
-  Resolver(com.google.devtools.build.workspace.maven.Resolver resolver, EventHandler handler) {
+  WorkspaceResolver(Resolver resolver, EventHandler handler) {
     this.resolver = resolver;
     this.handler = handler;
     ConfiguredRuleClassProvider.Builder ruleClassBuilder =
@@ -110,8 +112,8 @@
         AttributeMap attributeMap = AggregatingAttributeMapper.of(workspaceRule);
         Rule rule;
         try {
-          rule = new Rule(attributeMap.get("artifact", Type.STRING));
-        } catch (Rule.InvalidRuleException e) {
+          rule = new Rule(Resolver.getArtifact(attributeMap.get("artifact", Type.STRING)));
+        } catch (InvalidArtifactCoordinateException e) {
           handler.handle(Event.error(location, "Couldn't get attribute: " + e.getMessage()));
           return;
         }
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/BUILD b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/BUILD
index 94517fd..be51073 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/BUILD
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/BUILD
@@ -17,6 +17,7 @@
         ":rule",
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:maven-connector",
+        "//third_party:aether",
         "//third_party:guava",
         "//third_party:jsr305",
         "//third_party:maven_model",
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/DefaultModelResolver.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/DefaultModelResolver.java
index 395d619..d00d714 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/DefaultModelResolver.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/DefaultModelResolver.java
@@ -26,6 +26,7 @@
 import org.apache.maven.model.building.UrlModelSource;
 import org.apache.maven.model.resolution.ModelResolver;
 import org.apache.maven.model.resolution.UnresolvableModelException;
+import org.eclipse.aether.artifact.Artifact;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
@@ -42,26 +43,30 @@
 public class DefaultModelResolver implements ModelResolver {
 
   private final Set<Repository> repositories;
-  private final Map<String, ModelSource> artifactToUrl;
+  private final Map<String, ModelSource> ruleNameToModelSource;
 
   public DefaultModelResolver() {
     repositories = Sets.newHashSet();
     repositories.add(MavenConnector.getMavenCentral());
-    artifactToUrl = Maps.newHashMap();
+    ruleNameToModelSource = Maps.newHashMap();
   }
 
   private DefaultModelResolver(
-      Set<Repository> repositories, Map<String, ModelSource> artifactToRepository) {
+      Set<Repository> repositories, Map<String, ModelSource> ruleNameToModelSource) {
     this.repositories = repositories;
-    this.artifactToUrl = artifactToRepository;
+    this.ruleNameToModelSource = ruleNameToModelSource;
   }
 
+  public ModelSource resolveModel(Artifact artifact) throws UnresolvableModelException {
+    return resolveModel(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
+  }
+  
   @Override
   public ModelSource resolveModel(String groupId, String artifactId, String version)
       throws UnresolvableModelException {
-    String artifact = Rule.name(groupId, artifactId);
-    if (artifactToUrl.containsKey(artifact)) {
-      return artifactToUrl.get(artifact);
+    String ruleName = Rule.name(groupId, artifactId);
+    if (ruleNameToModelSource.containsKey(ruleName)) {
+      return ruleNameToModelSource.get(ruleName);
     }
     for (Repository repository : repositories) {
       UrlModelSource modelSource = getModelSource(
@@ -72,13 +77,13 @@
     }
 
     // TODO(kchodorow): use Java 8 features to make this a one-liner.
-    List<String> urls = Lists.newArrayList();
+    List<String> attemptedUrls = Lists.newArrayList();
     for (Repository repository : repositories) {
-      urls.add(repository.getUrl());
+      attemptedUrls.add(repository.getUrl());
     }
     throw new UnresolvableModelException("Could not find any repositories that knew how to "
         + "resolve " + groupId + ":" + artifactId + ":" + version + " (checked "
-        + Joiner.on(", ").join(urls) + ")", groupId, artifactId, version);
+        + Joiner.on(", ").join(attemptedUrls) + ")", groupId, artifactId, version);
   }
 
   // TODO(kchodorow): make this work with local repositories.
@@ -94,7 +99,7 @@
           + "-" + version + ".pom");
       if (pomFileExists(urlUrl)) {
         UrlModelSource urlModelSource = new UrlModelSource(urlUrl);
-        artifactToUrl.put(Rule.name(groupId, artifactId), urlModelSource);
+        ruleNameToModelSource.put(Rule.name(groupId, artifactId), urlModelSource);
         return urlModelSource;
       }
     } catch (MalformedURLException e) {
@@ -121,22 +126,26 @@
     return false;
   }
 
+  // For compatibility with older versions of ModelResolver which don't have this method,
+  // don't add @Override.
   public ModelSource resolveModel(Parent parent) throws UnresolvableModelException {
     return resolveModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
   }
 
-  @Override
+  // For compatibility with older versions of ModelResolver which don't have this method,
+  // don't add @Override.
   public void addRepository(Repository repository) {
     repositories.add(repository);
   }
 
+  @Override
   public void addRepository(Repository repository, boolean replace) {
     addRepository(repository);
   }
 
   @Override
   public ModelResolver newCopy() {
-    return new DefaultModelResolver(repositories, artifactToUrl);
+    return new DefaultModelResolver(repositories, ruleNameToModelSource);
   }
 
   /**
@@ -152,8 +161,8 @@
 
   public boolean putModelSource(String groupId, String artifactId, ModelSource modelSource) {
     String key = Rule.name(groupId, artifactId);
-    if (!artifactToUrl.containsKey(key)) {
-      artifactToUrl.put(key, modelSource);
+    if (!ruleNameToModelSource.containsKey(key)) {
+      ruleNameToModelSource.put(key, modelSource);
       return true;
     }
     return false;
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
index bbc9e52..d379dec 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
 
+import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Model;
 import org.apache.maven.model.Parent;
 import org.apache.maven.model.Repository;
@@ -40,6 +41,8 @@
 import org.apache.maven.model.plugin.DefaultPluginConfigurationExpander;
 import org.apache.maven.model.profile.DefaultProfileSelector;
 import org.apache.maven.model.resolution.UnresolvableModelException;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
 
 import java.io.File;
 import java.io.IOException;
@@ -59,6 +62,31 @@
  * Resolves Maven dependencies.
  */
 public class Resolver {
+
+  /**
+   * Exception thrown if an artifact coordinate could not be parsed.
+   */
+  public static class InvalidArtifactCoordinateException extends Exception {
+    InvalidArtifactCoordinateException(String message) {
+      super(message);
+    }
+  }
+  
+  public static Artifact getArtifact(String atrifactCoords)
+      throws InvalidArtifactCoordinateException {
+    try {
+      return new DefaultArtifact(atrifactCoords);
+    } catch (IllegalArgumentException e) {
+      throw new InvalidArtifactCoordinateException(e.getMessage());
+    }
+  }
+
+  public static Artifact getArtifact(Dependency dependency)
+      throws InvalidArtifactCoordinateException {
+    return getArtifact(dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
+        + dependency.getVersion());
+  }
+  
   private static final String COMPILE_SCOPE = "compile";
 
   private final EventHandler handler;
@@ -110,6 +138,7 @@
       }
       outputStream.println("    ],");
       outputStream.println(")");
+      outputStream.println();
     }
   }
 
@@ -148,6 +177,27 @@
   }
 
   /**
+   * Resolves an artifact as a root of a dependency graph.
+   */
+  public void resolveArtifact(String artifactCoord) {
+    Artifact artifact;
+    ModelSource modelSource;
+    try {
+      artifact = getArtifact(artifactCoord);
+      modelSource = modelResolver.resolveModel(artifact);
+    } catch (UnresolvableModelException | InvalidArtifactCoordinateException e) {
+      handler.handle(Event.error(e.getMessage()));
+      return;
+    }
+
+    addHeader(artifactCoord);
+    Rule rule = new Rule(artifact);
+    addRootDependency(rule);
+    deps.put(rule.name(), rule); // add the artifact rule to the workspace 
+    resolveEffectiveModel(modelSource, Sets.<String>newHashSet(), rule);
+  }
+  
+  /**
    * Resolves all dependencies from a given "model source," which could be either a URL or a local
    * file.
    * @return the model.
@@ -171,7 +221,7 @@
       modelResolver.addRepository(repo);
     }
 
-    for (org.apache.maven.model.Dependency dependency : model.getDependencies()) {
+    for (Dependency dependency : model.getDependencies()) {
       if (!dependency.getScope().equals(COMPILE_SCOPE)) {
         continue;
       }
@@ -182,7 +232,7 @@
         continue;
       }
       try {
-        Rule artifactRule = new Rule(dependency);
+        Rule artifactRule = new Rule(getArtifact(dependency), dependency.getExclusions());
         HashSet<String> localDepExclusions = new HashSet<>(exclusions);
         localDepExclusions.addAll(artifactRule.getExclusions());
 
@@ -205,7 +255,7 @@
         } else {
           rootDependencies.add(artifactRule);
         }
-      } catch (UnresolvableModelException | Rule.InvalidRuleException e) {
+      } catch (UnresolvableModelException | InvalidArtifactCoordinateException e) {
         handler.handle(Event.error("Could not resolve dependency " + dependency.getGroupId()
             + ":" + dependency.getArtifactId() + ":" + dependency.getVersion() + ": "
             + e.getMessage()));
diff --git a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Rule.java b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
index 8f14523..13698f3 100644
--- a/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
+++ b/src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
@@ -20,11 +20,10 @@
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
 
-import org.apache.maven.model.Dependency;
 import org.apache.maven.model.Exclusion;
 import org.eclipse.aether.artifact.Artifact;
-import org.eclipse.aether.artifact.DefaultArtifact;
 
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
@@ -32,6 +31,7 @@
  * A struct representing the fields of maven_jar to be written to the WORKSPACE file.
  */
 public final class Rule implements Comparable<Rule> {
+
   private final Artifact artifact;
   private final Set<String> parents;
   private final Set<String> exclusions;
@@ -39,24 +39,20 @@
   private String repository;
   private String sha1;
 
-  public Rule(String artifactStr) throws InvalidRuleException {
-    try {
-      this.artifact = new DefaultArtifact(artifactStr);
-    } catch (IllegalArgumentException e) {
-      throw new InvalidRuleException(e.getMessage());
-    }
+  public Rule(Artifact artifact) {
+    this.artifact = artifact;
     this.parents = Sets.newHashSet();
     this.dependencies = Sets.newTreeSet();
     this.exclusions = Sets.newHashSet();
     this.repository = MavenConnector.MAVEN_CENTRAL_URL;
   }
 
-  public Rule(Dependency dependency) throws InvalidRuleException {
-    this(dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
-        + dependency.getVersion());
+  public Rule(Artifact artifact, List<Exclusion> exclusions) {
+    this(artifact);
 
-    for (Exclusion exclusion : dependency.getExclusions()) {
-      exclusions.add(String.format("%s:%s", exclusion.getGroupId(), exclusion.getArtifactId()));
+    for (Exclusion exclusion : exclusions) {
+      String coord = String.format("%s:%s", exclusion.getGroupId(), exclusion.getArtifactId());
+      this.exclusions.add(coord);
     }
   }
 
@@ -190,13 +186,4 @@
   public int compareTo(Rule o) {
     return name().compareTo(o.name());
   }
-
-  /**
-   * Exception thrown if the rule could not be created.
-   */
-  public static class InvalidRuleException extends Exception {
-    InvalidRuleException(String message) {
-      super(message);
-    }
-  }
 }
diff --git a/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/BUILD b/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/BUILD
index 919502f..edce307 100644
--- a/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/BUILD
+++ b/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/BUILD
@@ -3,6 +3,7 @@
     srcs = ["RuleTest.java"],
     deps = [
         "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven",
         "//src/tools/generate_workspace/src/main/java/com/google/devtools/build/workspace/maven:rule",
         "//third_party:junit4",
         "//third_party:truth",
diff --git a/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/RuleTest.java b/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/RuleTest.java
index 18cd687..2782aba 100644
--- a/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/RuleTest.java
+++ b/src/tools/generate_workspace/src/test/java/com/google/devtools/build/workspace/maven/RuleTest.java
@@ -38,7 +38,7 @@
 
   @Test
   public void testUrl() throws Exception {
-    Rule rule = new Rule("foo:bar:1.2.3");
+    Rule rule = new Rule(Resolver.getArtifact("foo:bar:1.2.3"));
     assertThat(rule.getUrl())
         .isEqualTo("https://repo1.maven.org/maven2/foo/bar/1.2.3/bar-1.2.3.pom");
     rule.setRepository("http://myrepo.com/foo/bar/1.2.3/bar-1.2.3.pom", handler);