    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.release.plugin.mojos;
    018
    019import java.io.File;
    020import java.io.FileInputStream;
    021import java.io.FileOutputStream;
    022import java.io.IOException;
    023import java.io.PrintWriter;
    024import java.util.ArrayList;
    025import java.util.Collections;
    026import java.util.HashSet;
    027import java.util.List;
    028import java.util.Properties;
    029import java.util.Set;
    030
    031import org.apache.commons.codec.digest.DigestUtils;
    032import org.apache.commons.lang3.StringUtils;
    033import org.apache.commons.release.plugin.SharedFunctions;
    034import org.apache.maven.artifact.Artifact;
    035import org.apache.maven.plugin.AbstractMojo;
    036import org.apache.maven.plugin.MojoExecutionException;
    037import org.apache.maven.plugins.annotations.LifecyclePhase;
    038import org.apache.maven.plugins.annotations.Mojo;
    039import org.apache.maven.plugins.annotations.Parameter;
    040import org.apache.maven.project.MavenProject;
    041
    042/**
    043 * The purpose of this Maven mojo is to detach the artifacts generated by the maven-assembly-plugin,
    044 * which for the Apache Commons Project do not get uploaded to Nexus, and putting those artifacts
    045 * in the dev distribution location for Apache projects.
    046 *
    047 * @author chtompki
    048 * @since 1.0
    049 */
    050@Mojo(name = "detach-distributions",
    051        defaultPhase = LifecyclePhase.VERIFY,
    052        threadSafe = true,
    053        aggregator = true)
    054public class CommonsDistributionDetachmentMojo extends AbstractMojo {
    055
    056    /**
    057     * A list of "artifact types" in the Maven vernacular, to
    058     * be detached from the deployment. For the time being we want
    059     * all artifacts generated by the maven-assembly-plugin to be detached
    060     * from the deployment, namely *-src.zip, *-src.tar.gz, *-bin.zip,
    061     * *-bin.tar.gz, and the corresponding .asc pgp signatures.
    062     */
    063    private static final Set<String> ARTIFACT_TYPES_TO_DETACH;
    064    static {
    065        Set<String> hashSet = new HashSet<>();
    066        hashSet.add("zip");
    067        hashSet.add("tar.gz");
    068        hashSet.add("zip.asc");
    069        hashSet.add("tar.gz.asc");
    070        ARTIFACT_TYPES_TO_DETACH = Collections.unmodifiableSet(hashSet);
    071    }
    072
    073    /**
    074     * This list is supposed to hold the Maven references to the aforementioned artifacts so that we
    075     * can upload them to svn after they've been detached from the Maven deployment.
    076     */
    077    private final List<Artifact> detachedArtifacts = new ArrayList<>();
    078
    079    /**
    080     * A {@link Properties} of {@link Artifact} → {@link String} containing the sha256 signatures
    081     * for the individual artifacts, where the {@link Artifact} is represented as:
    082     * <code>groupId:artifactId:version:type=sha512</code>.
    083     */
    084    private final Properties artifactSha512s = new Properties();
    085
    086    /**
    087     * The maven project context injection so that we can get a hold of the variables at hand.
    088     */
    089    @Parameter(defaultValue = "${project}", required = true)
    090    private MavenProject project;
    091
    092    /**
    093     * The working directory in <code>target</code> that we use as a sandbox for the plugin.
    094     */
    095    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin",
    096            property = "commons.outputDirectory")
    097    private File workingDirectory;
    098
    099    /**
    100     * The subversion staging url to which we upload all of our staged artifacts.
    101     */
    102    @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl")
    103    private String distSvnStagingUrl;
    104
    105    /**
    106     * A parameter to generally avoid running unless it is specifically turned on by the consuming module.
    107     */
    108    @Parameter(defaultValue = "false", property = "commons.release.isDistModule")
    109    private Boolean isDistModule;
    110
    111    @Override
    112    public void execute() throws MojoExecutionException {
    113        if (!isDistModule) {
    114            getLog().info(
    115                    "This module is marked as a non distribution or assembly module, and the plugin will not run.");
    116            return;
    117        }
    118        if (StringUtils.isEmpty(distSvnStagingUrl)) {
    119            getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run.");
    120            return;
    121        }
    122        getLog().info("Detaching Assemblies");
    123        for (Object attachedArtifact : project.getAttachedArtifacts()) {
    124            putAttachedArtifactInSha512Map((Artifact) attachedArtifact);
    125            if (ARTIFACT_TYPES_TO_DETACH.contains(((Artifact) attachedArtifact).getType())) {
    126                detachedArtifacts.add((Artifact) attachedArtifact);
    127            }
    128        }
    129        if (detachedArtifacts.isEmpty()) {
    130            getLog().info("Current project contains no distributions. Not executing.");
    131            return;
    132        }
    133        for (Artifact artifactToRemove : detachedArtifacts) {
    134            project.getAttachedArtifacts().remove(artifactToRemove);
    135        }
    136        if (!workingDirectory.exists()) {
    137            SharedFunctions.initDirectory(getLog(), workingDirectory);
    138        }
    139        writeAllArtifactsInSha512PropertiesFile();
    140        copyRemovedArtifactsToWorkingDirectory();
    141        getLog().info("");
    142        hashArtifacts();
    143    }
    144
    145    /**
    146     * Takes an attached artifact and puts the signature in the map.
    147     * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo.
    148     * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha512 of the
    149     *                                artifact.
    150     */
    151    private void putAttachedArtifactInSha512Map(Artifact artifact) throws MojoExecutionException {
    152        try {
    153            String artifactKey = getArtifactKey(artifact);
    154            try (FileInputStream fis = new FileInputStream(artifact.getFile())) {
    155                artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(fis));
    156            }
    157        } catch (IOException e) {
    158            throw new MojoExecutionException(
    159                "Could not find artifact signature for: "
    160                    + artifact.getArtifactId()
    161                    + "-"
    162                    + artifact.getClassifier()
    163                    + "-"
    164                    + artifact.getVersion()
    165                    + " type: "
    166                    + artifact.getType(),
    167                e);
    168        }
    169    }
    170
    171    /**
    172     * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's.
    173     *
    174     * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
    175     */
    176    private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException {
    177        File propertiesFile = new File(workingDirectory, "sha512.properties");
    178        getLog().info("Writting " + propertiesFile);
    179        try (FileOutputStream fileWriter = new FileOutputStream(propertiesFile)) {
    180            artifactSha512s.store(fileWriter, "Release SHA-512s");
    181        } catch (IOException e) {
    182            throw new MojoExecutionException("Failure to write SHA-512's", e);
    183        }
    184    }
    185
    186    /**
    187     * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code>
    188     * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later.
    189     *
    190     * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
    191     *                                properly wrapped so that Maven can handle it.
    192     */
    193    private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException {
    194        final String wdAbsolutePath = workingDirectory.getAbsolutePath();
    195        getLog().info(
    196                "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath);
    197        for (Artifact artifact: detachedArtifacts) {
    198            File artifactFile = artifact.getFile();
    199            StringBuilder copiedArtifactAbsolutePath = new StringBuilder(wdAbsolutePath);
    200            copiedArtifactAbsolutePath.append("/");
    201            copiedArtifactAbsolutePath.append(artifactFile.getName());
    202            File copiedArtifact = new File(copiedArtifactAbsolutePath.toString());
    203            getLog().info("Copying: " + artifactFile.getName());
    204            SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact);
    205        }
    206    }
    207
    208    /**
    209     *  A helper method that creates sha256  and sha512 signature files for our detached artifacts in the
    210     *  <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by
    211     *  the {@link CommonsDistributionStagingMojo}.
    212     *
    213     * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
    214     *                                properly wrapped so that Maven can handle it.
    215     */
    216    private void hashArtifacts() throws MojoExecutionException {
    217        for (Artifact artifact : detachedArtifacts) {
    218            if (!artifact.getFile().getName().contains("asc")) {
    219                String artifactKey = getArtifactKey(artifact);
    220                try {
    221                    String digest;
    222                    // SHA-512
    223                    digest = artifactSha512s.getProperty(artifactKey.toString());
    224                    getLog().info(artifact.getFile().getName() + " sha512: " + digest);
    225                    try (PrintWriter printWriter = new PrintWriter(
    226                            getSha512FilePath(workingDirectory, artifact.getFile()))) {
    227                        printWriter.println(digest);
    228                    }
    229                } catch (IOException e) {
    230                    throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e);
    231                }
    232            }
    233        }
    234    }
    235
    236    /**
    237     * A helper method to create a file path for the <code>sha512</code> signature file from a given file.
    238     *
    239     * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file.
    240     * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file.
    241     * @return a {@link String} that is the absolute path to the <code>.sha512</code> file.
    242     */
    243    private String getSha512FilePath(File directory, File file) {
    244        StringBuilder buffer = new StringBuilder(directory.getAbsolutePath());
    245        buffer.append("/");
    246        buffer.append(file.getName());
    247        buffer.append(".sha512");
    248        return buffer.toString();
    249    }
    250
    251    /**
    252     * Generates the unique artifact key for storage in our sha256 map and sha512 map. For example,
    253     * commons-test-1.4-src.tar.gz should have it's name as the key.
    254     *
    255     * @param artifact the {@link Artifact} that we wish to generate a key for.
    256     * @return the generated key
    257     */
    258    private String getArtifactKey(Artifact artifact) {
    259        StringBuilder artifactKey = new StringBuilder();
    260        artifactKey.append(artifact.getArtifactId()).append('-')
    261                .append(artifact.getVersion()).append('-');
    262        if (artifact.hasClassifier()) {
    263            artifactKey.append(artifact.getClassifier()).append('-');
    264        }
    265        artifactKey.append(artifact.getType());
    266        return artifactKey.toString();
    267    }
    268}