Make generate_workspace dump transitive dependencies for Maven artifacts

This moves the WORKSPACE-parsing code out of WorkspaceFileFunction.java so
generate_workspace can parse a WORKSPACE into an ExternalPackage without having
to muck with Skyframe.

Addresses issue #89.

--
MOS_MIGRATED_REVID=96777708
diff --git a/site/docs/external.md b/site/docs/external.md
index 9527898..a6e57ed 100644
--- a/site/docs/external.md
+++ b/site/docs/external.md
@@ -29,23 +29,51 @@
 file size, but hopefully limits the chances of having one library include _C_
 at version 1.0 and another include _C_ at 2.0.
 
-# Converting existing projects
+Bazel provides a tool to help generate these expansive _WORKSPACE_ files, called
+`generate_workspace`. Run the following to build the tool and see usage:
 
-To convert a Maven project, first run the `generate_workspace` tool:
-
-```bash
-$ bazel run src/main/java/com/google/devtools/build/workspace:generate_workspace /path/to/your/maven/project >> WORKSPACE
+```
+bazel run src/main/java/com/google/devtools/build/workspace:generate_workspace
 ```
 
-This will parse the _pom.xml_ file and discover project dependencies. All of
-these dependencies will be written in
-[`maven_jar`](http://bazel.io/docs/build-encyclopedia.html#maven_jar) format to
-stdout, which can be redirected or copied to the _WORKSPACE_ file.
+You can either specify directories containing Bazel projects (i.e., _WORKSPACE_
+files) or Maven projects (i.e., _pom.xml_ files).  For example:
 
-At the moment, `generate_workspace` will only include direct dependencies.
+```bash
+$ bazel run src/main/java/com/google/devtools/build/workspace:generate_workspace \
+>    --maven_project=/path/to/my/project \
+>    --bazel_project=/path/to/skunkworks \
+>    --bazel_project=/path/to/teleporter/project
+# --------------------
+# The following dependencies were calculated from:
+# /path/to/my/project/pom.xml
+# /path/to/skunkworks/WORKSPACE
+# /path/to/teleporter/project/WORKSPACE
 
-You will still need to manually add these libraries as dependencies of your
-`java_` targets.
+
+# com.example.some-project:a:1.2.3
+# com.example.another-project:b:3.2.1 wanted version 2.4
+maven_jar(
+    name = "javax/servlet/servlet-api",
+    artifact = "javax.servlet:servlet-api:2.5",
+)
+
+[Other dependencies]
+# --------------------
+
+WARNING /path/to/my/project/pom.xml:1: javax.servlet:servlet-api already processed for version 2.5 but com.example.another-project:b:3.2.1 wants version 2.4, ignoring.
+```
+Everything after the second `--------------------` is printed to stderr, not
+stdout. This is where any errors or warnings are printed. You may need to edit
+the versions that `generate_workspace` automatically chooses for the artifacts.
+
+If you specify multiple Bazel or Maven projects, they will all be combined into
+one _WORKSPACE_ file (e.g., if the Bazel project depends on junit and the Maven
+project also depends on junit, junit will only appear once as a dependency in
+the output).
+
+Once these `maven_jar`s have been added to your _WORKSPACE_ file, you will still
+need to add the jars as dependencies of your `java_` targets in _BUILD_ files.
 
 # Types of external dependencies
 
diff --git a/src/main/java/BUILD b/src/main/java/BUILD
index 561d4ae..aa8a0f6 100644
--- a/src/main/java/BUILD
+++ b/src/main/java/BUILD
@@ -288,7 +288,10 @@
             "com/google/devtools/build/lib/server/**/*.java",
             "com/google/devtools/build/lib/standalone/*.java",
             "com/google/devtools/build/lib/webstatusserver/**/*.java",
-            "com/google/devtools/build/lib/worker/*.java",
+            "com/google/devtools/build/lib/worker/**",
+        ],
+        exclude = [
+            "com/google/devtools/build/lib/bazel/repository/MavenConnector.java",
         ],
     ),
     resources = glob([
@@ -301,7 +304,10 @@
         "//third_party:d3-js",
         "//third_party:jquery",
     ],
-    visibility = ["//src/test/java:__subpackages__"],
+    visibility = [
+        "//src/main/java/com/google/devtools/build/workspace:__pkg__",
+        "//src/test/java:__subpackages__",
+    ],
     runtime_deps = [
         "//third_party:aether",
         "//third_party:apache_commons_logging",
@@ -320,6 +326,7 @@
         ":events",
         ":genquery",
         ":graph",
+        ":maven-connector",
         ":options",
         ":packages",
         ":query2",
@@ -347,6 +354,15 @@
 )
 
 java_library(
+    name = "maven-connector",
+    srcs = ["com/google/devtools/build/lib/bazel/repository/MavenConnector.java"],
+    deps = [
+        "//third_party:aether",
+        "//third_party:maven_model",
+    ],
+)
+
+java_library(
     name = "java-toolchain-parser",
     testonly = 1,
     srcs = glob([
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java
index 89840d9..318e6ac 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelMain.java
@@ -30,7 +30,7 @@
 public final class BazelMain {
   private static final String BUILD_DATA_PROPERTIES = "/build-data.properties";
 
-  private static final List<Class<? extends BlazeModule>> BAZEL_MODULES = ImmutableList.of(
+  public static final List<Class<? extends BlazeModule>> BAZEL_MODULES = ImmutableList.of(
       com.google.devtools.build.lib.bazel.BazelShutdownLoggerModule.class,
       com.google.devtools.build.lib.bazel.BazelWorkspaceStatusModule.class,
       com.google.devtools.build.lib.bazel.BazelDiffAwarenessModule.class,
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenConnector.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenConnector.java
new file mode 100644
index 0000000..60addc6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenConnector.java
@@ -0,0 +1,68 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// 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.devtools.build.lib.bazel.repository;
+
+import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
+import org.eclipse.aether.AbstractRepositoryListener;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
+import org.eclipse.aether.impl.DefaultServiceLocator;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
+import org.eclipse.aether.spi.connector.transport.TransporterFactory;
+import org.eclipse.aether.transfer.AbstractTransferListener;
+import org.eclipse.aether.transport.file.FileTransporterFactory;
+import org.eclipse.aether.transport.http.HttpTransporterFactory;
+
+/**
+ * Connections to Maven repositories.
+ */
+public class MavenConnector {
+  private static final String MAVEN_CENTRAL_URL = "http://central.maven.org/maven2/";
+
+  private final String localRepositoryPath;
+
+  public MavenConnector(String localRepositoryPath) {
+    this.localRepositoryPath = localRepositoryPath;
+  }
+
+  public RepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
+    DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
+    LocalRepository localRepo = new LocalRepository(localRepositoryPath);
+    session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
+    session.setTransferListener(new AbstractTransferListener() {});
+    session.setRepositoryListener(new AbstractRepositoryListener() {});
+    return session;
+  }
+
+  public RepositorySystem newRepositorySystem() {
+    DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
+    locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
+    locator.addService(TransporterFactory.class, FileTransporterFactory.class);
+    locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
+    return locator.getService(RepositorySystem.class);
+  }
+
+  /**
+   * How is this not a built-in for aether?
+   */
+  public static RemoteRepository getMavenCentral() {
+    return new RemoteRepository.Builder(
+        "central", "default", MAVEN_CENTRAL_URL).build();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
index 8af654f..fce3328 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenJarFunction.java
@@ -36,25 +36,14 @@
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 
-import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
-import org.eclipse.aether.AbstractRepositoryListener;
-import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
-import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
-import org.eclipse.aether.impl.DefaultServiceLocator;
-import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ArtifactRequest;
 import org.eclipse.aether.resolution.ArtifactResolutionException;
 import org.eclipse.aether.resolution.ArtifactResult;
-import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
-import org.eclipse.aether.spi.connector.transport.TransporterFactory;
-import org.eclipse.aether.transfer.AbstractTransferListener;
-import org.eclipse.aether.transport.file.FileTransporterFactory;
-import org.eclipse.aether.transport.http.HttpTransporterFactory;
 
 import java.io.IOException;
 import java.util.List;
@@ -82,8 +71,7 @@
   MavenDownloader createMavenDownloader(AttributeMap mapper) {
     String name = mapper.getName();
     Path outputDirectory = getExternalRepositoryDirectory().getRelative(name);
-    MavenDownloader downloader = new MavenDownloader(name, mapper, outputDirectory);
-    return downloader;
+    return new MavenDownloader(name, mapper, outputDirectory);
   }
 
   SkyValue createOutputTree(MavenDownloader downloader, Environment env)
@@ -93,7 +81,7 @@
       return null;
     }
 
-    Path repositoryJar = null;
+    Path repositoryJar;
     try {
       repositoryJar = downloader.download();
     } catch (IOException e) {
@@ -136,8 +124,6 @@
    * This downloader creates a connection to one or more Maven repositories and downloads a jar.
    */
   static class MavenDownloader {
-    private static final String MAVEN_CENTRAL_URL = "http://central.maven.org/maven2/";
-
     private final String name;
     private final String artifact;
     private final Path outputDirectory;
@@ -172,9 +158,7 @@
               "user-defined repository " + repositories.size(), "default", repositoryUrl).build());
         }
       } else {
-        this.repositories = Lists.newArrayList();
-        this.repositories.add(new RemoteRepository.Builder(
-            "central", "default", MAVEN_CENTRAL_URL).build());
+        this.repositories = ImmutableList.of(MavenConnector.getMavenCentral());
       }
     }
 
@@ -196,8 +180,9 @@
      * Download the Maven artifact to the output directory. Returns the path to the jar.
      */
     public Path download() throws IOException {
-      RepositorySystem system = newRepositorySystem();
-      RepositorySystemSession session = newRepositorySystemSession(system);
+      MavenConnector connector = new MavenConnector(outputDirectory.getPathString());
+      RepositorySystem system = connector.newRepositorySystem();
+      RepositorySystemSession session = connector.newRepositorySystemSession(system);
 
       ArtifactRequest artifactRequest = new ArtifactRequest();
       Artifact artifact;
@@ -228,23 +213,6 @@
       }
       return downloadPath;
     }
-
-    private RepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
-      DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
-      LocalRepository localRepo = new LocalRepository(outputDirectory.getPathString());
-      session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
-      session.setTransferListener(new AbstractTransferListener() {});
-      session.setRepositoryListener(new AbstractRepositoryListener() {});
-      return session;
-    }
-
-    private RepositorySystem newRepositorySystem() {
-      DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
-      locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
-      locator.addService(TransporterFactory.class, FileTransporterFactory.class);
-      locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
-      return locator.getService(RepositorySystem.class);
-    }
   }
 
 }
diff --git a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
new file mode 100644
index 0000000..a304c6d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
@@ -0,0 +1,151 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.devtools.build.lib.packages;
+
+import static com.google.devtools.build.lib.syntax.Environment.NONE;
+
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.ExternalPackage.Binding;
+import com.google.devtools.build.lib.packages.ExternalPackage.Builder;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.BuiltinFunction;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.FunctionSignature;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+
+import java.io.File;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Parser for WORKSPACE files.  Fills in an ExternalPackage.Builder
+ */
+public class WorkspaceFactory {
+  private final Builder builder;
+  private final Environment environment;
+
+  public WorkspaceFactory(Builder builder, RuleClassProvider ruleClassProvider) {
+    this(builder, ruleClassProvider, null);
+  }
+
+  public WorkspaceFactory(
+      Builder builder, RuleClassProvider ruleClassProvider, @Nullable String installDir) {
+    this.builder = builder;
+    this.environment = createWorkspaceEnv(builder, ruleClassProvider, installDir);
+  }
+
+  public void parse(ParserInputSource source)
+      throws InterruptedException {
+    StoredEventHandler localReporter = new StoredEventHandler();
+    BuildFileAST buildFileAST;
+    buildFileAST = BuildFileAST.parseBuildFile(source, localReporter, null, false);
+    if (buildFileAST.containsErrors()) {
+      localReporter.handle(Event.error("WORKSPACE file could not be parsed"));
+    } else {
+      if (!buildFileAST.exec(environment, localReporter)) {
+        localReporter.handle(Event.error("Error evaluating WORKSPACE file " + source.getPath()));
+      }
+    }
+
+    builder.addEvents(localReporter.getEvents());
+    if (localReporter.hasErrors()) {
+      builder.setContainsErrors();
+    }
+  }
+
+  // TODO(bazel-team): use @SkylarkSignature annotations on a BuiltinFunction.Factory
+  // for signature + documentation of this and other functions in this file.
+  private static BuiltinFunction newWorkspaceNameFunction(final Builder builder) {
+    return new BuiltinFunction("workspace",
+        FunctionSignature.namedOnly("name"), BuiltinFunction.USE_LOC) {
+      public Object invoke(String name, Location loc) throws EvalException {
+        String errorMessage = LabelValidator.validateTargetName(name);
+        if (errorMessage != null) {
+          throw new EvalException(loc, errorMessage);
+        }
+        builder.setWorkspaceName(name);
+        return NONE;
+      }
+    };
+  }
+
+  private static BuiltinFunction newBindFunction(final Builder builder) {
+    return new BuiltinFunction("bind",
+        FunctionSignature.namedOnly("name", "actual"), BuiltinFunction.USE_LOC) {
+      public Object invoke(String name, String actual, Location loc)
+          throws EvalException, InterruptedException {
+        Label nameLabel = null;
+        try {
+          nameLabel = Label.parseAbsolute("//external:" + name);
+          builder.addBinding(nameLabel, new Binding(Label.parseAbsolute(actual), loc));
+        } catch (Label.SyntaxException e) {
+          throw new EvalException(loc, e.getMessage());
+        }
+        return NONE;
+      }
+    };
+  }
+
+  /**
+   * Returns a function-value implementing the build rule "ruleClass" (e.g. cc_library) in the
+   * specified package context.
+   */
+  private static BuiltinFunction newRuleFunction(
+      final RuleFactory ruleFactory, final Builder builder, final String ruleClassName) {
+    return new BuiltinFunction(ruleClassName,
+        FunctionSignature.KWARGS, BuiltinFunction.USE_AST) {
+      public Object invoke(Map<String, Object> kwargs, FuncallExpression ast)
+          throws EvalException {
+        try {
+          RuleClass ruleClass = ruleFactory.getRuleClass(ruleClassName);
+          builder.createAndAddRepositoryRule(ruleClass, kwargs, ast);
+        } catch (RuleFactory.InvalidRuleException | Package.NameConflictException |
+            Label.SyntaxException e) {
+          throw new EvalException(ast.getLocation(), e.getMessage());
+        }
+        return NONE;
+      }
+    };
+  }
+
+  private Environment createWorkspaceEnv(
+      Builder builder, RuleClassProvider ruleClassProvider, String installDir) {
+    Environment workspaceEnv = new Environment();
+
+    RuleFactory ruleFactory = new RuleFactory(ruleClassProvider);
+    for (String ruleClass : ruleFactory.getRuleClassNames()) {
+      BaseFunction ruleFunction = newRuleFunction(ruleFactory, builder, ruleClass);
+      workspaceEnv.update(ruleClass, ruleFunction);
+    }
+
+    if (installDir != null) {
+      workspaceEnv.update("__embedded_dir__", installDir);
+    }
+    File jreDirectory = new File(System.getProperty("java.home"));
+    workspaceEnv.update("DEFAULT_SERVER_JAVABASE", jreDirectory.getParentFile().toString());
+
+    workspaceEnv.update("bind", newBindFunction(builder));
+    workspaceEnv.update("workspace", newWorkspaceNameFunction(builder));
+    return workspaceEnv;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
index cab94da..fa770b0 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
@@ -14,30 +14,13 @@
 
 package com.google.devtools.build.lib.skyframe;
 
-import static com.google.devtools.build.lib.syntax.Environment.NONE;
-
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
-import com.google.devtools.build.lib.cmdline.LabelValidator;
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.events.Location;
-import com.google.devtools.build.lib.events.StoredEventHandler;
-import com.google.devtools.build.lib.packages.ExternalPackage.Binding;
 import com.google.devtools.build.lib.packages.ExternalPackage.Builder;
 import com.google.devtools.build.lib.packages.ExternalPackage.Builder.NoSuchBindingException;
-import com.google.devtools.build.lib.packages.Package.NameConflictException;
 import com.google.devtools.build.lib.packages.PackageFactory;
-import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClassProvider;
-import com.google.devtools.build.lib.packages.RuleFactory;
-import com.google.devtools.build.lib.packages.Type.ConversionException;
-import com.google.devtools.build.lib.syntax.BaseFunction;
-import com.google.devtools.build.lib.syntax.BuildFileAST;
-import com.google.devtools.build.lib.syntax.BuiltinFunction;
+import com.google.devtools.build.lib.packages.WorkspaceFactory;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.FuncallExpression;
-import com.google.devtools.build.lib.syntax.FunctionSignature;
-import com.google.devtools.build.lib.syntax.Label;
-import com.google.devtools.build.lib.syntax.Label.SyntaxException;
 import com.google.devtools.build.lib.syntax.ParserInputSource;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -48,17 +31,13 @@
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 
-import java.io.File;
 import java.io.IOException;
-import java.util.Map;
 
 /**
  * A SkyFunction to parse WORKSPACE files.
  */
 public class WorkspaceFileFunction implements SkyFunction {
 
-  private static final String BIND = "bind";
-
   private final PackageFactory packageFactory;
   private final Path installDir;
   private final RuleClassProvider ruleClassProvider;
@@ -83,21 +62,22 @@
 
     Path repoWorkspace = workspaceRoot.getRoot().getRelative(workspaceRoot.getRelativePath());
     Builder builder = new Builder(repoWorkspace);
-    parseWorkspaceFile(
-        ParserInputSource.create(
-            ruleClassProvider.getDefaultWorkspaceFile(), new PathFragment("DEFAULT.WORKSPACE")),
-        builder);
+    WorkspaceFactory parser = new WorkspaceFactory(
+        builder, packageFactory.getRuleClassProvider(), installDir.getPathString());
+    parser.parse(ParserInputSource.create(
+        ruleClassProvider.getDefaultWorkspaceFile(), new PathFragment("DEFAULT.WORKSPACE")));
     if (!workspaceFileValue.exists()) {
       return new PackageValue(builder.build());
     }
+
     try {
-      ParserInputSource repoWorkspaceSource = ParserInputSource.create(repoWorkspace);
-      parseWorkspaceFile(repoWorkspaceSource, builder);
+      parser.parse(ParserInputSource.create(repoWorkspace));
     } catch (IOException e) {
       throw new WorkspaceFileFunctionException(e, Transience.TRANSIENT);
     }
+
     try {
-      builder.resolveBindTargets(packageFactory.getRuleClass(BIND));
+      builder.resolveBindTargets(packageFactory.getRuleClass("bind"));
     } catch (NoSuchBindingException e) {
       throw new WorkspaceFileFunctionException(
           new EvalException(e.getLocation(), e.getMessage()));
@@ -108,108 +88,11 @@
     return new PackageValue(builder.build());
   }
 
-  private void parseWorkspaceFile(ParserInputSource source, Builder builder)
-      throws WorkspaceFileFunctionException, InterruptedException {
-    StoredEventHandler localReporter = new StoredEventHandler();
-    BuildFileAST buildFileAST;
-    buildFileAST = BuildFileAST.parseBuildFile(source, localReporter, null, false);
-    if (buildFileAST.containsErrors()) {
-      localReporter.handle(Event.error("WORKSPACE file could not be parsed"));
-    } else {
-      if (!evaluateWorkspaceFile(buildFileAST, builder, localReporter)) {
-        localReporter.handle(Event.error("Error evaluating WORKSPACE file " + source.getPath()));
-      }
-    }
-
-    builder.addEvents(localReporter.getEvents());
-    if (localReporter.hasErrors()) {
-      builder.setContainsErrors();
-    }
-  }
-
   @Override
   public String extractTag(SkyKey skyKey) {
     return null;
   }
 
-  // TODO(bazel-team): use @SkylarkSignature annotations on a BuiltinFunction.Factory
-  // for signature + documentation of this and other functions in this file.
-  private static BuiltinFunction newWorkspaceNameFunction(final Builder builder) {
-    return new BuiltinFunction("workspace",
-        FunctionSignature.namedOnly("name"), BuiltinFunction.USE_LOC) {
-      public Object invoke(String name,
-          Location loc) throws EvalException {
-        String errorMessage = LabelValidator.validateTargetName(name);
-        if (errorMessage != null) {
-          throw new EvalException(loc, errorMessage);
-        }
-        builder.setWorkspaceName(name);
-        return NONE;
-      }
-    };
-  }
-
-  private static BuiltinFunction newBindFunction(final Builder builder) {
-    return new BuiltinFunction(BIND,
-        FunctionSignature.namedOnly("name", "actual"), BuiltinFunction.USE_LOC) {
-      public Object invoke(String name, String actual,
-          Location loc) throws EvalException, ConversionException, InterruptedException {
-        Label nameLabel = null;
-        try {
-          nameLabel = Label.parseAbsolute("//external:" + name);
-          builder.addBinding(nameLabel, new Binding(Label.parseAbsolute(actual), loc));
-        } catch (SyntaxException e) {
-          throw new EvalException(loc, e.getMessage());
-        }
-        return NONE;
-      }
-    };
-  }
-
-  /**
-   * Returns a function-value implementing the build rule "ruleClass" (e.g. cc_library) in the
-   * specified package context.
-   */
-  private static BuiltinFunction newRuleFunction(final RuleFactory ruleFactory,
-      final Builder builder, final String ruleClassName) {
-    return new BuiltinFunction(ruleClassName,
-        FunctionSignature.KWARGS, BuiltinFunction.USE_AST) {
-      public Object invoke(Map<String, Object> kwargs,
-          FuncallExpression ast) throws EvalException {
-        try {
-          RuleClass ruleClass = ruleFactory.getRuleClass(ruleClassName);
-          builder.createAndAddRepositoryRule(ruleClass, kwargs, ast);
-        } catch (RuleFactory.InvalidRuleException | NameConflictException | SyntaxException e) {
-          throw new EvalException(ast.getLocation(), e.getMessage());
-        }
-        return NONE;
-      }
-    };
-  }
-
-  public boolean evaluateWorkspaceFile(BuildFileAST buildFileAST, Builder builder,
-      StoredEventHandler eventHandler) throws InterruptedException {
-    // Environment is defined in SkyFunction and the syntax package.
-    com.google.devtools.build.lib.syntax.Environment workspaceEnv =
-        new com.google.devtools.build.lib.syntax.Environment();
-
-    RuleFactory ruleFactory = new RuleFactory(packageFactory.getRuleClassProvider());
-    for (String ruleClass : ruleFactory.getRuleClassNames()) {
-      BaseFunction ruleFunction = newRuleFunction(ruleFactory, builder, ruleClass);
-      workspaceEnv.update(ruleClass, ruleFunction);
-    }
-
-    workspaceEnv.update("__embedded_dir__", this.installDir.toString());
-    // TODO(kchodorow): Get all the toolchain rules and load this from there.
-    File jreDirectory = new File(System.getProperty("java.home"));
-    workspaceEnv.update("DEFAULT_SERVER_JAVABASE", jreDirectory.getParentFile().toString());
-
-    workspaceEnv.update(BIND, newBindFunction(builder));
-    workspaceEnv.update("workspace", newWorkspaceNameFunction(builder));
-
-    return buildFileAST.exec(workspaceEnv, eventHandler);
-  }
-
   private static final class WorkspaceFileFunctionException extends SkyFunctionException {
     public WorkspaceFileFunctionException(IOException e, Transience transience) {
       super(e, transience);
diff --git a/src/main/java/com/google/devtools/build/workspace/BUILD b/src/main/java/com/google/devtools/build/workspace/BUILD
index c0f35dd..919b431 100644
--- a/src/main/java/com/google/devtools/build/workspace/BUILD
+++ b/src/main/java/com/google/devtools/build/workspace/BUILD
@@ -1,10 +1,34 @@
 java_binary(
     name = "generate_workspace",
-    srcs = ["WorkspaceFileGenerator.java"],
-    main_class = "com.google.devtools.build.workspace.WorkspaceFileGenerator",
+    srcs = [
+        "GenerateWorkspace.java",
+        "GenerateWorkspaceOptions.java",
+    ],
+    main_class = "com.google.devtools.build.workspace.GenerateWorkspace",
     visibility = ["//visibility:public"],
     deps = [
+        ":workspace",
+        "//src/main/java:bazel-core",
+        "//src/main/java:common",
         "//src/main/java:events",
+        "//src/main/java:options",
+        "//src/main/java:packages",
+        "//src/main/java:vfs",
         "//src/main/java/com/google/devtools/build/workspace/maven",
     ],
 )
+
+java_library(
+    name = "workspace",
+    srcs = ["Resolver.java"],
+    deps = [
+        "//src/main/java:analysis-exec-rules-skyframe",
+        "//src/main/java:bazel-core",
+        "//src/main/java:events",
+        "//src/main/java:packages",
+        "//src/main/java:vfs",
+        "//src/main/java/com/google/devtools/build/workspace/maven",
+        "//src/main/java/com/google/devtools/build/workspace/maven:rule",
+        "//third_party:guava",
+    ],
+)
diff --git a/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java b/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java
new file mode 100644
index 0000000..7000279
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/workspace/GenerateWorkspace.java
@@ -0,0 +1,95 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.devtools.build.workspace;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.FileSystem;
+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.common.options.OptionsParser;
+
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Generates a WORKSPACE file for Bazel from other types of dependency trackers.
+ */
+public class GenerateWorkspace {
+
+  private final StoredEventHandler handler;
+  private final FileSystem fileSystem;
+  private final com.google.devtools.build.workspace.maven.Resolver resolver;
+
+  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()) {
+      printUsage(parser);
+      return;
+    }
+
+    GenerateWorkspace workspaceFileGenerator = new GenerateWorkspace();
+    workspaceFileGenerator.generateFromWorkspace(options.bazelProjects);
+    workspaceFileGenerator.generateFromPom(options.mavenProjects);
+    workspaceFileGenerator.print();
+  }
+
+  private static void printUsage(OptionsParser parser) {
+    System.out.println("Usage: generate_workspace (-b PATH|-m PATH)+\n\n"
+        + "Generates a workspace file from the given projects. At least one bazel_project or "
+        + "maven_project must be specified.\n");
+    System.out.println(parser.describeOptions(Collections.<String, String>emptyMap(),
+        OptionsParser.HelpVerbosity.LONG));
+  }
+
+  private GenerateWorkspace() {
+    this.handler = new StoredEventHandler();
+    this.fileSystem = OS.getCurrent() == OS.WINDOWS
+        ? new JavaIoFileSystem() : new UnixFileSystem();
+    this.resolver = new com.google.devtools.build.workspace.maven.Resolver(handler, fileSystem);
+  }
+
+  private void generateFromWorkspace(List<String> projects) {
+    for (String project : projects) {
+      Resolver workspaceResolver = new Resolver(resolver, handler);
+      java.nio.file.Path cwd = Paths.get(System.getProperty("user.dir"));
+      Path projectPath = fileSystem.getPath(cwd.resolve(project).toString());
+      ExternalPackage externalPackage =
+          workspaceResolver.parse(projectPath.getRelative("WORKSPACE"));
+      workspaceResolver.resolveTransitiveDependencies(externalPackage);
+    }
+  }
+
+  private void generateFromPom(List<String> projects) {
+    for (String project : projects) {
+      resolver.resolvePomDependencies(project);
+    }
+  }
+
+  private void print() {
+    resolver.writeDependencies(System.out);
+    resolver.cleanup();
+
+    for (Event event : handler.getEvents()) {
+      System.err.println(event);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java b/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java
new file mode 100644
index 0000000..0db735f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/workspace/GenerateWorkspaceOptions.java
@@ -0,0 +1,51 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.devtools.build.workspace;
+
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.OptionsBase;
+
+import java.util.List;
+
+/**
+ * Command-line options for generate_workspace tool.
+ */
+public class GenerateWorkspaceOptions extends OptionsBase {
+  @Option(
+      name = "help",
+      abbrev = 'h',
+      help = "Prints usage info.",
+      defaultValue = "true"
+  )
+  public boolean help;
+
+  @Option(
+      name = "bazel_project",
+      abbrev = 'b',
+      help = "Directory contains a Bazel project.",
+      allowMultiple = true,
+      defaultValue = ""
+  )
+  public List<String> bazelProjects;
+
+  @Option(
+      name = "maven_project",
+      abbrev = 'm',
+      help = "Directory containing a Maven project.",
+      allowMultiple = true,
+      defaultValue = ""
+  )
+  public List<String> mavenProjects;
+}
diff --git a/src/main/java/com/google/devtools/build/workspace/Resolver.java b/src/main/java/com/google/devtools/build/workspace/Resolver.java
new file mode 100644
index 0000000..4229fc4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/workspace/Resolver.java
@@ -0,0 +1,122 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.devtools.build.workspace;
+
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
+import com.google.devtools.build.lib.bazel.BazelMain;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.AttributeMap;
+import com.google.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.WorkspaceFactory;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.workspace.maven.Rule;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Finds the transitive dependencies of a WORKSPACE file.
+ */
+public class Resolver {
+
+  private final RuleClassProvider ruleClassProvider;
+  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) {
+    this.resolver = resolver;
+    this.handler = handler;
+    ConfiguredRuleClassProvider.Builder ruleClassBuilder =
+        new ConfiguredRuleClassProvider.Builder();
+    List<BlazeModule> blazeModules = BlazeRuntime.createModules(BazelMain.BAZEL_MODULES);
+    for (BlazeModule blazeModule : blazeModules) {
+      blazeModule.initializeRuleClasses(ruleClassBuilder);
+    }
+    this.ruleClassProvider = ruleClassBuilder.build();
+  }
+
+  /**
+   * Converts the WORKSPACE file content into an ExternalPackage.
+   */
+  public ExternalPackage parse(Path workspacePath) {
+    resolver.addHeader(workspacePath.getPathString());
+    ExternalPackage.Builder builder = new ExternalPackage.Builder(workspacePath);
+    WorkspaceFactory parser = new WorkspaceFactory(builder, ruleClassProvider);
+    try {
+      parser.parse(ParserInputSource.create(workspacePath));
+    } catch (IOException | InterruptedException e) {
+      handler.handle(Event.error(Location.fromFile(workspacePath), e.getMessage()));
+    }
+
+    return builder.build();
+  }
+
+  /**
+   * Calculates transitive dependencies of the given //external package.
+   */
+  public void resolveTransitiveDependencies(ExternalPackage externalPackage) {
+    Location location = Location.fromFile(externalPackage.getFilename());
+    for (Target target : externalPackage.getTargets()) {
+      // Targets are //external:foo.
+      if (target.getTargetKind().startsWith("bind")
+          || target.getTargetKind().startsWith("source ")) {
+        continue;
+      } else if (target.getTargetKind().startsWith("maven_jar ")) {
+        PackageIdentifier.RepositoryName repositoryName;
+        try {
+          repositoryName = PackageIdentifier.RepositoryName.create("@" + target.getName());
+        } catch (Label.SyntaxException e) {
+          handler.handle(Event.error(location, "Invalid repository name for " + target + ": "
+              + e.getMessage()));
+          return;
+        }
+        com.google.devtools.build.lib.packages.Rule workspaceRule =
+            externalPackage.getRepositoryInfo(repositoryName);
+
+        AttributeMap attributeMap = AggregatingAttributeMapper.of(workspaceRule);
+        Rule rule;
+        try {
+          if (attributeMap.has("artifact", Type.STRING)
+              && !attributeMap.get("artifact", Type.STRING).isEmpty()) {
+            rule = new Rule(attributeMap.get("artifact", Type.STRING));
+          } else {
+            rule = new Rule(
+                attributeMap.get("artifact_id", Type.STRING),
+                attributeMap.get("group_id", Type.STRING),
+                attributeMap.get("version", Type.STRING));
+          }
+        } catch (Rule.InvalidRuleException e) {
+          handler.handle(Event.error(location, e.getMessage()));
+          return;
+        }
+        resolver.getArtifactDependencies(rule, location);
+      } else {
+        handler.handle(Event.warn(location, "Cannot fetch transitive dependencies for " + target
+            + " yet, skipping"));
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/workspace/WorkspaceFileGenerator.java b/src/main/java/com/google/devtools/build/workspace/WorkspaceFileGenerator.java
deleted file mode 100644
index 8561910..0000000
--- a/src/main/java/com/google/devtools/build/workspace/WorkspaceFileGenerator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2015 Google Inc. All rights reserved.
-//
-// 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.devtools.build.workspace;
-
-import com.google.devtools.build.lib.events.Event;
-import com.google.devtools.build.lib.events.StoredEventHandler;
-import com.google.devtools.build.workspace.maven.Resolver;
-
-import java.io.File;
-
-/**
- * Generates a WORKSPACE file for Bazel from other types of dependency trackers.
- */
-public class WorkspaceFileGenerator {
-
-  public static void main(String[] args) {
-    String directory;
-    if (args.length == 1) {
-      directory = args[0];
-    } else {
-      directory = System.getProperty("user.dir");
-    }
-    StoredEventHandler handler = new StoredEventHandler();
-    Resolver connector = new Resolver(new File(directory), handler);
-    connector.writeDependencies(System.out);
-    if (handler.hasErrors()) {
-      for (Event event : handler.getEvents()) {
-        System.err.println(event);
-      }
-    }
-  }
-
-}
diff --git a/src/main/java/com/google/devtools/build/workspace/maven/BUILD b/src/main/java/com/google/devtools/build/workspace/maven/BUILD
index 3b0e2f5..74d1132 100644
--- a/src/main/java/com/google/devtools/build/workspace/maven/BUILD
+++ b/src/main/java/com/google/devtools/build/workspace/maven/BUILD
@@ -15,6 +15,8 @@
     deps = [
         ":rule",
         "//src/main/java:events",
+        "//src/main/java:maven-connector",
+        "//src/main/java:vfs",
         "//third_party:aether",
         "//third_party:guava",
         "//third_party:maven_model",
@@ -25,5 +27,11 @@
 java_library(
     name = "rule",
     srcs = ["Rule.java"],
-    deps = ["//third_party:auto_value"],
+    visibility = [
+        "//src/main/java/com/google/devtools/build:__subpackages__",
+    ],
+    deps = [
+        "//third_party:aether",
+        "//third_party:guava",
+    ],
 )
diff --git a/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java b/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
index 58cd6cc..b1ff699 100644
--- a/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
+++ b/src/main/java/com/google/devtools/build/workspace/maven/Resolver.java
@@ -14,9 +14,15 @@
 
 package com.google.devtools.build.workspace.maven;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+import com.google.devtools.build.lib.bazel.repository.MavenConnector;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
-import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.vfs.FileSystem;
+
 import org.apache.maven.model.Model;
 import org.apache.maven.model.Repository;
 import org.apache.maven.model.building.DefaultModelBuilderFactory;
@@ -26,54 +32,100 @@
 import org.apache.maven.model.building.ModelBuildingResult;
 import org.apache.maven.model.io.DefaultModelReader;
 import org.apache.maven.model.locator.DefaultModelLocator;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.collection.CollectRequest;
+import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.PrintStream;
-import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * Resolves Maven dependencies.
  */
 public class Resolver {
-  private final File projectDirectory;
-  private final Map<String, Rule> deps;
+  private final MavenConnector connector;
+  private final File localRepository;
+  private final FileSystem fileSystem;
   private final EventHandler handler;
 
-  public Resolver(File projectDirectory, EventHandler handler) {
-    this.projectDirectory = projectDirectory;
-    deps = new HashMap<String, Rule>();
+  private final List<String> headers;
+  // Mapping of maven_jar name to Rule.
+  private final Map<String, Rule> deps;
+
+  public Resolver(EventHandler handler, FileSystem fileSystem) {
     this.handler = handler;
+    this.fileSystem = fileSystem;
+    this.localRepository = Files.createTempDir();
+    this.connector = new MavenConnector(localRepository.getPath());
+    headers = Lists.newArrayList();
+    deps = Maps.newHashMap();
   }
 
   /**
-   * Find the pom.xml, parse it, and write the equivalent WORKSPACE file to the provided
-   * outputStream.
+   * Writes all resolved dependencies in WORKSPACE file format to the outputStream.
    */
   public void writeDependencies(PrintStream outputStream) {
-    resolveDependencies();
+    outputStream.println("# --------------------\n"
+        + "# The following dependencies were calculated from:");
+    for (String header : headers) {
+      outputStream.println("# " + header);
+    }
+    outputStream.print("\n\n");
     for (Rule rule : deps.values()) {
-      outputStream.println(rule.toString() + "\n");
+      outputStream.println(rule + "\n");
+    }
+    outputStream.println("# --------------------\n");
+  }
+
+  public void addHeader(String header) {
+    headers.add(header);
+  }
+
+  /**
+   * Remove the temporary directory storing pom files.
+   */
+  public void cleanup() {
+    try {
+      for (File file : Files.fileTreeTraverser().postOrderTraversal(localRepository)) {
+        java.nio.file.Files.delete(file.toPath());
+      }
+    } catch (IOException e) {
+      handler.handle(Event.error(Location.fromFile(fileSystem.getPath(localRepository.getPath())),
+          "Could not create local repository directory " + localRepository + ": "
+              + e.getMessage()));
     }
   }
 
-  private void resolveDependencies() {
+  /**
+   * Resolves all dependencies from a pom.xml file in the given directory.
+   */
+  public void resolvePomDependencies(String project) {
     DefaultModelProcessor processor = new DefaultModelProcessor();
     processor.setModelLocator(new DefaultModelLocator());
     processor.setModelReader(new DefaultModelReader());
-    File pom = processor.locatePom(projectDirectory);
+    File pom = processor.locatePom(new File(project));
+    Location pomLocation = Location.fromFile(fileSystem.getPath(pom.getPath()));
+    addHeader(pom.getAbsolutePath());
 
     DefaultModelBuilderFactory factory = new DefaultModelBuilderFactory();
     DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
     request.setPomFile(pom);
-    Model model = null;
+    Model model;
     try {
       ModelBuildingResult result = factory.newInstance().build(request);
       model = result.getEffectiveModel();
     } catch (ModelBuildingException e) {
-      System.err.println("Unable to resolve Maven model from " + pom + ": " + e.getMessage());
+      handler.handle(Event.error(pomLocation,
+          "Unable to resolve Maven model from " + pom + ": " + e.getMessage()));
       return;
     }
 
@@ -84,22 +136,79 @@
     }
 
     for (org.apache.maven.model.Dependency dependency : model.getDependencies()) {
-      Rule rule = new Rule(
-          dependency.getArtifactId(), dependency.getGroupId(), dependency.getVersion());
-      if (deps.containsKey(rule.name())) {
-        Rule existingDependency = deps.get(rule.name());
-        // Check that the versions are the same.
-        if (!existingDependency.version().equals(dependency.getVersion())) {
-          handler.handle(new Event(EventKind.ERROR, null, dependency.getGroupId() + ":"
-              + dependency.getArtifactId() + " already processed for version "
-              + existingDependency.version() + " but " + model + " wants version "
-              + dependency.getVersion() + ", ignoring."));
-        }
-        // If it already exists at the right version, we're done.
-      } else {
-        deps.put(rule.name(), rule);
-        // TODO(kchodorow): fetch transitive dependencies.
+      try {
+        Rule artifactRule = new Rule(
+            dependency.getArtifactId(), dependency.getGroupId(), dependency.getVersion());
+        addArtifact(artifactRule, model.toString(), pomLocation);
+        getArtifactDependencies(artifactRule, pomLocation);
+      } catch (Rule.InvalidRuleException e) {
+        handler.handle(Event.error(pomLocation, e.getMessage()));
       }
     }
   }
+
+  /**
+   * Adds transitive dependencies of the given artifact.
+   */
+  public void getArtifactDependencies(Rule artifactRule, Location location) {
+    Artifact artifact = artifactRule.getArtifact();
+
+    RepositorySystem system = connector.newRepositorySystem();
+    RepositorySystemSession session = connector.newRepositorySystemSession(system);
+
+    ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
+    descriptorRequest.setArtifact(artifact);
+    descriptorRequest.addRepository(MavenConnector.getMavenCentral());
+
+    ArtifactDescriptorResult descriptorResult;
+    try {
+      descriptorResult = system.readArtifactDescriptor(session, descriptorRequest);
+      if (descriptorResult == null) {
+        return;
+      }
+    } catch (ArtifactDescriptorException e) {
+      handler.handle(Event.error(location, e.getMessage()));
+      return;
+    }
+
+    for (Dependency dependency : descriptorResult.getDependencies()) {
+      Artifact depArtifact = dependency.getArtifact();
+      try {
+        Rule rule = new Rule(
+            depArtifact.getArtifactId(), depArtifact.getGroupId(), depArtifact.getVersion());
+        if (addArtifact(rule, artifactRule.toMavenArtifactString(), location)) {
+          getArtifactDependencies(rule, location);
+        }
+      } catch (Rule.InvalidRuleException e) {
+        handler.handle(Event.error(location, e.getMessage()));
+      }
+    }
+  }
+
+  /**
+   * Adds the artifact to the list of deps, if it is not already there. Returns if the artifact
+   * was already in the list. If the artifact was in the list at a different version, adds an
+   * error event to the event handler.
+   */
+  private boolean addArtifact(Rule dependency, String parent, Location pomLocation) {
+    String artifactName = dependency.name();
+    if (deps.containsKey(artifactName)) {
+      Rule existingDependency = deps.get(artifactName);
+      // Check that the versions are the same.
+      if (!existingDependency.version().equals(dependency.version())) {
+        handler.handle(Event.warn(pomLocation, dependency.groupId() + ":"
+            + dependency.artifactId() + " already processed for version "
+            + existingDependency.version() + " but " + parent + " wants version "
+            + dependency.version() + ", ignoring."));
+        existingDependency.addParent(parent + " wanted version " + dependency.version());
+      } else {
+        existingDependency.addParent(parent);
+      }
+      return false;
+    }
+
+    deps.put(artifactName, dependency);
+    dependency.addParent(parent);
+    return true;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/workspace/maven/Rule.java b/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
index 33a78eb..65fa88c 100644
--- a/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
+++ b/src/main/java/com/google/devtools/build/workspace/maven/Rule.java
@@ -14,43 +14,87 @@
 
 package com.google.devtools.build.workspace.maven;
 
+import com.google.common.collect.Lists;
+
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+
+import java.util.List;
+
 /**
  * A struct representing the fields of maven_jar to be written to the WORKSPACE file.
  */
 public final class Rule {
-  private final String artifactId;
-  private final String groupId;
-  private final String version;
+  private final Artifact artifact;
+  private final List<String> parents;
 
-  public Rule(String artifactId, String groupId, String version) {
-    this.artifactId = artifactId;
-    this.groupId = groupId;
-    this.version = version;
+  public Rule(String artifactStr) throws InvalidRuleException {
+    try {
+      this.artifact = new DefaultArtifact(artifactStr);
+    } catch (IllegalArgumentException e) {
+      throw new InvalidRuleException(e.getMessage());
+    }
+    this.parents = Lists.newArrayList();
+  }
+
+  public Rule(String artifactId, String groupId, String version)
+      throws InvalidRuleException {
+    this(groupId + ":" + artifactId + ":" + version);
+  }
+
+  public void addParent(String parent) {
+    parents.add(parent);
   }
 
   public String artifactId() {
-    return artifactId;
+    return artifact.getArtifactId();
   }
 
   public String groupId() {
-    return groupId;
+    return artifact.getGroupId();
   }
 
   public String version() {
-    return version;
+    return artifact.getVersion();
   }
 
+  /**
+   * A unique name for this artifact to use in maven_jar's name attribute.
+   */
   String name() {
     return (groupId() + "/" + artifactId()).replaceAll("\\.", "/");
   }
 
+  public Artifact getArtifact() {
+    return artifact;
+  }
+
+  public String toMavenArtifactString() {
+    return groupId() + ":" + artifactId() + ":" + version();
+  }
+
+  /**
+   * The way this jar should be stringified for the WORKSPACE file.
+   */
   @Override
   public String toString() {
-    return "maven_jar(\n"
+    StringBuilder builder = new StringBuilder();
+    for (String parent : parents) {
+      builder.append("# " + parent + "\n");
+    }
+    builder.append("maven_jar(\n"
         + "    name = \"" + name() + "\",\n"
-        + "    artifact_id = \"" + artifactId() + "\",\n"
-        + "    group_id = \"" + groupId() + "\",\n"
-        + "    version = \"" + version() + "\",\n"
-        + ")";
+        + "    artifact = \"" + toMavenArtifactString() + "\",\n"
+        + ")");
+    return builder.toString();
+  }
+
+  /**
+   * Exception thrown if the rule could not be created.
+   */
+  public static class InvalidRuleException extends Exception {
+    InvalidRuleException(String message) {
+      super(message);
+    }
   }
 }
diff --git a/src/test/shell/bazel/workspace_test.sh b/src/test/shell/bazel/workspace_test.sh
index 0fd3f15..00bfd85d 100755
--- a/src/test/shell/bazel/workspace_test.sh
+++ b/src/test/shell/bazel/workspace_test.sh
@@ -55,11 +55,9 @@
 function test_minimal_pom() {
   write_pom
 
-  ${bazel_data}/src/main/java/com/google/devtools/build/workspace/generate_workspace &> $TEST_log || \
-    fail "generating workspace failed"
-  expect_log "artifact_id = \"x\","
-  expect_log "group_id = \"com.y.z\","
-  expect_log "version = \"3.2.1\","
+  ${bazel_data}/src/main/java/com/google/devtools/build/workspace/generate_workspace \
+    --maven_project=$(pwd) &> $TEST_log || fail "generating workspace failed"
+  expect_log "artifact = \"com.y.z:x:3.2.1\","
 }
 
 function test_parent_pom_inheritence() {
@@ -86,15 +84,13 @@
 </project>
 EOF
 
-  ${bazel_data}/src/main/java/com/google/devtools/build/workspace/generate_workspace my-module &> $TEST_log || \
+  ${bazel_data}/src/main/java/com/google/devtools/build/workspace/generate_workspace \
+    --maven_project=$(pwd)/my-module &> $TEST_log || \
     fail "generating workspace failed"
   expect_log "name = \"com/y/z/x\","
-  expect_log "artifact_id = \"x\","
-  expect_log "group_id = \"com.y.z\","
-  expect_log "version = \"3.2.1\","
+  expect_log "artifact = \"com.y.z:x:3.2.1\","
   expect_log "name = \"com/z/w/x\","
-  expect_log "group_id = \"com.z.w\","
-  expect_log "version = \"1.2.3\","
+  expect_log "artifact = \"com.z.w:x:1.2.3\","
 }
 
 run_suite "workspace tests"