MavenArtifactHelper.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2018 wcm.io
 * %%
 * 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.
 * #L%
 */
package io.wcm.devops.conga.tooling.maven.plugin.util;

import static org.apache.maven.artifact.Artifact.SCOPE_COMPILE;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.ArtifactType;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
import org.eclipse.aether.resolution.ArtifactDescriptorResult;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

import io.wcm.devops.conga.generator.spi.context.PluginContextOptions;
import io.wcm.devops.conga.model.environment.Environment;
import io.wcm.devops.conga.tooling.maven.plugin.urlfile.MavenUrlFilePlugin;

/**
 * Helper for resolving maven artifacts.
 */
public class MavenArtifactHelper {

  private final MavenProject project;
  private final RepositorySystem repoSystem;
  private final RepositorySystemSession repoSession;
  private final List<RemoteRepository> remoteRepos;
  private final Map<String, String> artifactTypeMappings;
  private final List<String> environmentDependencyUrls;
  private final PluginContextOptions pluginContextOptions;

  /**
   * @param environment CONGA environment
   * @param pluginContextOptions Plugin context options
   */
  public MavenArtifactHelper(Environment environment, PluginContextOptions pluginContextOptions) {
    MavenContext mavenContext = (MavenContext)pluginContextOptions.getContainerContext();
    this.project = mavenContext.getProject();
    this.repoSystem = mavenContext.getRepoSystem();
    this.repoSession = mavenContext.getRepoSession();
    this.remoteRepos = mavenContext.getRemoteRepos();
    this.artifactTypeMappings = mavenContext.getArtifactTypeMappings();
    this.environmentDependencyUrls = environment != null ? environment.getDependencies() : List.of();
    this.pluginContextOptions = pluginContextOptions;
  }

  /**
   * Get Maven artifact for given artifact coordinates.
   * @param artifactCoords Artifact coordinates in either Maven-style or Pax URL-style.
   * @return Maven artifact
   * @throws IOException If artifact resolution was not successful
   */
  public Artifact resolveArtifact(String artifactCoords) throws IOException {
    Artifact artifact;
    if (StringUtils.contains(artifactCoords, "/")) {
      artifact = getArtifactFromMavenCoordinatesPaxUrlStyle(artifactCoords);
    }
    else {
      artifact = getArtifactFromMavenCoordinates(artifactCoords);
    }
    return resolveArtifact(artifact);
  }

  /**
   * Get Maven artifact for given artifact coordinates.
   * @param groupId Group Id
   * @param artifactId Artifact Id
   * @param type Type
   * @param classifier Classifier
   * @param version Version
   * @return Artifact
   * @throws IOException If dependency resolution fails
   */
  @SuppressWarnings("PMD.UseObjectForClearerAPI")
  public Artifact resolveArtifact(String groupId, String artifactId, String type, String classifier, String version) throws IOException {
    Artifact artifact = createArtifact(groupId, artifactId, type, classifier, version);
    return resolveArtifact(artifact);
  }

  /**
   * Get transitive compile dependencies of given artifact.
   * @param artifact Maven artifact
   * @return List of artifact dependencies
   * @throws IOException If artifact resolution was not successful
   */
  public List<Artifact> getTransitiveDependencies(Artifact artifact) throws IOException {
    List<Artifact> dependencies = new ArrayList<>();
    ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
    descriptorRequest.setArtifact(artifact);
    descriptorRequest.setRepositories(remoteRepos);
    try {
      ArtifactDescriptorResult result = repoSystem.readArtifactDescriptor(repoSession, descriptorRequest);
      for (Dependency dependency : result.getDependencies()) {
        if (StringUtils.equals(dependency.getScope(), SCOPE_COMPILE)) {
          Artifact resolvedArtifact = resolveArtifact(dependency.getArtifact());
          dependencies.add(resolvedArtifact);
        }
      }
    }
    catch (ArtifactDescriptorException ex) {
      throw new IOException("Unable to get artifact descriptor for: '" + artifact + "': " + ex.getMessage(), ex);
    }
    return dependencies;
  }

  /**
   * Transform a list of dependency urls to a list of references maven artifacts including their transitive
   * dependencies.
   * @param dependencyUrls Dependency URLs
   * @return List of artifacts
   * @throws IOException If dependency resolution fails
   */
  public List<Artifact> dependencyUrlsToArtifactsWithTransitiveDependencies(Collection<String> dependencyUrls) throws IOException {
    List<Artifact> artifacts = new ArrayList<>();
    for (String dependencyUrl : environmentDependencyUrls) {
      String resolvedDependencyUrl = ClassLoaderUtil.resolveDependencyUrl(dependencyUrl, pluginContextOptions);
      if (!StringUtils.startsWith(resolvedDependencyUrl, MavenUrlFilePlugin.PREFIX)) {
        continue;
      }

      String mavenCoords = MavenUrlFilePlugin.getMavenCoords(resolvedDependencyUrl);
      Artifact artifact = resolveArtifact(mavenCoords);
      artifacts.add(artifact);
      artifacts.addAll(getTransitiveDependencies(artifact));
    }
    return artifacts;
  }

  /**
   * Parse coordinates following definition from https://maven.apache.org/pom.html#Maven_Coordinates
   * @param artifactCoords Artifact coordinates
   * @return Artifact object
   */
  @SuppressWarnings("PMD.PreserveStackTrace")
  private Artifact getArtifactFromMavenCoordinates(String artifactCoords) throws IOException {
    try {
      Artifact artifact = new DefaultArtifact(artifactCoords);
      return createArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact.getExtension(), artifact.getClassifier(), artifact.getVersion());
    }
    catch (IllegalArgumentException ex) {
      throw new IOException(ex.getMessage());
    }
  }

  /**
   * Parse coordinates in slingstart/Pax URL style following definition from https://ops4j1.jira.com/wiki/x/CoA6
   * @param artifactCoords Artifact coordinates
   * @return Artifact object
   */
  private Artifact getArtifactFromMavenCoordinatesPaxUrlStyle(String artifactCoords) throws IOException {
    String[] parts = StringUtils.splitPreserveAllTokens(artifactCoords, "/");

    String version = null;
    String packaging = null;
    String classifier = null;

    switch (parts.length) {
      case 2:
        // groupId/artifactId
        break;

      case 3:
        // groupId/artifactId/version
        version = StringUtils.defaultIfBlank(parts[2], null);
        break;

      case 4:
        // groupId/artifactId/version/type
        packaging = StringUtils.defaultIfBlank(parts[3], null);
        version = StringUtils.defaultIfBlank(parts[2], null);
        break;

      case 5:
        // groupId/artifactId/version/type/classifier
        packaging = StringUtils.defaultIfBlank(parts[3], null);
        classifier = StringUtils.defaultIfBlank(parts[4], null);
        version = StringUtils.defaultIfBlank(parts[2], null);
        break;

      default:
        throw new IOException("Invalid artifact: " + artifactCoords);
    }

    String groupId = StringUtils.defaultIfBlank(parts[0], null);
    String artifactId = StringUtils.defaultIfBlank(parts[1], null);

    return createArtifact(groupId, artifactId, packaging, classifier, version);
  }

  private Artifact createArtifact(String groupId, String artifactId, String type, String classifier, String version) throws IOException {

    String artifactTypeString = Objects.toString(type, "jar");
    String artifactExtension = artifactTypeString;

    ArtifactType artifactType = repoSession.getArtifactTypeRegistry().get(artifactExtension);
    if (artifactType != null) {
      artifactExtension = artifactType.getExtension();
    }

    // apply custom mapping from artifact type to extension if defined in plugin config
    if (artifactTypeMappings != null && artifactTypeMappings.containsKey(artifactExtension)) {
      artifactExtension = artifactTypeMappings.get(artifactExtension);
    }

    String artifactVersion = version;
    if (artifactVersion == null) {
      artifactVersion = resolveArtifactVersion(groupId, artifactId, artifactTypeString, classifier);
    }

    if (StringUtils.isBlank(groupId) || StringUtils.isBlank(artifactId) || StringUtils.isBlank(artifactVersion)) {
      throw new IOException("Invalid Maven artifact reference: "
          + "artifactId=" + artifactId + ", "
          + "groupId=" + groupId + ", "
          + "version=" + artifactVersion + ", "
          + "extension=" + artifactExtension + ", "
          + "classifier=" + classifier + ","
          + "type=" + artifactType);
    }

    return new DefaultArtifact(groupId, artifactId, classifier, artifactExtension, artifactVersion, artifactType);
  }

  private Artifact resolveArtifact(Artifact artifact) throws IOException {
    ArtifactRequest artifactRequest = new ArtifactRequest();
    artifactRequest.setArtifact(artifact);
    artifactRequest.setRepositories(remoteRepos);
    try {
      ArtifactResult result = repoSystem.resolveArtifact(repoSession, artifactRequest);
      return result.getArtifact();
    }
    catch (final ArtifactResolutionException ex) {
      throw new IOException("Unable to get artifact for '" + artifact + "': " + ex.getMessage(), ex);
    }
  }

  private String resolveArtifactVersion(String groupId, String artifactId, String type, String classifier) throws IOException {
    String version = findVersionInMavenProject(groupId, artifactId, type, classifier);
    if (version == null) {
      version = findVersionInEnvironmentDependencies(groupId, artifactId, type, classifier);
    }
    return version;
  }

  private String findVersionInMavenProject(String groupId, String artifactId, String type, String classifier) {
    Set<org.apache.maven.artifact.Artifact> dependencies = project.getArtifacts();
    if (dependencies != null) {
      for (org.apache.maven.artifact.Artifact dependency : dependencies) {
        if (artifactEquals(dependency, artifactId, groupId, type, classifier)) {
          return dependency.getVersion();
        }
      }
    }
    return null;
  }

  private boolean artifactEquals(org.apache.maven.artifact.Artifact dependency, String artifactId, String groupId, String type, String classifier) {
    return StringUtils.equals(dependency.getGroupId(), groupId)
        && StringUtils.equals(dependency.getArtifactId(), artifactId)
        && StringUtils.equals(StringUtils.defaultString(dependency.getClassifier()), StringUtils.defaultString(classifier))
        && StringUtils.equals(dependency.getType(), type);
  }

  private String findVersionInEnvironmentDependencies(String groupId, String artifactId, String packaging, String classifier) throws IOException {
    List<Artifact> dependencies = dependencyUrlsToArtifactsWithTransitiveDependencies(environmentDependencyUrls);

    for (Artifact dependency : dependencies) {
      if (artifactEquals(dependency, groupId, artifactId, packaging, classifier)) {
        return dependency.getVersion();
      }
    }

    return null;
  }

  private boolean artifactEquals(Artifact dependency, String groupId, String artifactId, String type, String classifier) {
    return StringUtils.equals(dependency.getGroupId(), groupId)
        && StringUtils.equals(dependency.getArtifactId(), artifactId)
        && StringUtils.equals(StringUtils.defaultString(dependency.getClassifier()), StringUtils.defaultString(classifier))
        && StringUtils.equals(dependency.getExtension(), type);
  }

}