// Copyright 2015 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.workspace.maven;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Model;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Repository;
import org.apache.maven.model.building.DefaultModelProcessor;
import org.apache.maven.model.building.FileModelSource;
import org.apache.maven.model.building.ModelSource;
import org.apache.maven.model.io.DefaultModelReader;
import org.apache.maven.model.locator.DefaultModelLocator;
import org.apache.maven.model.resolution.UnresolvableModelException;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Resolves Maven dependencies.
*/
public class Resolver {
/**
* Exception thrown if an artifact coordinate could not be parsed.
*/
public static class InvalidArtifactCoordinateException extends Exception {
InvalidArtifactCoordinateException(String message) {
super(message);
}
}
public static Artifact getArtifact(String atrifactCoords)
throws InvalidArtifactCoordinateException {
try {
return new DefaultArtifact(atrifactCoords);
} catch (IllegalArgumentException e) {
throw new InvalidArtifactCoordinateException(e.getMessage());
}
}
public static Artifact getArtifact(Dependency dependency)
throws InvalidArtifactCoordinateException {
return getArtifact(dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"
+ dependency.getVersion());
}
private static final String COMPILE_SCOPE = "compile";
private final EventHandler handler;
private final DefaultModelResolver modelResolver;
// Mapping of maven_jar name to Rule.
private final Map<String, Rule> deps;
public Resolver(EventHandler handler, DefaultModelResolver resolver) {
this.handler = handler;
this.deps = Maps.newHashMap();
this.modelResolver = resolver;
}
/**
* Returns all maven_jars.
*/
public Collection<Rule> getRules() {
return deps.values();
}
public DefaultModelResolver getModelResolver() {
return modelResolver;
}
/**
* Given a local path to a Maven project, this attempts to find the transitive dependencies of
* the project.
* @param projectPath The path to search for Maven projects.
*/
public String resolvePomDependencies(String projectPath) {
DefaultModelProcessor processor = new DefaultModelProcessor();
processor.setModelLocator(new DefaultModelLocator());
processor.setModelReader(new DefaultModelReader());
File pom = processor.locatePom(new File(projectPath));
FileModelSource pomSource = new FileModelSource(pom);
// First resolve the model source locations.
resolveSourceLocations(pomSource);
// Next, fully resolve the models.
resolveEffectiveModel(pomSource, Sets.<String>newHashSet(), null);
return pom.getAbsolutePath();
}
/**
* Resolves an artifact as a root of a dependency graph.
*/
public void resolveArtifact(String artifactCoord) {
Artifact artifact;
ModelSource modelSource;
try {
artifact = getArtifact(artifactCoord);
modelSource = modelResolver.resolveModel(artifact);
} catch (UnresolvableModelException | InvalidArtifactCoordinateException e) {
handler.handle(Event.error(e.getMessage()));
return;
}
Rule rule = new Rule(artifact);
deps.put(rule.name(), rule); // add the artifact rule to the workspace
resolveEffectiveModel(modelSource, Sets.<String>newHashSet(), rule);
}
/**
* Resolves all dependencies from a given "model source," which could be either a URL or a local
* file.
* @return the model.
*/
@Nullable
public Model resolveEffectiveModel(ModelSource modelSource, Set<String> exclusions, Rule parent) {
Model model = modelResolver.getEffectiveModel(modelSource, handler);
if (model == null) {
return null;
}
for (Repository repo : model.getRepositories()) {
modelResolver.addRepository(repo);
}
for (Dependency dependency : model.getDependencies()) {
if (!dependency.getScope().equals(COMPILE_SCOPE)) {
continue;
}
if (dependency.isOptional()) {
continue;
}
if (exclusions.contains(dependency.getGroupId() + ":" + dependency.getArtifactId())) {
continue;
}
try {
Rule artifactRule = new Rule(getArtifact(dependency), dependency.getExclusions());
HashSet<String> localDepExclusions = new HashSet<>(exclusions);
localDepExclusions.addAll(artifactRule.getExclusions());
boolean isNewDependency = addArtifact(artifactRule, model.toString());
if (isNewDependency) {
ModelSource depModelSource = modelResolver.resolveModel(
dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion());
if (depModelSource != null) {
artifactRule.setRepository(depModelSource.getLocation(), handler);
artifactRule.setSha1(downloadSha1(artifactRule));
resolveEffectiveModel(depModelSource, localDepExclusions, artifactRule);
} else {
handler.handle(Event.error("Could not get a model for " + dependency));
}
}
if (parent != null) {
parent.addDependency(artifactRule);
parent.getDependencies().addAll(artifactRule.getDependencies());
} else {
addArtifact(artifactRule, modelSource.getLocation());
}
} catch (UnresolvableModelException | InvalidArtifactCoordinateException e) {
handler.handle(Event.error("Could not resolve dependency " + dependency.getGroupId()
+ ":" + dependency.getArtifactId() + ":" + dependency.getVersion() + ": "
+ e.getMessage()));
}
}
return model;
}
/**
* Find the POM files for a given pom's parent(s) and submodules.
*/
private void resolveSourceLocations(FileModelSource fileModelSource) {
Model model = modelResolver.getRawModel(fileModelSource, handler);
if (model == null) {
return;
}
// Self.
Parent parent = model.getParent();
if (model.getGroupId() == null) {
model.setGroupId(parent.getGroupId());
}
if (!modelResolver.putModelSource(
model.getGroupId(), model.getArtifactId(), fileModelSource)) {
return;
}
// Parent.
File pomDirectory = new File(fileModelSource.getLocation()).getParentFile();
if (parent != null && !parent.getArtifactId().equals(model.getArtifactId())) {
File parentPom;
try {
parentPom = new File(pomDirectory, parent.getRelativePath()).getCanonicalFile();
} catch (IOException e) {
handler.handle(Event.error("Unable to get canonical path of " + pomDirectory + " and "
+ parent.getRelativePath()));
return;
}
if (parentPom.exists()) {
resolveSourceLocations(new FileModelSource(parentPom));
}
}
// Submodules.
for (String module : model.getModules()) {
resolveSourceLocations(new FileModelSource(new File(pomDirectory, module + "/pom.xml")));
}
}
/**
* Adds the artifact to the map of deps, if it is not already there. Returns if the artifact
* was newly added. If the artifact was in the list at a different version, adds an comment
* about the desired version.
*/
public boolean addArtifact(Rule dependency, String parent) {
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())) {
existingDependency.addParent(parent + " wanted version " + dependency.version());
} else {
existingDependency.addParent(parent);
}
return false;
}
deps.put(artifactName, dependency);
dependency.addParent(parent);
return true;
}
static String getSha1Url(String url, String extension) {
return url.replaceAll(".pom$", "." + extension + ".sha1");
}
/**
* Downloads the SHA-1 for the given artifact.
*/
public String downloadSha1(Rule rule) {
String sha1Url = getSha1Url(rule.getUrl(), rule.getArtifact().getExtension());
try {
HttpURLConnection connection = (HttpURLConnection) new URL(sha1Url).openConnection();
connection.setInstanceFollowRedirects(true);
connection.connect();
return extractSha1(
CharStreams.toString(
new InputStreamReader(connection.getInputStream(), Charset.defaultCharset())));
} catch (IOException e) {
handler.handle(Event.warn("Failed to download the sha1 at " + sha1Url));
}
return null;
}
static String extractSha1(String sha1Contents) {
return sha1Contents.split("\\s+")[0];
}
}