// Copyright 2016 The Bazel Authors. 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.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache;
import com.google.devtools.build.lib.bazel.repository.cache.RepositoryCache.KeyType;
import com.google.devtools.build.lib.bazel.repository.downloader.HttpDownloader;
import com.google.devtools.build.lib.rules.repository.WorkspaceAttributeMapper;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;
import java.util.Map;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import org.apache.maven.settings.Server;
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.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
/**
* Downloader for JAR files from Maven repositories.
* TODO(jingwen): standardize interface between this and HttpDownloader
*/
public class MavenDownloader extends HttpDownloader {
@Nullable
private String name;
@Nullable
private Path outputDirectory;
public MavenDownloader(RepositoryCache repositoryCache) {
super(repositoryCache);
}
/**
* Returns the name for this artifact-fetching rule.
*/
public String getName() {
return name;
}
/**
* Returns the directory that this artifact will be downloaded to.
*/
public Path getOutputDirectory() {
return outputDirectory;
}
/**
* Download the Maven artifact to the output directory. Returns the path to the jar.
*/
public Path download(String name, WorkspaceAttributeMapper mapper, Path outputDirectory,
MavenServerValue serverValue) throws IOException, EvalException {
this.name = name;
this.outputDirectory = outputDirectory;
String url = serverValue.getUrl();
Server server = serverValue.getServer();
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 + "'");
}
try {
artifact = new DefaultArtifact(artifactId);
} 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;
}
}
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();
ArtifactRequest artifactRequest = new ArtifactRequest();
artifactRequest.setArtifact(artifact);
artifactRequest.setRepositories(ImmutableList.of(repository));
try {
ArtifactResult artifactResult = system.resolveArtifact(session, artifactRequest);
artifact = artifactResult.getArtifact();
} catch (ArtifactResolutionException e) {
throw new IOException("Failed to fetch Maven dependency: " + e.getMessage());
}
Path downloadPath = outputDirectory.getRelative(artifact.getFile().getAbsolutePath());
// Verify checksum.
if (!Strings.isNullOrEmpty(sha1)) {
RepositoryCache.assertFileChecksum(sha1, downloadPath, KeyType.SHA1);
}
if (isCaching) {
repositoryCache.put(sha1, downloadPath, KeyType.SHA1);
}
return downloadPath;
}
private Path getDownloadDestination(Artifact artifact) {
String groupIdPath = artifact.getGroupId().replace('.', '/');
String artifactId = artifact.getArtifactId();
String version = artifact.getVersion();
String filename = artifactId + '-' + version + '.' + artifact.getExtension();
StringJoiner joiner = new StringJoiner("/");
joiner.add(groupIdPath).add(artifactId).add(version).add(filename);
return outputDirectory.getRelative(joiner.toString());
}
private static class MavenAuthentication implements Authentication {
private final Map<String, String> authenticationInfo;
private MavenAuthentication(Server server) {
Builder<String, String> builder = ImmutableMap.<String, String>builder();
// From https://maven.apache.org/settings.html: "If you use a private key to login to the
// server, make sure you omit the <password> element. Otherwise, the key will be ignored."
if (server.getPassword() != null) {
builder.put(AuthenticationContext.USERNAME, server.getUsername());
builder.put(AuthenticationContext.PASSWORD, server.getPassword());
} else if (server.getPrivateKey() != null) {
// getPrivateKey sounds like it returns the key, but it actually returns a path to it.
builder.put(AuthenticationContext.PRIVATE_KEY_PATH, server.getPrivateKey());
builder.put(AuthenticationContext.PRIVATE_KEY_PASSPHRASE, server.getPassphrase());
}
authenticationInfo = builder.build();
}
@Override
public void fill(
AuthenticationContext authenticationContext, String s, Map<String, String> map) {
for (Map.Entry<String, String> entry : authenticationInfo.entrySet()) {
authenticationContext.put(entry.getKey(), entry.getValue());
}
}
@Override
public void digest(AuthenticationDigest authenticationDigest) {
// No-op.
}
}
}