Pull maven -src jars when available alongside jars re:
https://github.com/bazelbuild/bazel/issues/308

RELNOTES: None.
PiperOrigin-RevId: 166219871
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/JarDecompressor.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/JarDecompressor.java
index 703c4a3..26286ad 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/JarDecompressor.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/JarDecompressor.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.lib.bazel.repository;
 
 import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
 import com.google.devtools.build.lib.bazel.repository.DecompressorValue.Decompressor;
 import com.google.devtools.build.lib.rules.repository.RepositoryFunction;
 import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
@@ -34,34 +35,46 @@
   protected JarDecompressor() {
   }
 
-  /**
-   * The .jar can be used compressed, so this just exposes it in a way Bazel can use.
-   *
-   * <p>It moves the jar from some-name/x/y/z/foo.jar to some-name/jar/foo.jar and creates a
-   * BUILD.bazel file containing one entry: the .jar.
-   */
   @Override
   @Nullable
   public Path decompress(DecompressorDescriptor descriptor) throws RepositoryFunctionException {
-    // Example: archiveFile is external/some-name/foo.jar.
-    String baseName = descriptor.archivePath().getBaseName();
+    return decompressWithSrcjar(descriptor, Optional.absent());
+  }
 
+  /**
+   * The jar (and srcjar if available) can be used compressed, so this just exposes them in a way
+   * Bazel can use.
+   *
+   * <p>It moves the jar(s) from some-name/x/y/z/foo.jar to some-name/jar/foo.jar and creates a
+   * BUILD.bazel file containing only the jar(s).
+   */
+  public Path decompressWithSrcjar(
+      DecompressorDescriptor descriptor, Optional<DecompressorDescriptor> srcjarDescriptor)
+      throws RepositoryFunctionException {
     try {
+      // external/some-name/
       FileSystemUtils.createDirectoryAndParents(descriptor.repositoryPath());
-      // external/some-name/WORKSPACE.
+      // external/some-name/WORKSPACE
       RepositoryFunction.createWorkspaceFile(
           descriptor.repositoryPath(), descriptor.targetKind(), descriptor.targetName());
-      // external/some-name/jar.
+      // external/some-name/jar/
       Path jarDirectory = descriptor.repositoryPath().getRelative(getPackageName());
       FileSystemUtils.createDirectoryAndParents(jarDirectory);
-      // external/some-name/repository/jar/foo.jar is a symbolic link to the jar in
-      // external/some-name.
-      Path jarSymlink = jarDirectory.getRelative(baseName);
-      if (!jarSymlink.exists()) {
-        jarSymlink.createSymbolicLink(descriptor.archivePath());
-      }
-      // external/some-name/repository/jar/BUILD.bazel defines the //jar target.
+      // external/some-name/jar/BUILD.bazel defines the //jar target.
       Path buildFile = jarDirectory.getRelative("BUILD.bazel");
+
+      // Example: get foo.jar from external/some-name/foo.jar.
+      String baseName = descriptor.archivePath().getBaseName();
+      makeSymlink(descriptor);
+      String buildFileContents;
+      if (srcjarDescriptor.isPresent()) {
+        String srcjarBaseName = srcjarDescriptor.get().archivePath().getBaseName();
+        makeSymlink(srcjarDescriptor.get());
+        buildFileContents = createBuildFileWithSrcjar(baseName, srcjarBaseName);
+      } else {
+        buildFileContents = createBuildFile(baseName);
+      }
+
       FileSystemUtils.writeLinesAs(
           buildFile,
           Charset.forName("UTF-8"),
@@ -69,7 +82,7 @@
               + descriptor.targetKind()
               + " rule "
               + descriptor.targetName(),
-          createBuildFile(baseName));
+          buildFileContents);
       if (descriptor.executable()) {
         descriptor.archivePath().chmod(0755);
       }
@@ -80,6 +93,25 @@
     return descriptor.repositoryPath();
   }
 
+  /*
+   * Links some-name/x/y/z/foo.jar to some-name/jar/foo.jar (represented by {@code descriptor}
+   */
+  protected void makeSymlink(DecompressorDescriptor descriptor) throws RepositoryFunctionException {
+    try {
+      Path jarDirectory = descriptor.repositoryPath().getRelative(getPackageName());
+      // external/some-name/jar/foo.jar is a symbolic link to the jar in
+      // external/some-name.
+      Path jarSymlink = jarDirectory.getRelative(descriptor.archivePath().getBaseName());
+      if (!jarSymlink.exists()) {
+        jarSymlink.createSymbolicLink(descriptor.archivePath());
+      }
+    } catch (IOException e) {
+      throw new RepositoryFunctionException(
+          new IOException("Error auto-creating jar repo structure: " + e.getMessage()),
+          Transience.TRANSIENT);
+    }
+  }
+
   protected String getPackageName() {
     return "jar";
   }
@@ -99,4 +131,24 @@
             "    visibility = ['//visibility:public']",
             ")");
   }
+
+  protected String createBuildFileWithSrcjar(String baseName, String srcjarBaseName) {
+    return Joiner.on("\n")
+        .join(
+            "java_import(",
+            "    name = 'jar',",
+            "    jars = ['" + baseName + "'],",
+            "    srcjar = '" + srcjarBaseName + "',",
+            "    visibility = ['//visibility:public']",
+            ")",
+            "",
+            "filegroup(",
+            "    name = 'file',",
+            "    srcs = [",
+            "        '" + baseName + "',",
+            "        '" + srcjarBaseName + "',",
+            "    ],",
+            "    visibility = ['//visibility:public']",
+            ")");
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenDownloader.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenDownloader.java
index 264173e..e663a80 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenDownloader.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/MavenDownloader.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.bazel.repository;
 
+import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -72,9 +73,10 @@
   }
 
   /**
-   * Download the Maven artifact to the output directory. Returns the path to the jar.
+   * Download the Maven artifact to the output directory. Returns the path to the jar (and the
+   * srcjar if available).
    */
-  public Path download(String name, WorkspaceAttributeMapper mapper, Path outputDirectory,
+  public JarPaths download(String name, WorkspaceAttributeMapper mapper, Path outputDirectory,
       MavenServerValue serverValue) throws IOException, EvalException {
     this.name = name;
     this.outputDirectory = outputDirectory;
@@ -82,59 +84,73 @@
     String url = serverValue.getUrl();
     Server server = serverValue.getServer();
 
+    // Initialize maven artifacts
     Artifact artifact;
-    String artifactId = mapper.get("artifact", Type.STRING);
-    String sha1 = mapper.isAttributeValueExplicitlySpecified("sha1")
-        ? mapper.get("sha1", Type.STRING) : null;
-        if (sha1 != null && !KeyType.SHA1.isValid(sha1)) {
-          throw new IOException("Invalid SHA-1 for maven_jar " + name + ": '" + sha1 + "'");
-        }
+    String artifactCoords = mapper.get("artifact", Type.STRING);
+    String sha1 =
+        mapper.isAttributeValueExplicitlySpecified("sha1") ? mapper.get("sha1", Type.STRING) : null;
+    if (sha1 != null && !KeyType.SHA1.isValid(sha1)) {
+      throw new IOException("Invalid SHA-1 for maven_jar " + name + ": '" + sha1 + "'");
+    }
+
     try {
-      artifact = new DefaultArtifact(artifactId);
+      artifact = new DefaultArtifact(artifactCoords);
     } catch (IllegalArgumentException e) {
       throw new IOException(e.getMessage());
     }
- 
+
     boolean isCaching = repositoryCache.isEnabled() && KeyType.SHA1.isValid(sha1);
 
     if (isCaching) {
       Path downloadPath = getDownloadDestination(artifact);
       Path cachedDestination = repositoryCache.get(sha1, downloadPath, KeyType.SHA1);
       if (cachedDestination != null) {
-        return cachedDestination;
+        return new JarPaths(cachedDestination, Optional.absent());
       }
     }
 
+    // Setup env for fetching jars
     MavenConnector connector = new MavenConnector(outputDirectory.getPathString());
     RepositorySystem system = connector.newRepositorySystem();
     RepositorySystemSession session = connector.newRepositorySystemSession(system);
+    RemoteRepository repository =
+        new RemoteRepository.Builder(name, MavenServerValue.DEFAULT_ID, url)
+            .setAuthentication(new MavenAuthentication(server))
+            .build();
 
-    RemoteRepository repository = new RemoteRepository.Builder(
-        name, MavenServerValue.DEFAULT_ID, url)
-        .setAuthentication(new MavenAuthentication(server))
-        .build();
-    ArtifactRequest artifactRequest = new ArtifactRequest();
-
-    artifactRequest.setArtifact(artifact);
-    artifactRequest.setRepositories(ImmutableList.of(repository));
-
+    // Try fetching jar.
+    final Path jarDownload;
     try {
-      ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
-      artifact = artifactResult.getArtifact();
+      artifact = downloadArtifact(artifact, repository, session, system);
     } catch (ArtifactResolutionException e) {
       throw new IOException("Failed to fetch Maven dependency: " + e.getMessage());
     }
 
-    Path downloadPath = outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
+    // Try also fetching srcjar.
+    Artifact artifactWithSrcs = srcjarCoords(artifact);
+    try {
+      artifactWithSrcs = downloadArtifact(artifactWithSrcs, repository, session, system);
+    } catch (ArtifactResolutionException e) {
+      // Intentionally ignored - missing srcjar is not an error.
+    }
+
+    jarDownload = outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
     // Verify checksum.
     if (!Strings.isNullOrEmpty(sha1)) {
-      RepositoryCache.assertFileChecksum(sha1, downloadPath, KeyType.SHA1);
+      RepositoryCache.assertFileChecksum(sha1, jarDownload, KeyType.SHA1);
     }
 
     if (isCaching) {
-      repositoryCache.put(sha1, downloadPath, KeyType.SHA1);
+      repositoryCache.put(sha1, jarDownload, KeyType.SHA1);
     }
-    return downloadPath;
+
+    if (artifactWithSrcs.getFile() != null) {
+      Path srcjarDownload =
+          outputDirectory.getRelative(artifactWithSrcs.getFile().getAbsolutePath());
+      return new JarPaths(jarDownload, Optional.fromNullable(srcjarDownload));
+    } else {
+      return new JarPaths(jarDownload, Optional.absent());
+    }
   }
 
   private Path getDownloadDestination(Artifact artifact) {
@@ -149,6 +165,40 @@
     return outputDirectory.getRelative(joiner.toString());
   }
 
+  private Artifact srcjarCoords(Artifact jar) {
+    return new DefaultArtifact(
+        jar.getGroupId(), jar.getArtifactId(), "sources", jar.getExtension(), jar.getVersion());
+  }
+
+  /*
+   * Set up request for and resolve (retrieve to local repo) artifact
+   */
+  private Artifact downloadArtifact(
+      Artifact artifact,
+      RemoteRepository repository,
+      RepositorySystemSession session,
+      RepositorySystem system)
+      throws ArtifactResolutionException {
+    ArtifactRequest artifactRequest = new ArtifactRequest();
+    artifactRequest.setArtifact(artifact);
+    artifactRequest.setRepositories(ImmutableList.of(repository));
+    ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
+    return artifactResult.getArtifact();
+  }
+
+  /*
+   * Class for packaging srcjar and jar paths together when srcjar is available.
+   */
+  static class JarPaths {
+    final Path jar;
+    @Nullable final Optional<Path> srcjar;
+
+    private JarPaths(Path jar, Optional<Path> srcjar) {
+      this.jar = jar;
+      this.srcjar = srcjar;
+    }
+  }
+
   private static class MavenAuthentication implements Authentication {
 
     private final Map<String, String> authenticationInfo;
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 907227e..45aa2ba 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
@@ -14,9 +14,11 @@
 
 package com.google.devtools.build.lib.bazel.repository;
 
+import com.google.common.base.Optional;
 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.bazel.repository.MavenDownloader.JarPaths;
 import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue;
@@ -98,7 +100,7 @@
     if (env.valuesMissing()) {
       return null;
     }
-    
+
     Path outputDir = getExternalRepositoryDirectory(directories).getRelative(rule.getName());
     return createOutputTree(rule, outputDir, serverValue);
   }
@@ -110,10 +112,11 @@
 
     createDirectory(outputDirectory);
     String name = rule.getName();
-    Path repositoryJar;
+    final JarPaths repositoryJars;
     try {
-      repositoryJar = mavenDownloader.download(
-          name, WorkspaceAttributeMapper.of(rule), outputDirectory, serverValue);
+      repositoryJars =
+          mavenDownloader.download(
+              name, WorkspaceAttributeMapper.of(rule), outputDirectory, serverValue);
     } catch (IOException e) {
       throw new RepositoryFunctionException(e, Transience.TRANSIENT);
     } catch (EvalException e) {
@@ -121,13 +124,25 @@
     }
 
     // Add a WORKSPACE file & BUILD file to the Maven jar.
-    Path result = DecompressorValue.decompress(DecompressorDescriptor.builder()
+    DecompressorDescriptor jar = getDescriptorBuilder(name, repositoryJars.jar, outputDirectory);
+    DecompressorDescriptor srcjar =
+        repositoryJars.srcjar.isPresent()
+            ? getDescriptorBuilder(name, repositoryJars.srcjar.get(), outputDirectory)
+            : null;
+    JarDecompressor decompressor = (JarDecompressor) jar.getDecompressor();
+    Path result = decompressor.decompressWithSrcjar(jar, Optional.fromNullable(srcjar));
+    return RepositoryDirectoryValue.builder().setPath(result);
+  }
+
+  private DecompressorDescriptor getDescriptorBuilder(String name, Path jar, Path outputDirectory)
+      throws RepositoryFunctionException, InterruptedException {
+    return DecompressorDescriptor.builder()
         .setDecompressor(JarDecompressor.INSTANCE)
         .setTargetKind(MavenJarRule.NAME)
         .setTargetName(name)
-        .setArchivePath(repositoryJar)
-        .setRepositoryPath(outputDirectory).build());
-    return RepositoryDirectoryValue.builder().setPath(result);
+        .setArchivePath(jar)
+        .setRepositoryPath(outputDirectory)
+        .build();
   }
 
   /**
diff --git a/src/test/java/com/google/devtools/build/lib/rules/repository/JarDecompressorTest.java b/src/test/java/com/google/devtools/build/lib/rules/repository/JarDecompressorTest.java
index 37cfa8d..11dfdbb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/repository/JarDecompressorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/repository/JarDecompressorTest.java
@@ -16,9 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.base.Optional;
 import com.google.devtools.build.lib.bazel.repository.DecompressorDescriptor;
-import com.google.devtools.build.lib.bazel.repository.DecompressorValue;
 import com.google.devtools.build.lib.bazel.repository.JarDecompressor;
+import com.google.devtools.build.lib.bazel.rules.workspace.MavenJarRule;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
@@ -32,35 +33,69 @@
  */
 @RunWith(JUnit4.class)
 public class JarDecompressorTest {
-  private DecompressorDescriptor.Builder descriptorBuilder;
+  private DecompressorDescriptor.Builder jarDescriptorBuilder;
+  private DecompressorDescriptor.Builder srcjarDescriptorBuilder;
+  private JarDecompressor decompressor;
 
   @Before
   public void setUpFs() throws Exception {
     Scratch fs = new Scratch();
     Path dir = fs.dir("/whatever/external/tester");
     Path jar = fs.file("/foo.jar", "I'm a jar");
+    Path srcjar = fs.file("/foo-sources.jar", "I'm a source jar");
     FileSystemUtils.createDirectoryAndParents(dir);
-    descriptorBuilder = DecompressorDescriptor.builder()
-        .setDecompressor(JarDecompressor.INSTANCE)
-        .setTargetName("tester")
-        .setTargetKind("http_jar rule")
-        .setRepositoryPath(dir)
-        .setArchivePath(jar);
+    jarDescriptorBuilder =
+        DecompressorDescriptor.builder()
+            .setDecompressor(JarDecompressor.INSTANCE)
+            .setTargetName("tester")
+            .setTargetKind(MavenJarRule.NAME)
+            .setRepositoryPath(dir)
+            .setArchivePath(jar);
+    srcjarDescriptorBuilder =
+        DecompressorDescriptor.builder()
+            .setDecompressor(JarDecompressor.INSTANCE)
+            .setTargetName("tester")
+            .setTargetKind(MavenJarRule.NAME)
+            .setRepositoryPath(dir)
+            .setArchivePath(srcjar);
+    decompressor = (JarDecompressor) jarDescriptorBuilder.build().getDecompressor();
   }
 
   @Test
-  public void testTargets() throws Exception {
-    Path outputDir = DecompressorValue.decompress(descriptorBuilder.build());
+  public void testTargetsWithSources() throws Exception {
+    Path outputDir =
+        decompressor.decompressWithSrcjar(
+            jarDescriptorBuilder.build(), Optional.fromNullable(srcjarDescriptorBuilder.build()));
     assertThat(outputDir.exists()).isTrue();
+    assertThat(outputDir.getRelative("jar/foo.jar").exists()).isTrue();
+    assertThat(outputDir.getRelative("jar/foo-sources.jar").exists()).isTrue();
     String buildContent =
         new String(FileSystemUtils.readContentAsLatin1(outputDir.getRelative("jar/BUILD.bazel")));
     assertThat(buildContent).contains("java_import");
+    assertThat(buildContent).contains("srcjar = 'foo-sources.jar'");
     assertThat(buildContent).contains("filegroup");
   }
 
   @Test
+  public void testTargetsWithoutSources() throws Exception {
+    Path outputDir =
+        decompressor.decompressWithSrcjar(jarDescriptorBuilder.build(), Optional.absent());
+    assertThat(outputDir.exists()).isTrue();
+    assertThat(outputDir.getRelative("jar/foo.jar").exists()).isTrue();
+    assertThat(outputDir.getRelative("jar/foo-sources.jar").exists()).isFalse();
+    String buildContent =
+        new String(FileSystemUtils.readContentAsLatin1(outputDir.getRelative("jar/BUILD.bazel")));
+    assertThat(buildContent).contains("java_import");
+    assertThat(buildContent).doesNotContain("srcjar = 'foo-sources.jar'");
+    assertThat(buildContent).contains("filegroup");
+  }
+
+  @Test
+  // Note: WORKSPACE gen is not affected by presence or absence of Optional arg to
+  // decompressWithSrcjar
   public void testWorkspaceGen() throws Exception {
-    Path outputDir = DecompressorValue.decompress(descriptorBuilder.build());
+    Path outputDir =
+        decompressor.decompressWithSrcjar(jarDescriptorBuilder.build(), Optional.absent());
     assertThat(outputDir.exists()).isTrue();
     String workspaceContent = new String(
         FileSystemUtils.readContentAsLatin1(outputDir.getRelative("WORKSPACE")));
diff --git a/src/test/shell/bazel/maven_test.sh b/src/test/shell/bazel/maven_test.sh
index 04e217c..7a14c20 100755
--- a/src/test/shell/bazel/maven_test.sh
+++ b/src/test/shell/bazel/maven_test.sh
@@ -17,6 +17,8 @@
 # Test //external mechanisms
 #
 
+set -euo pipefail
+
 # Load the test setup defined in the parent directory
 CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 source "${CURRENT_DIR}/../integration_test_setup.sh" \
@@ -85,6 +87,27 @@
   expect_log "Tra-la!"
 }
 
+# makes sure both jar and srcjar are downloaded
+function test_maven_jar_downloads() {
+  serve_artifact com.example.carnivore carnivore 1.23
+
+  cat > WORKSPACE <<EOF
+maven_jar(
+    name = 'endangered',
+    artifact = "com.example.carnivore:carnivore:1.23",
+    repository = 'http://127.0.0.1:$fileserver_port/',
+)
+bind(name = 'mongoose', actual = '@endangered//jar')
+EOF
+
+  bazel run //zoo:ball-pit >& $TEST_log || fail "Expected run to succeed"
+  output_base="$(bazel info output_base)"
+  test -e "${output_base}/external/endangered/jar/carnivore-1.23.jar" \
+    || fail "jar not downloaded to expected place"
+  test -e "${output_base}/external/endangered/jar/carnivore-1.23-sources.jar" \
+    || fail "srcjar not downloaded to expected place"
+}
+
 function test_maven_jar_404() {
   setup_zoo
   serve_not_found
diff --git a/src/test/shell/bazel/remote_helpers.sh b/src/test/shell/bazel/remote_helpers.sh
index 7903063..ff76585 100755
--- a/src/test/shell/bazel/remote_helpers.sh
+++ b/src/test/shell/bazel/remote_helpers.sh
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+set -euo pipefail
+
 case "${PLATFORM}" in
   darwin|freebsd)
     function nc_l() {
@@ -68,8 +70,10 @@
 EOF
   ${bazel_javabase}/bin/javac $pkg_dir/Mongoose.java
   test_jar=$TEST_TMPDIR/libcarnivore.jar
+  test_srcjar=$TEST_TMPDIR/libcarnivore-sources.jar
   cd ${TEST_TMPDIR}
   ${bazel_javabase}/bin/jar cf $test_jar carnivore/Mongoose.class
+  ${bazel_javabase}/bin/jar cf $test_srcjar carnivore/Mongoose.java
   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 ' ')
@@ -168,11 +172,14 @@
   else
     make_test_jar
     local artifact=$test_jar
+    local srcjar_artifact=$test_srcjar
   fi
   maven_path=$PWD/$(echo $group_id | sed 's/\./\//g')/$artifact_id/$version
   mkdir -p $maven_path
   openssl sha1 $artifact > $maven_path/$artifact_id-$version.$packaging.sha1
+  openssl sha1 $srcjar_artifact > $maven_path/$artifact_id-$version-sources.$packaging.sha1
   mv $artifact $maven_path/$artifact_id-$version.$packaging
+  mv $srcjar_artifact $maven_path/$artifact_id-$version-sources.$packaging
 }
 
 function serve_artifact() {