CommonsDistributionDetachmentMojo.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.release.plugin.mojos;

  18. import java.io.File;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.OutputStream;
  22. import java.io.PrintWriter;
  23. import java.nio.file.Files;
  24. import java.util.ArrayList;
  25. import java.util.Collections;
  26. import java.util.HashSet;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.Set;

  30. import org.apache.commons.codec.digest.DigestUtils;
  31. import org.apache.commons.collections4.properties.SortedProperties;
  32. import org.apache.commons.lang3.StringUtils;
  33. import org.apache.commons.lang3.reflect.MethodUtils;
  34. import org.apache.commons.release.plugin.SharedFunctions;
  35. import org.apache.maven.artifact.Artifact;
  36. import org.apache.maven.plugin.AbstractMojo;
  37. import org.apache.maven.plugin.MojoExecutionException;
  38. import org.apache.maven.plugins.annotations.LifecyclePhase;
  39. import org.apache.maven.plugins.annotations.Mojo;
  40. import org.apache.maven.plugins.annotations.Parameter;
  41. import org.apache.maven.project.MavenProject;

  42. /**
  43.  * The purpose of this Maven mojo is to detach the artifacts generated by the maven-assembly-plugin,
  44.  * which for the Apache Commons Project do not get uploaded to Nexus, and putting those artifacts
  45.  * in the dev distribution location for Apache projects.
  46.  *
  47.  * @since 1.0
  48.  */
  49. @Mojo(name = "detach-distributions",
  50.         defaultPhase = LifecyclePhase.VERIFY,
  51.         threadSafe = true,
  52.         aggregator = true)
  53. public class CommonsDistributionDetachmentMojo extends AbstractMojo {

  54.     /**
  55.      * A list of "artifact types" in the Maven vernacular, to
  56.      * be detached from the deployment. For the time being we want
  57.      * all artifacts generated by the maven-assembly-plugin to be detached
  58.      * from the deployment, namely *-src.zip, *-src.tar.gz, *-bin.zip,
  59.      * *-bin.tar.gz, and the corresponding .asc pgp signatures.
  60.      */
  61.     private static final Set<String> ARTIFACT_TYPES_TO_DETACH;
  62.     static {
  63.         final Set<String> hashSet = new HashSet<>();
  64.         hashSet.add("zip");
  65.         hashSet.add("tar.gz");
  66.         hashSet.add("zip.asc");
  67.         hashSet.add("tar.gz.asc");
  68.         ARTIFACT_TYPES_TO_DETACH = Collections.unmodifiableSet(hashSet);
  69.     }

  70.     /**
  71.      * This list is supposed to hold the Maven references to the aforementioned artifacts so that we
  72.      * can upload them to svn after they've been detached from the Maven deployment.
  73.      */
  74.     private final List<Artifact> detachedArtifacts = new ArrayList<>();

  75.     /**
  76.      * A {@link SortedProperties} of {@link Artifact} → {@link String} containing the sha512 signatures
  77.      * for the individual artifacts, where the {@link Artifact} is represented as:
  78.      * <code>groupId:artifactId:version:type=sha512</code>.
  79.      */
  80.     private final SortedProperties artifactSha512s = new SortedProperties();

  81.     /**
  82.      * The maven project context injection so that we can get a hold of the variables at hand.
  83.      */
  84.     @Parameter(defaultValue = "${project}", required = true)
  85.     private MavenProject project;

  86.     /**
  87.      * The working directory in <code>target</code> that we use as a sandbox for the plugin.
  88.      */
  89.     @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin",
  90.             property = "commons.outputDirectory")
  91.     private File workingDirectory;

  92.     /**
  93.      * The subversion staging url to which we upload all of our staged artifacts.
  94.      */
  95.     @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl")
  96.     private String distSvnStagingUrl;

  97.     /**
  98.      * A parameter to generally avoid running unless it is specifically turned on by the consuming module.
  99.      */
  100.     @Parameter(defaultValue = "false", property = "commons.release.isDistModule")
  101.     private Boolean isDistModule;

  102.     @Override
  103.     public void execute() throws MojoExecutionException {
  104.         if (!isDistModule) {
  105.             getLog().info("This module is marked as a non distribution or assembly module, and the plugin will not run.");
  106.             return;
  107.         }
  108.         if (StringUtils.isEmpty(distSvnStagingUrl)) {
  109.             getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run.");
  110.             return;
  111.         }
  112.         getLog().info("Detaching Assemblies");
  113.         for (final Artifact attachedArtifact : project.getAttachedArtifacts()) {
  114.             putAttachedArtifactInSha512Map(attachedArtifact);
  115.             if (ARTIFACT_TYPES_TO_DETACH.contains(attachedArtifact.getType())) {
  116.                 detachedArtifacts.add(attachedArtifact);
  117.             }
  118.         }
  119.         if (detachedArtifacts.isEmpty()) {
  120.             getLog().info("Current project contains no distributions. Not executing.");
  121.             return;
  122.         }
  123.         //
  124.         // PROBLEM CODE for Maven >= 3.8.3
  125.         // https://issues.apache.org/jira/browse/MNG-7316
  126.         try {
  127.             // (1) Try the normal way
  128.             // Maven 3.8.3 throws an exception here because MavenProject.getAttachedArtifacts()
  129.             // returns an IMMUTABLE collection.
  130.             project.getAttachedArtifacts().removeAll(detachedArtifacts);
  131.         } catch (final UnsupportedOperationException e) {
  132.             // (2) HACK workaround for https://issues.apache.org/jira/browse/MNG-7316
  133.             final ArrayList<Artifact> arrayList = new ArrayList<>(project.getAttachedArtifacts());
  134.             arrayList.removeAll(detachedArtifacts);
  135.             try {
  136.                 // MavenProject#setAttachedArtifacts(List) is protected
  137.                 MethodUtils.invokeMethod(project, true, "setAttachedArtifacts", arrayList);
  138.             } catch (final ReflectiveOperationException roe) {
  139.                 throw new MojoExecutionException(roe);
  140.             }
  141.         }
  142.         if (!workingDirectory.exists()) {
  143.             SharedFunctions.initDirectory(getLog(), workingDirectory);
  144.         }
  145.         writeAllArtifactsInSha512PropertiesFile();
  146.         copyRemovedArtifactsToWorkingDirectory();
  147.         getLog().info("");
  148.         hashArtifacts();
  149.     }

  150.     /**
  151.      * Takes an attached artifact and puts the signature in the map.
  152.      * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo.
  153.      * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha512 of the
  154.      *                                artifact.
  155.      */
  156.     private void putAttachedArtifactInSha512Map(final Artifact artifact) throws MojoExecutionException {
  157.         try {
  158.             final String artifactKey = getArtifactKey(artifact);
  159.             if (!artifactKey.endsWith(".asc")) { // .asc files don't need hashes
  160.                 try (InputStream fis = Files.newInputStream(artifact.getFile().toPath())) {
  161.                     artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(fis));
  162.                 }
  163.             }
  164.         } catch (final IOException e) {
  165.             throw new MojoExecutionException(
  166.                 "Could not find artifact signature for: "
  167.                     + artifact.getArtifactId()
  168.                     + "-"
  169.                     + artifact.getClassifier()
  170.                     + "-"
  171.                     + artifact.getVersion()
  172.                     + " type: "
  173.                     + artifact.getType(),
  174.                 e);
  175.         }
  176.     }

  177.     /**
  178.      * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's.
  179.      *
  180.      * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
  181.      */
  182.     private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException {
  183.         final File propertiesFile = new File(workingDirectory, "sha512.properties");
  184.         getLog().info("Writing " + propertiesFile);
  185.         try (OutputStream fileWriter = Files.newOutputStream(propertiesFile.toPath())) {
  186.             artifactSha512s.store(fileWriter, "Release SHA-512s");
  187.         } catch (final IOException e) {
  188.             throw new MojoExecutionException("Failure to write SHA-512's", e);
  189.         }
  190.     }

  191.     /**
  192.      * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code>
  193.      * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later.
  194.      *
  195.      * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
  196.      *                                properly wrapped so that Maven can handle it.
  197.      */
  198.     private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException {
  199.         final String wdAbsolutePath = workingDirectory.getAbsolutePath();
  200.         getLog().info(
  201.                 "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath);
  202.         for (final Artifact artifact: detachedArtifacts) {
  203.             final File artifactFile = artifact.getFile();
  204.             final StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath);
  205.             copiedArtifactAbsolutePath.append("/");
  206.             copiedArtifactAbsolutePath.append(artifactFile.getName());
  207.             final File copiedArtifact = new File(copiedArtifactAbsolutePath.toString());
  208.             getLog().info("Copying: " + artifactFile.getName());
  209.             SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact);
  210.         }
  211.     }

  212.     /**
  213.      *  A helper method that creates sha512 signature files for our detached artifacts in the
  214.      *  <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by
  215.      *  the {@link CommonsDistributionStagingMojo}.
  216.      *
  217.      * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
  218.      *                                properly wrapped so that Maven can handle it.
  219.      */
  220.     private void hashArtifacts() throws MojoExecutionException {
  221.         for (final Artifact artifact : detachedArtifacts) {
  222.             if (!artifact.getFile().getName().toLowerCase(Locale.ROOT).contains("asc")) {
  223.                 final String artifactKey = getArtifactKey(artifact);
  224.                 try {
  225.                     final String digest;
  226.                     // SHA-512
  227.                     digest = artifactSha512s.getProperty(artifactKey.toString());
  228.                     getLog().info(artifact.getFile().getName() + " sha512: " + digest);
  229.                     try (PrintWriter printWriter = new PrintWriter(
  230.                             getSha512FilePath(workingDirectory, artifact.getFile()))) {
  231.                         printWriter.println(digest);
  232.                     }
  233.                 } catch (final IOException e) {
  234.                     throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e);
  235.                 }
  236.             }
  237.         }
  238.     }

  239.     /**
  240.      * A helper method to create a file path for the <code>sha512</code> signature file from a given file.
  241.      *
  242.      * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file.
  243.      * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file.
  244.      * @return a {@link String} that is the absolute path to the <code>.sha512</code> file.
  245.      */
  246.     private String getSha512FilePath(final File directory, final File file) {
  247.         final StringBuilder buffer = new StringBuilder(directory.getAbsolutePath());
  248.         buffer.append("/");
  249.         buffer.append(file.getName());
  250.         buffer.append(".sha512");
  251.         return buffer.toString();
  252.     }

  253.     /**
  254.      * Generates the unique artifact key for storage in our sha512 map. For example,
  255.      * commons-test-1.4-src.tar.gz should have its name as the key.
  256.      *
  257.      * @param artifact the {@link Artifact} that we wish to generate a key for.
  258.      * @return the generated key
  259.      */
  260.     private String getArtifactKey(final Artifact artifact) {
  261.         return artifact.getFile().getName();
  262.     }
  263. }