Add a maven_server rule

This will also be used for authentication, but that has not been implemented
yet.

--
MOS_MIGRATED_REVID=103194964
diff --git a/src/main/java/BUILD b/src/main/java/BUILD
index 5b2be77..9c7214a 100644
--- a/src/main/java/BUILD
+++ b/src/main/java/BUILD
@@ -427,6 +427,7 @@
         "//third_party:apache_commons_logging",
         "//third_party:apache_httpclient",
         "//third_party:apache_httpcore",
+        "//third_party:maven",
         "//third_party:maven_model",
         "//third_party:plexus_interpolation",
         "//third_party:plexus_utils",
@@ -470,7 +471,9 @@
         "//third_party:joda_time",
         "//third_party:jsr305",
         "//third_party:jsr330_inject",
+        "//third_party:maven",
         "//third_party:maven_model",
+        "//third_party:plexus_component_annotations",
         "//third_party:protobuf",
         "//third_party:slf4j",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index 7e90a11..1c6347f 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -37,6 +37,7 @@
 import com.google.devtools.build.lib.bazel.repository.JarFunction;
 import com.google.devtools.build.lib.bazel.repository.LocalRepositoryFunction;
 import com.google.devtools.build.lib.bazel.repository.MavenJarFunction;
+import com.google.devtools.build.lib.bazel.repository.MavenServerFunction;
 import com.google.devtools.build.lib.bazel.repository.NewGitRepositoryFunction;
 import com.google.devtools.build.lib.bazel.repository.NewHttpArchiveFunction;
 import com.google.devtools.build.lib.bazel.repository.NewLocalRepositoryFunction;
@@ -220,6 +221,7 @@
     builder.put(ZipFunction.NAME, new ZipFunction());
     builder.put(TarGzFunction.NAME, new TarGzFunction());
     builder.put(FileFunction.NAME, new FileFunction());
+    builder.put(MavenServerFunction.NAME, new MavenServerFunction(directories));
     return builder.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 f149235..497daeb 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
@@ -18,18 +18,19 @@
 import com.google.common.base.Ascii;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier.RepositoryName;
+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.Rule;
 import com.google.devtools.build.lib.packages.Type;
 import com.google.devtools.build.lib.skyframe.FileValue;
 import com.google.devtools.build.lib.skyframe.RepositoryValue;
+import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
 import com.google.devtools.build.skyframe.SkyFunctionName;
@@ -46,7 +47,6 @@
 import org.eclipse.aether.resolution.ArtifactResult;
 
 import java.io.IOException;
-import java.util.List;
 
 import javax.annotation.Nullable;
 
@@ -55,6 +55,8 @@
  */
 public class MavenJarFunction extends HttpArchiveFunction {
 
+  private static final String DEFAULT_SERVER = "default";
+
   @Override
   public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctionException {
     RepositoryName repositoryName = (RepositoryName) skyKey.argument();
@@ -62,16 +64,43 @@
     if (rule == null) {
       return null;
     }
+
+    String url;
     AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule);
-    MavenDownloader downloader = createMavenDownloader(mapper);
+    boolean hasRepository = mapper.has("repository", Type.STRING)
+        && !mapper.get("repository", Type.STRING).isEmpty();
+    boolean hasServer = mapper.has("server", Type.STRING)
+        && !mapper.get("server", Type.STRING).isEmpty();
+    if (hasRepository && hasServer) {
+      throw new RepositoryFunctionException(new EvalException(
+          Location.fromFile(getWorkspace().getRelative("WORKSPACE")), rule + " specifies both "
+              + "'repository' and 'server', which are mutually exclusive options"),
+          Transience.PERSISTENT);
+    } else if (hasRepository) {
+      url = mapper.get("repository", Type.STRING);
+    } else {
+      String serverName = DEFAULT_SERVER;
+      if (mapper.has("server", Type.STRING) && !mapper.get("server", Type.STRING).isEmpty()) {
+        serverName = mapper.get("server", Type.STRING);
+      }
+
+      MavenServerValue mavenServerValue = (MavenServerValue) env.getValue(
+          MavenServerValue.key(serverName));
+      if (mavenServerValue == null) {
+        return null;
+      }
+      url = mavenServerValue.getUrl();
+    }
+
+    MavenDownloader downloader = createMavenDownloader(mapper, url);
     return createOutputTree(downloader, env);
   }
 
   @VisibleForTesting
-  MavenDownloader createMavenDownloader(AttributeMap mapper) {
+  MavenDownloader createMavenDownloader(AttributeMap mapper, String url) {
     String name = mapper.getName();
     Path outputDirectory = getExternalRepositoryDirectory().getRelative(name);
-    return new MavenDownloader(name, mapper, outputDirectory);
+    return new MavenDownloader(name, mapper, outputDirectory, url);
   }
 
   SkyValue createOutputTree(MavenDownloader downloader, Environment env)
@@ -129,10 +158,10 @@
     private final Path outputDirectory;
     @Nullable
     private final String sha1;
-    // TODO(kchodorow): change this to a single repository on 9/15.
-    private final List<RemoteRepository> repositories;
+    private final String url;
 
-    public MavenDownloader(String name, AttributeMap mapper, Path outputDirectory) {
+    public MavenDownloader(
+        String name, AttributeMap mapper, Path outputDirectory, String url) {
       this.name = name;
       this.outputDirectory = outputDirectory;
 
@@ -144,22 +173,7 @@
             + mapper.get("version", Type.STRING);
       }
       this.sha1 = (mapper.has("sha1", Type.STRING)) ? mapper.get("sha1", Type.STRING) : null;
-
-      if (mapper.has("repository", Type.STRING)
-          && !mapper.get("repository", Type.STRING).isEmpty()) {
-        this.repositories = ImmutableList.of(new RemoteRepository.Builder(
-            "user-defined repository", "default", mapper.get("repository", Type.STRING)).build());
-      } else if (mapper.has("repositories", Type.STRING_LIST)
-          && !mapper.get("repositories", Type.STRING_LIST).isEmpty()) {
-        // TODO(kchodorow): remove after 9/15, uses deprecated list of repositories attribute.
-        this.repositories = Lists.newArrayList();
-        for (String repositoryUrl : mapper.get("repositories", Type.STRING_LIST)) {
-          this.repositories.add(new RemoteRepository.Builder(
-              "user-defined repository " + repositories.size(), "default", repositoryUrl).build());
-        }
-      } else {
-        this.repositories = ImmutableList.of(MavenConnector.getMavenCentralRemote());
-      }
+      this.url = url;
     }
 
     /**
@@ -184,6 +198,7 @@
       RepositorySystem system = connector.newRepositorySystem();
       RepositorySystemSession session = connector.newRepositorySystemSession(system);
 
+      RemoteRepository repository = new RemoteRepository.Builder(name, "default", url).build();
       ArtifactRequest artifactRequest = new ArtifactRequest();
       Artifact artifact;
       try {
@@ -192,7 +207,7 @@
         throw new IOException(e.getMessage());
       }
       artifactRequest.setArtifact(artifact);
-      artifactRequest.setRepositories(repositories);
+      artifactRequest.setRepositories(ImmutableList.of(repository));
 
       try {
         ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java
new file mode 100644
index 0000000..4485021
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerFunction.java
@@ -0,0 +1,125 @@
+// 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.bazel.repository;
+
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenServerRule;
+import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
+import com.google.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.FileValue;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import org.apache.maven.settings.Server;
+import org.apache.maven.settings.Settings;
+import org.apache.maven.settings.building.DefaultSettingsBuilder;
+import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
+import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
+import org.apache.maven.settings.building.SettingsBuildingException;
+import org.apache.maven.settings.building.SettingsBuildingResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementation of maven_repository.
+ */
+public class MavenServerFunction extends RepositoryFunction {
+  public static final SkyFunctionName NAME = SkyFunctionName.create("MAVEN_SERVER_FUNCTION");
+
+  public MavenServerFunction(BlazeDirectories directories) {
+    setDirectories(directories);
+  }
+
+  @Nullable
+  @Override
+  public SkyValue compute(SkyKey skyKey, Environment env) throws RepositoryFunctionException {
+    String repository = skyKey.argument().toString();
+    ExternalPackage externalPackage = RepositoryFunction.getExternalPackage(env);
+    Rule repositoryRule = externalPackage.getRule(repository);
+
+    boolean foundRepoRule = repositoryRule != null
+        && repositoryRule.getRuleClass().equals(MavenServerRule.NAME);
+    if (!foundRepoRule) {
+      if (repository.equals(MavenServerValue.DEFAULT_ID)) {
+        // The default repository is being used and the WORKSPACE is not overriding the default.
+        return new MavenServerValue();
+      }
+      throw new RepositoryFunctionException(
+          new IOException("Could not find maven repository " + repository), Transience.TRANSIENT);
+    }
+
+    AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(repositoryRule);
+    String serverName = repositoryRule.getName();
+    String url = mapper.get("url", Type.STRING);
+    if (!mapper.has("settings_file", Type.STRING)
+        || mapper.get("settings_file", Type.STRING).isEmpty()) {
+      return new MavenServerValue(serverName, url, new Server());
+    }
+    PathFragment settingsFilePath = new PathFragment(mapper.get("settings_file", Type.STRING));
+    RootedPath settingsPath = RootedPath.toRootedPath(
+        getWorkspace().getRelative(settingsFilePath), PathFragment.EMPTY_FRAGMENT);
+    FileValue settingsFile = (FileValue) env.getValue(FileValue.key(settingsPath));
+    if (settingsFile == null) {
+      return null;
+    }
+
+    if (!settingsFile.exists()) {
+      throw new RepositoryFunctionException(
+          new IOException("Could not find settings file " + settingsPath), Transience.TRANSIENT);
+    }
+
+    DefaultSettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
+    request.setUserSettingsFile(new File(settingsFile.realRootedPath().asPath().toString()));
+    DefaultSettingsBuilder builder = (new DefaultSettingsBuilderFactory()).newInstance();
+    SettingsBuildingResult result;
+    try {
+      result = builder.build(request);
+    } catch (SettingsBuildingException e) {
+      throw new RepositoryFunctionException(
+          new IOException("Error parsing settings file " + settingsFile + ": " + e.getMessage()),
+          Transience.TRANSIENT);
+    }
+    if (!result.getProblems().isEmpty()) {
+      throw new RepositoryFunctionException(
+          new IOException("Errors interpreting settings file: "
+              + Arrays.toString(result.getProblems().toArray())), Transience.PERSISTENT);
+    }
+    Settings settings = result.getEffectiveSettings();
+    Server server = settings.getServer(mapper.getName());
+    server = server == null ? new Server() : server;
+    return new MavenServerValue(serverName, url, server);
+  }
+
+  @Override
+  public SkyFunctionName getSkyFunctionName() {
+    return NAME;
+  }
+
+  @Override
+  public Class<? extends RuleDefinition> getRuleDefinition() {
+    return MavenServerRule.class;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java
new file mode 100644
index 0000000..78f2e26
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenServerValue.java
@@ -0,0 +1,78 @@
+// 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.bazel.repository;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import org.apache.maven.settings.Server;
+
+/**
+ * A Maven repository's identifier.
+ */
+public class MavenServerValue implements SkyValue {
+  public static final String DEFAULT_ID = "default";
+
+  private final String id;
+  private final String url;
+  private final Server server;
+
+  public static SkyKey key(String serverName) {
+    Preconditions.checkNotNull(serverName);
+    return new SkyKey(MavenServerFunction.NAME, serverName);
+  }
+
+  public MavenServerValue() {
+    id = DEFAULT_ID;
+    url = MavenConnector.getMavenCentralRemote().getUrl();
+    server = new Server();
+  }
+
+  public MavenServerValue(String id, String url, Server server) {
+    Preconditions.checkNotNull(id);
+    Preconditions.checkNotNull(url);
+    Preconditions.checkNotNull(server);
+    this.id = id;
+    this.url = url;
+    this.server = server;
+  }
+
+  @Override
+  public boolean equals(Object object) {
+    if (this == object) {
+      return true;
+    }
+    if (object == null || !(object instanceof MavenServerValue)) {
+      return false;
+    }
+
+    MavenServerValue other = (MavenServerValue) object;
+    return id.equals(other.id) && url.equals(other.url);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(id, url);
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public Server getServer() {
+    return server;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java
index d4042fe5..47fc19d 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryDelegatorFunction.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.bazel.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.NoSuchPackageException;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.skyframe.FileValue;
@@ -84,7 +85,9 @@
 
     RepositoryFunction handler = handlers.get(rule.getRuleClass());
     if (handler == null) {
-      throw new IllegalStateException("Could not find handler for " + rule);
+      throw new RepositoryFunctionException(new EvalException(
+          Location.fromFile(directories.getWorkspace().getRelative("WORKSPACE")),
+          "Could not find handler for " + rule), Transience.PERSISTENT);
     }
     SkyKey key = new SkyKey(handler.getSkyFunctionName(), repositoryName);
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java
index 4bf10f7..1fe07a9 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryFunction.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier.RepositoryName;
 import com.google.devtools.build.lib.packages.AggregatingAttributeMapper;
 import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
@@ -243,16 +244,8 @@
     }
   }
 
-
-  /**
-   * Uses a remote repository name to fetch the corresponding Rule describing how to get it.
-   * This should be called from {@link SkyFunction#compute} functions, which should return null if
-   * this returns null. If {@code ruleClassName} is set, the rule found must have a matching rule
-   * class name.
-   */
   @Nullable
-  public static Rule getRule(
-      RepositoryName repositoryName, @Nullable String ruleClassName, Environment env)
+  public static ExternalPackage getExternalPackage(Environment env)
       throws RepositoryFunctionException {
     SkyKey packageKey = PackageValue.key(ExternalPackage.PACKAGE_IDENTIFIER);
     PackageValue packageValue;
@@ -268,7 +261,35 @@
     if (packageValue == null) {
       return null;
     }
-    ExternalPackage externalPackage = (ExternalPackage) packageValue.getPackage();
+    return (ExternalPackage) packageValue.getPackage();
+  }
+
+  @Nullable
+  public static Rule getRule(
+      String ruleName, @Nullable String ruleClassName, Environment env)
+      throws RepositoryFunctionException {
+    try {
+      return getRule(RepositoryName.create("@" + ruleName), ruleClassName, env);
+    } catch (LabelSyntaxException e) {
+      throw new RepositoryFunctionException(
+          new IOException("Invalid rule name " + ruleName), Transience.PERSISTENT);
+    }
+  }
+
+  /**
+   * Uses a remote repository name to fetch the corresponding Rule describing how to get it.
+   * This should be called from {@link SkyFunction#compute} functions, which should return null if
+   * this returns null. If {@code ruleClassName} is set, the rule found must have a matching rule
+   * class name.
+   */
+  @Nullable
+  public static Rule getRule(
+      RepositoryName repositoryName, @Nullable String ruleClassName, Environment env)
+      throws RepositoryFunctionException {
+    ExternalPackage externalPackage = getExternalPackage(env);
+    if (externalPackage == null) {
+      return null;
+    }
     Rule rule = externalPackage.getRepositoryInfo(repositoryName);
     if (rule == null) {
       throw new RepositoryFunctionException(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
index 605a15d..c8f829e 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
@@ -66,6 +66,7 @@
 import com.google.devtools.build.lib.bazel.rules.workspace.HttpJarRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.LocalRepositoryRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenServerRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.NewGitRepositoryRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.NewHttpArchiveRule;
 import com.google.devtools.build.lib.bazel.rules.workspace.NewLocalRepositoryRule;
@@ -354,6 +355,7 @@
     builder.addRuleDefinition(new HttpFileRule());
     builder.addRuleDefinition(new LocalRepositoryRule());
     builder.addRuleDefinition(new MavenJarRule());
+    builder.addRuleDefinition(new MavenServerRule());
     builder.addRuleDefinition(new NewHttpArchiveRule());
     builder.addRuleDefinition(new NewGitRepositoryRule());
     builder.addRuleDefinition(new NewLocalRepositoryRule());
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java
index a9a0a8e..f8f5b91 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenJarRule.java
@@ -48,13 +48,17 @@
         A URL for a Maven repository to fetch the jar from.
         ${SYNOPSIS}
 
-        <p>Defaults to Maven Central ("central.maven.org").</p>
-
-        <p><b>To be implemented: add a maven_repository rule that allows a default repository
-        to be specified once.</b></p>
+        <p>Either this or <code>server</code> can be specified. Defaults to Maven Central
+         ("central.maven.org").</p>
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
         .add(attr("repository", Type.STRING))
-        .add(attr("repositories", Type.STRING_LIST).undocumented("deprecated"))
+        /* <!-- #BLAZE_RULE(maven_jar).attribute(server) -->
+        A maven_server to use for this artifact.
+         ${SYNOPSIS}
+
+        <p>Either this or <code>repository</code> can be specified.</p>
+        <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+        .add(attr("server", Type.STRING))
         /* <!-- #BLAZE_RULE(maven_jar).ATTRIBUTE(sha1) -->
          A SHA-1 hash of the desired jar.
          ${SYNOPSIS}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java
new file mode 100644
index 0000000..602d705
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/workspace/MavenServerRule.java
@@ -0,0 +1,74 @@
+// 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.rules.workspace;
+
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.packages.Type;
+
+/**
+ * Rule definition for the maven_jar rule.
+ */
+public class MavenServerRule implements RuleDefinition {
+
+  public static final String NAME = "maven_server";
+
+  @Override
+  public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+    return builder
+        /* <!-- #BLAZE_RULE(maven_server).ATTRIBUTE(url) -->
+        A URL for accessing the server.
+        ${SYNOPSIS}
+
+        <p>For example, Maven Central (which is the default and does not need to be defined) would
+        be specified as <code>url = "http://central.maven.org/maven2/"</code>.</p>
+        <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+        .add(attr("url", Type.STRING))
+        /* <!-- #BLAZE_RULE(maven_server).ATTRIBUTE(settings_file) -->
+        A path to a settings.xml file.
+        ${SYNOPSIS}
+        <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+        .add(attr("settings_file", Type.STRING))
+        .setWorkspaceOnly()
+        .build();
+  }
+
+  @Override
+  public Metadata getMetadata() {
+    return RuleDefinition.Metadata.builder()
+        .name(MavenServerRule.NAME)
+        .type(RuleClassType.WORKSPACE)
+        .ancestors(WorkspaceBaseRule.class)
+        .factoryClass(WorkspaceConfiguredTargetFactory.class)
+        .build();
+  }
+}
+/*<!-- #BLAZE_RULE (NAME = maven_server, TYPE = OTHER, FAMILY = Workspace)[GENERIC_RULE] -->
+
+${ATTRIBUTE_SIGNATURE}
+
+<p>How to access a Maven repository.</p>
+
+${ATTRIBUTE_DEFINITION}
+
+<p>This is a combination of a &lt;repository&gt; definition from a pom.xml file and a
+&lt;server&lt; definition from a settings.xml file.</p>
+
+<!-- #END_BLAZE_RULE -->*/
diff --git a/src/test/shell/bazel/generate_workspace_test.sh b/src/test/shell/bazel/generate_workspace_test.sh
index ddfc41a..07dda35 100755
--- a/src/test/shell/bazel/generate_workspace_test.sh
+++ b/src/test/shell/bazel/generate_workspace_test.sh
@@ -18,25 +18,24 @@
 #
 
 # Load test environment
-source $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-setup.sh \
+src_dir=$(cd "$(dirname ${BASH_SOURCE[0]})" && pwd)
+source $src_dir/test-setup.sh \
   || { echo "test-setup.sh not found!" >&2; exit 1; }
+source $src_dir/remote_helpers.sh \
+  || { echo "remote_helpers.sh not found!" >&2; exit 1; }
 
 export JAVA_RUNFILES=$TEST_SRCDIR
 
 function set_up() {
   # Set up custom repository directory.
   m2=$TEST_TMPDIR/my-m2
+  rm -rf $m2
   mkdir -p $m2
-  cd $m2
-  m2_port=$(pick_random_unused_tcp_port) || exit 1
-  python -m SimpleHTTPServer $m2_port &
-  m2_pid=$!
-  wait_for_server_startup
-  cd -
+  startup_server $m2
 }
 
 function tear_down() {
-  kill $m2_pid
+  shutdown_server
   rm -rf $m2
 }
 
@@ -74,18 +73,6 @@
   ${bazel_javabase}/bin/jar cf $pkg_dir/$artifactId-$version.jar $TEST_TMPDIR/$groupId.class
 }
 
-# Waits for the SimpleHTTPServer to actually start up before the test is run.
-# Otherwise the entire test can run before the server starts listening for
-# connections, which causes flakes.
-function wait_for_server_startup() {
-  touch some-file
-  while ! curl localhost:$m2_port/some-file; do
-    echo "waiting for server, exit code: $?"
-  done
-  echo "done waiting for server, exit code: $?"
-  rm some-file
-}
-
 function get_workspace_file() {
   cat $TEST_log | tail -n 2 | head -n 1
 }
@@ -109,7 +96,7 @@
     <repository>
       <id>my-repo1</id>
       <name>a custom repo</name>
-      <url>http://localhost:$m2_port/</url>
+      <url>http://localhost:$fileserver_port/</url>
     </repository>
   </repositories>
 
@@ -130,7 +117,7 @@
   cat $(cat $TEST_log | tail -n 1) > build
 
   assert_contains "artifact = \"blorp:glorp:1.2.3\"," ws
-  assert_contains "repository = \"http://localhost:$m2_port/\"," ws
+  assert_contains "repository = \"http://localhost:$fileserver_port/\"," ws
   assert_contains "\"@blorp/glorp//jar\"," build
 }
 
diff --git a/src/test/shell/bazel/maven_test.sh b/src/test/shell/bazel/maven_test.sh
index 3bdb599..b7b7def 100755
--- a/src/test/shell/bazel/maven_test.sh
+++ b/src/test/shell/bazel/maven_test.sh
@@ -18,13 +18,14 @@
 #
 
 # Load test environment
-src=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+src=$(cd "$(dirname ${BASH_SOURCE[0]})" && pwd)
 source $src/test-setup.sh \
   || { echo "test-setup.sh not found!" >&2; exit 1; }
 source $src/remote_helpers.sh \
   || { echo "remote_helpers.sh not found!" >&2; exit 1; }
 
 function set_up() {
+  startup_server $PWD
   mkdir -p zoo
   cat > zoo/BUILD <<EOF
 java_binary(
@@ -46,6 +47,10 @@
 EOF
 }
 
+function tear_down() {
+  shutdown_server
+}
+
 function test_maven_jar() {
   serve_jar
 
@@ -68,21 +73,18 @@
 
 # Same as test_maven_jar, except omit sha1 implying "we don't care".
 function test_maven_jar_no_sha1() {
-  serve_jar
+  serve_artifact com.example.carnivore carnivore 1.23
 
   cat > WORKSPACE <<EOF
 maven_jar(
     name = 'endangered',
     artifact = "com.example.carnivore:carnivore:1.23",
-    repository = 'http://localhost:$nc_port/',
+    repository = 'http://localhost:$fileserver_port/',
 )
 bind(name = 'mongoose', actual = '@endangered//jar')
 EOF
 
-  bazel fetch //zoo:ball-pit || fail "Fetch failed"
   bazel run //zoo:ball-pit >& $TEST_log || fail "Expected run to succeed"
-  kill_nc
-  assert_contains "GET /com/example/carnivore/carnivore/1.23/carnivore-1.23.jar" $nc_log
   expect_log "Tra-la!"
 }
 
@@ -104,7 +106,8 @@
 bind(name = 'mongoose', actual = '@endangered//jar')
 EOF
 
-  bazel fetch //zoo:ball-pit >& $TEST_log && echo "Expected fetch to fail"
+  bazel clean --expunge
+  bazel build //zoo:ball-pit >& $TEST_log && echo "Expected build to fail"
   kill_nc
   expect_log "Failed to fetch Maven dependency: Could not find artifact"
 }
@@ -126,3 +129,93 @@
   kill_nc
   expect_log "has SHA-1 of $sha1, does not match expected SHA-1 ($sha256)"
 }
+
+function test_default_repository() {
+  serve_artifact thing amabop 1.9
+  cat > WORKSPACE <<EOF
+maven_server(
+    name = "default",
+    url = "http://localhost:$fileserver_port/",
+)
+
+maven_jar(
+    name = "thing-a-ma-bop",
+    artifact = "thing:amabop:1.9",
+)
+EOF
+
+  bazel build @thing-a-ma-bop//jar &> $TEST_log || fail "Building thing failed"
+  expect_log "Target @thing-a-ma-bop//jar:jar up-to-date"
+}
+
+function test_settings() {
+  serve_artifact thing amabop 1.9
+  cat > WORKSPACE <<EOF
+maven_server(
+    name = "x",
+    url = "http://localhost:$fileserver_port/",
+    settings_file = "settings.xml",
+)
+maven_jar(
+    name = "thing-a-ma-bop",
+    artifact = "thing:amabop:1.9",
+    server = "x",
+)
+EOF
+
+  cat > settings.xml <<EOF
+<settings>
+  <servers>
+    <server>
+      <id>default</id>
+    </server>
+  </servers>
+</settings>
+EOF
+
+  bazel build @thing-a-ma-bop//jar &> $TEST_log \
+    || fail "Building thing failed"
+  expect_log "Target @thing-a-ma-bop//jar:jar up-to-date"
+
+  # Create an invalid settings.xml (by using a tag that isn't allowed in
+  # settings).
+  cat > settings.xml <<EOF
+<settings>
+  <repositories>
+    <repository>
+      <id>default</id>
+    </repository>
+  </repositories>
+</settings>
+EOF
+  bazel clean --expunge
+  bazel build @thing-a-ma-bop//jar &> $TEST_log \
+    && fail "Building thing succeeded"
+  expect_log "Unrecognised tag: 'repositories'"
+}
+
+function test_maven_server_dep() {
+  cat > WORKSPACE <<EOF
+maven_server(
+    name = "x",
+    url = "http://localhost:12345/",
+)
+EOF
+
+  cat > BUILD <<EOF
+sh_binary(
+    name = "y",
+    srcs = ["y.sh"],
+    deps = ["@x//:bar"],
+)
+EOF
+
+  touch y.sh
+  chmod +x y.sh
+
+  bazel build //:y &> $TEST_log && fail "Building thing failed"
+  expect_log "no such package '@x//'"
+}
+
+
+run_suite "maven tests"
diff --git a/src/test/shell/bazel/remote_helpers.sh b/src/test/shell/bazel/remote_helpers.sh
index 653a3f6..e1a7a55 100755
--- a/src/test/shell/bazel/remote_helpers.sh
+++ b/src/test/shell/bazel/remote_helpers.sh
@@ -49,11 +49,14 @@
 
 # Creates a jar carnivore.Mongoose and serves it using serve_file.
 function serve_jar() {
-  pkg_dir=$TEST_TMPDIR/carnivore
-  if [ -e "$pkg_dir" ]; then
-    rm -fr $pkg_dir
-  fi
+  make_test_jar
+  serve_file $test_jar
+  cd ${WORKSPACE_DIR}
+}
 
+function make_test_jar() {
+  pkg_dir=$TEST_TMPDIR/carnivore
+  rm -fr $pkg_dir
   mkdir $pkg_dir
   cat > $pkg_dir/Mongoose.java <<EOF
 package carnivore;
@@ -67,12 +70,10 @@
   test_jar=$TEST_TMPDIR/libcarnivore.jar
   cd ${TEST_TMPDIR}
   ${bazel_javabase}/bin/jar cf $test_jar carnivore/Mongoose.class
-
   sha256=$(sha256sum $test_jar | cut -f 1 -d ' ')
   # OS X doesn't have sha1sum, so use openssl.
   sha1=$(openssl sha1 $test_jar | cut -f 2 -d ' ')
-  serve_file $test_jar
-  cd ${WORKSPACE_DIR}
+  cd -
 }
 
 # Serves a redirection from localhost:$redirect_port to $1. Sets the following variables:
@@ -93,6 +94,45 @@
   redirect_pid=$!
 }
 
+# Waits for the SimpleHTTPServer to actually start up before the test is run.
+# Otherwise the entire test can run before the server starts listening for
+# connections, which causes flakes.
+function wait_for_server_startup() {
+  touch some-file
+  while ! curl localhost:$fileserver_port/some-file; do
+    echo "waiting for server, exit code: $?"
+    sleep 1
+  done
+  echo "done waiting for server, exit code: $?"
+  rm some-file
+}
+
+
+function serve_artifact() {
+  local group_id=$1
+  local artifact_id=$2
+  local version=$3
+  make_test_jar
+  maven_path=$fileserver_root/$(echo $group_id | sed 's/\./\//g')/$artifact_id/$version
+  mkdir -p $maven_path
+  openssl sha1 $test_jar > $maven_path/$artifact_id-$version.jar.sha1
+  mv $test_jar $maven_path/$artifact_id-$version.jar
+}
+
+function startup_server() {
+  fileserver_root=$1
+  cd $fileserver_root
+  fileserver_port=$(pick_random_unused_tcp_port) || exit 1
+  python -m SimpleHTTPServer $fileserver_port &
+  fileserver_pid=$!
+  wait_for_server_startup
+  cd -
+}
+
+function shutdown_server() {
+  kill $fileserver_pid
+}
+
 function kill_nc() {
   # Try to kill nc, otherwise the test will time out if Bazel has a bug and
   # didn't make a request to it.
diff --git a/src/test/shell/bazel/test-setup.sh b/src/test/shell/bazel/test-setup.sh
index 237106d..0ff08f9 100755
--- a/src/test/shell/bazel/test-setup.sh
+++ b/src/test/shell/bazel/test-setup.sh
@@ -432,4 +432,3 @@
 
 setup_bazelrc
 setup_clean_workspace
-bazel fetch //tools/jdk/... >& $TEST_log
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 c903c02..6934f91 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
@@ -121,7 +121,6 @@
     return false;
   }
 
-  @Override
   public ModelSource resolveModel(Parent parent) throws UnresolvableModelException {
     return resolveModel(parent.getGroupId(), parent.getArtifactId(), parent.getVersion());
   }
@@ -131,7 +130,6 @@
     repositories.add(repository);
   }
 
-  @Override
   public void addRepository(Repository repository, boolean replace) {
     addRepository(repository);
   }
diff --git a/third_party/BUILD b/third_party/BUILD
index fbf5f56..838dd1d 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -258,6 +258,11 @@
 )
 
 java_import(
+    name = "maven",
+    jars = glob(["maven/*.jar"]),
+)
+
+java_import(
     name = "maven_model",
     jars = [
         "maven_model/maven-aether-provider-3.2.3.jar",
diff --git a/third_party/README.md b/third_party/README.md
index bf4ccdb..4e529af 100644
--- a/third_party/README.md
+++ b/third_party/README.md
@@ -202,7 +202,13 @@
 * License: MIT license
 
 
-[maven_model](http://maven.apache.org/ref/3.2.5/maven-model/)
+[maven](http://mvnrepository.com/artifact/org.apache.maven)
+-------------
+* Version: 3.3.3
+* License: Apache License 2.0
+
+
+[maven_model](http://maven.apache.org/ref/3.2.3/maven-model/)
 -------------
 * Version: 3.2.3
 * License: Apache License 2.0