/*
* Copyright 2015-present Facebook, Inc.
*
* 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.facebook.buck.maven;
import com.facebook.buck.jvm.java.HasMavenCoordinates;
import com.facebook.buck.jvm.java.MavenPublishable;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.SourcePathResolver;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.maven.model.Build;
import org.apache.maven.model.CiManagement;
import org.apache.maven.model.Contributor;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Developer;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.IssueManagement;
import org.apache.maven.model.License;
import org.apache.maven.model.MailingList;
import org.apache.maven.model.Model;
import org.apache.maven.model.Organization;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Prerequisites;
import org.apache.maven.model.Profile;
import org.apache.maven.model.Repository;
import org.apache.maven.model.Scm;
import org.apache.maven.model.building.DefaultModelBuilderFactory;
import org.apache.maven.model.building.DefaultModelBuildingRequest;
import org.apache.maven.model.building.ModelBuilder;
import org.apache.maven.model.building.ModelBuildingException;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.model.building.ModelBuildingResult;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
public class Pom {
private static final MavenXpp3Writer POM_WRITER = new MavenXpp3Writer();
private static final DefaultModelBuilderFactory MODEL_BUILDER_FACTORY =
new DefaultModelBuilderFactory();
/** Consistent with the value used in the implementation of {@link MavenXpp3Writer#write} */
private static final String POM_MODEL_VERSION = "4.0.0";
private final Model model;
private final MavenPublishable publishable;
private final SourcePathResolver pathResolver;
private final Path path;
public Pom(SourcePathResolver pathResolver, Path path, MavenPublishable buildRule) {
this.pathResolver = pathResolver;
this.path = path;
this.publishable = buildRule;
this.model = constructModel();
applyBuildRule();
}
public static Path generatePomFile(SourcePathResolver pathResolver, MavenPublishable rule)
throws IOException {
Path pom = getPomPath(rule);
Files.deleteIfExists(pom);
generatePomFile(pathResolver, rule, pom);
return pom;
}
private static Path getPomPath(HasMavenCoordinates rule) {
return rule.getProjectFilesystem()
.resolve(
BuildTargets.getGenPath(rule.getProjectFilesystem(), rule.getBuildTarget(), "%s.pom"));
}
@VisibleForTesting
static void generatePomFile(
SourcePathResolver pathResolver, MavenPublishable rule, Path optionallyExistingPom)
throws IOException {
new Pom(pathResolver, optionallyExistingPom, rule).flushToFile();
}
private void applyBuildRule() {
if (!HasMavenCoordinates.isMavenCoordsPresent(publishable)) {
throw new IllegalArgumentException(
"Cannot retrieve maven coordinates for target"
+ publishable.getBuildTarget().getFullyQualifiedName());
}
DefaultArtifact artifact = new DefaultArtifact(getMavenCoords(publishable).get());
Iterable<Artifact> deps =
FluentIterable.from(publishable.getMavenDeps())
.filter(HasMavenCoordinates::isMavenCoordsPresent)
.transform(input -> new DefaultArtifact(input.getMavenCoords().get()));
updateModel(artifact, deps);
}
private Model constructModel(File file, Model model) {
ModelBuilder modelBuilder = MODEL_BUILDER_FACTORY.newInstance();
try {
ModelBuildingRequest req = new DefaultModelBuildingRequest().setPomFile(file);
ModelBuildingResult modelBuildingResult = modelBuilder.build(req);
Model constructed = Preconditions.checkNotNull(modelBuildingResult.getRawModel());
return merge(model, constructed);
} catch (ModelBuildingException e) {
throw new RuntimeException(e);
}
}
private Model constructModel() {
File file = path.toFile();
Model model = new Model();
model.setModelVersion(POM_MODEL_VERSION);
if (publishable.getPomTemplate().isPresent()) {
model =
constructModel(
pathResolver.getAbsolutePath(publishable.getPomTemplate().get()).toFile(), model);
}
if (file.isFile()) {
model = constructModel(file, model);
}
return model;
}
private Model merge(Model first, @Nullable Model second) {
if (second == null) {
return first;
}
Model model = first.clone();
//---- Values from ModelBase
List<String> modules = second.getModules();
if (modules != null) {
for (String module : modules) {
model.addModule(module);
}
}
DistributionManagement distributionManagement = second.getDistributionManagement();
if (distributionManagement != null) {
model.setDistributionManagement(distributionManagement);
}
Properties properties = second.getProperties();
if (properties != null) {
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
model.addProperty((String) entry.getKey(), (String) entry.getValue());
}
}
DependencyManagement dependencyManagement = second.getDependencyManagement();
if (dependencyManagement != null) {
model.setDependencyManagement(dependencyManagement);
}
List<Dependency> dependencies = second.getDependencies();
if (dependencies != null) {
for (Dependency dependency : dependencies) {
model.addDependency(dependency);
}
}
List<Repository> repositories = second.getRepositories();
if (repositories != null) {
for (Repository repository : repositories) {
model.addRepository(repository);
}
}
List<Repository> pluginRepositories = second.getPluginRepositories();
if (pluginRepositories != null) {
for (Repository pluginRepository : pluginRepositories) {
model.addPluginRepository(pluginRepository);
}
}
// Ignore reports, reporting, and locations
//----- From Model
Parent parent = second.getParent();
if (parent != null) {
model.setParent(parent);
}
Organization organization = second.getOrganization();
if (organization != null) {
model.setOrganization(organization);
}
List<License> licenses = second.getLicenses();
Set<String> currentLicenseUrls = new HashSet<>();
if (model.getLicenses() != null) {
for (License license : model.getLicenses()) {
currentLicenseUrls.add(license.getUrl());
}
}
if (licenses != null) {
for (License license : licenses) {
if (!currentLicenseUrls.contains(license.getUrl())) {
model.addLicense(license);
currentLicenseUrls.add(license.getUrl());
}
}
}
List<Developer> developers = second.getDevelopers();
Set<String> currentDevelopers = new HashSet<>();
if (model.getDevelopers() != null) {
for (Developer developer : model.getDevelopers()) {
currentDevelopers.add(developer.getName());
}
}
if (developers != null) {
for (Developer developer : developers) {
if (!currentDevelopers.contains(developer.getName())) {
model.addDeveloper(developer);
currentDevelopers.add(developer.getName());
}
}
}
List<Contributor> contributors = second.getContributors();
Set<String> currentContributors = new HashSet<>();
if (model.getContributors() != null) {
for (Contributor contributor : model.getContributors()) {
currentDevelopers.add(contributor.getName());
}
}
if (contributors != null) {
for (Contributor contributor : contributors) {
if (!currentContributors.contains(contributor.getName())) {
model.addContributor(contributor);
currentContributors.add(contributor.getName());
}
}
}
List<MailingList> mailingLists = second.getMailingLists();
if (mailingLists != null) {
for (MailingList mailingList : mailingLists) {
model.addMailingList(mailingList);
}
}
Prerequisites prerequisites = second.getPrerequisites();
if (prerequisites != null) {
model.setPrerequisites(prerequisites);
}
Scm scm = second.getScm();
if (scm != null) {
model.setScm(scm);
}
String url = second.getUrl();
if (url != null) {
model.setUrl(url);
}
String description = second.getDescription();
if (description != null) {
model.setDescription(description);
}
IssueManagement issueManagement = second.getIssueManagement();
if (issueManagement != null) {
model.setIssueManagement(issueManagement);
}
CiManagement ciManagement = second.getCiManagement();
if (ciManagement != null) {
model.setCiManagement(ciManagement);
}
Build build = second.getBuild();
if (build != null) {
model.setBuild(build);
}
List<Profile> profiles = second.getProfiles();
Set<String> currentProfileIds = new HashSet<>();
if (model.getProfiles() != null) {
for (Profile profile : model.getProfiles()) {
currentProfileIds.add(profile.getId());
}
}
if (profiles != null) {
for (Profile profile : profiles) {
if (!currentProfileIds.contains(profile.getId())) {
model.addProfile(profile);
currentProfileIds.add(profile.getId());
}
}
}
return model;
}
private void updateModel(Artifact mavenCoordinates, Iterable<Artifact> deps) {
model.setGroupId(mavenCoordinates.getGroupId());
model.setArtifactId(mavenCoordinates.getArtifactId());
model.setVersion(mavenCoordinates.getVersion());
if (Strings.isNullOrEmpty(model.getName())) {
model.setName(mavenCoordinates.getArtifactId()); // better than nothing
}
// Dependencies
ImmutableMap<DepKey, Dependency> depIndex =
Maps.uniqueIndex(getModel().getDependencies(), DepKey::new);
for (Artifact artifactDep : deps) {
DepKey key = new DepKey(artifactDep);
Dependency dependency = depIndex.get(key);
if (dependency == null) {
dependency = key.createDependency();
getModel().addDependency(dependency);
}
updateDependency(dependency, artifactDep);
}
}
private static void updateDependency(Dependency dependency, Artifact providedMavenCoordinates) {
dependency.setVersion(providedMavenCoordinates.getVersion());
dependency.setClassifier(providedMavenCoordinates.getClassifier());
}
public void flushToFile() throws IOException {
getModel(); // Ensure model is initialized, reading file if necessary
Files.createDirectories(getPath().getParent());
try (Writer writer = Files.newBufferedWriter(getPath(), StandardCharsets.UTF_8)) {
POM_WRITER.write(writer, getModel());
}
}
private static Optional<String> getMavenCoords(BuildRule buildRule) {
if (buildRule instanceof HasMavenCoordinates) {
return ((HasMavenCoordinates) buildRule).getMavenCoords();
}
return Optional.empty();
}
public Model getModel() {
return model;
}
public Path getPath() {
return path;
}
private static final class DepKey {
private final String groupId;
private final String artifactId;
public DepKey(Artifact artifact) {
groupId = artifact.getGroupId();
artifactId = artifact.getArtifactId();
validate();
}
public DepKey(Dependency dependency) {
groupId = dependency.getGroupId();
artifactId = dependency.getArtifactId();
validate();
}
private void validate() {
Preconditions.checkNotNull(groupId);
Preconditions.checkNotNull(artifactId);
}
public Dependency createDependency() {
Dependency dependency = new Dependency();
dependency.setGroupId(groupId);
dependency.setArtifactId(artifactId);
return dependency;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DepKey)) {
return false;
}
DepKey depKey = (DepKey) o;
return Objects.equals(groupId, depKey.groupId)
&& Objects.equals(artifactId, depKey.artifactId);
}
@Override
public int hashCode() {
int result = groupId.hashCode();
result = 31 * result + artifactId.hashCode();
return result;
}
}
}