    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 sha1 signatures
    081     * for the individual artifacts, where the {@link Artifact} is represented as:
    082     * <code>groupId:artifactId:version:type=sha1</code>.
    083     */
    084    private final Properties artifactSha1s = new Properties();
    085
    086    /**
    087     * A {@link Properties} of {@link Artifact} → {@link String} containing the sha256 signatures
    088     * for the individual artifacts, where the {@link Artifact} is represented as:
    089     * <code>groupId:artifactId:version:type=sha1</code>.
    090     */
    091    private final Properties artifactSha256s = new Properties();
    092
    093    /**
    094     * The maven project context injection so that we can get a hold of the variables at hand.
    095     */
    096    @Parameter(defaultValue = "${project}", required = true)
    097    private MavenProject project;
    098
    099    /**
    100     * The working directory in <code>target</code> that we use as a sandbox for the plugin.
    101     */
    102    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin",
    103            property = "commons.outputDirectory")
    104    private File workingDirectory;
    105
    106    /**
    107     * The subversion staging url to which we upload all of our staged artifacts.
    108     */
    109    @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl")
    110    private String distSvnStagingUrl;
    111
    112    /**
    113     * A parameter to generally avoid running unless it is specifically turned on by the consuming module.
    114     */
    115    @Parameter(defaultValue = "false", property = "commons.release.isDistModule")
    116    private Boolean isDistModule;
    117
    118    @Override
    119    public void execute() throws MojoExecutionException {
    120        if (!isDistModule) {
    121            getLog().info("This module is marked as a non distribution "
    122                    + "or assembly module, and the plugin will not run.");
    123            return;
    124        }
    125        if (StringUtils.isEmpty(distSvnStagingUrl)) {
    126            getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run.");
    127            return;
    128        }
    129        getLog().info("Detaching Assemblies");
    130        for (Object attachedArtifact : project.getAttachedArtifacts()) {
    131            putAttachedArtifactInSha1Map((Artifact) attachedArtifact);
    132            putAttachedArtifactInSha256Map((Artifact) attachedArtifact);
    133            if (ARTIFACT_TYPES_TO_DETACH.contains(((Artifact) attachedArtifact).getType())) {
    134                detachedArtifacts.add((Artifact) attachedArtifact);
    135            }
    136        }
    137        if (detachedArtifacts.isEmpty()) {
    138            getLog().info("Current project contains no distributions. Not executing.");
    139            return;
    140        }
    141        for (Artifact artifactToRemove : detachedArtifacts) {
    142            project.getAttachedArtifacts().remove(artifactToRemove);
    143        }
    144        if (!workingDirectory.exists()) {
    145            SharedFunctions.initDirectory(getLog(), workingDirectory);
    146        }
    147        writeAllArtifactsInSha1PropertiesFile();
    148        writeAllArtifactsInSha256PropertiesFile();
    149        copyRemovedArtifactsToWorkingDirectory();
    150        getLog().info("");
    151        hashArtifacts();
    152    }
    153
    154    /**
    155     * Takes an attached artifact and puts the signature in the map.
    156     * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo.
    157     * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha1 of the
    158     *                                artifact.
    159     */
    160    private void putAttachedArtifactInSha1Map(Artifact artifact) throws MojoExecutionException {
    161        try {
    162            String artifactKey = getArtifactKey(artifact);
    163            try (FileInputStream fis = new FileInputStream(artifact.getFile())) {
    164                artifactSha1s.put(artifactKey, DigestUtils.sha1Hex(fis));
    165            }
    166        } catch (IOException e) {
    167            throw new MojoExecutionException(
    168                "Could not find artifact signature for: "
    169                    + artifact.getArtifactId()
    170                    + "-"
    171                    + artifact.getClassifier()
    172                    + "-"
    173                    + artifact.getVersion()
    174                    + " type: "
    175                    + artifact.getType(),
    176                e);
    177        }
    178    }
    179
    180    /**
    181     * Takes an attached artifact and puts the signature in the map.
    182     * @param artifact is a Maven {@link Artifact} taken from the project at start time of mojo.
    183     * @throws MojoExecutionException if an {@link IOException} occurs when getting the sha1 of the
    184     *                                artifact.
    185     */
    186    private void putAttachedArtifactInSha256Map(Artifact artifact) throws MojoExecutionException {
    187        try {
    188            String artifactKey = getArtifactKey(artifact);
    189            try (FileInputStream fis = new FileInputStream(artifact.getFile())) {
    190                artifactSha256s.put(artifactKey, DigestUtils.sha256Hex(fis));
    191            }
    192        } catch (IOException e) {
    193            throw new MojoExecutionException(
    194                "Could not find artifact signature for: "
    195                    + artifact.getArtifactId()
    196                    + "-"
    197                    + artifact.getClassifier()
    198                    + "-"
    199                    + artifact.getVersion()
    200                    + " type: "
    201                    + artifact.getType(),
    202                e);
    203        }
    204    }
    205
    206    /**
    207     * Writes to ./target/commons-release-plugin/sha1.properties the artifact sha1's.
    208     *
    209     * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
    210     */
    211    private void writeAllArtifactsInSha1PropertiesFile() throws MojoExecutionException {
    212        File propertiesFile = new File(workingDirectory, "sha1.properties");
    213        getLog().info("Writting " + propertiesFile);
    214        try (FileOutputStream fileWriter = new FileOutputStream(propertiesFile)) {
    215            artifactSha1s.store(fileWriter, "Release SHA-1s");
    216        } catch (IOException e) {
    217            throw new MojoExecutionException("Failure to write SHA-1's", e);
    218        }
    219    }
    220
    221    /**
    222     * Writes to ./target/commons-release-plugin/sha256.properties the artifact sha256's.
    223     *
    224     * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
    225     */
    226    private void writeAllArtifactsInSha256PropertiesFile() throws MojoExecutionException {
    227        File propertiesFile = new File(workingDirectory, "sha256.properties");
    228        getLog().info("Writting " + propertiesFile);
    229        try (FileOutputStream fileWriter = new FileOutputStream(propertiesFile)) {
    230            artifactSha256s.store(fileWriter, "Release SHA-256s");
    231        } catch (IOException e) {
    232            throw new MojoExecutionException("Failure to write SHA-256's", e);
    233        }
    234    }
    235
    236    /**
    237     * A helper method to copy the newly detached artifacts to <code>target/commons-release-plugin</code>
    238     * so that the {@link CommonsDistributionStagingMojo} can find the artifacts later.
    239     *
    240     * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
    241     *                                properly wrapped so that Maven can handle it.
    242     */
    243    private void copyRemovedArtifactsToWorkingDirectory() throws MojoExecutionException {
    244        StringBuffer copiedArtifactAbsolutePath;
    245        final String wdAbsolutePath = workingDirectory.getAbsolutePath();
    246        getLog().info(
    247                "Copying " + detachedArtifacts.size() + " detached artifacts to working directory " + wdAbsolutePath);
    248        for (Artifact artifact: detachedArtifacts) {
    249            File artifactFile = artifact.getFile();
    250            copiedArtifactAbsolutePath = new StringBuffer(wdAbsolutePath);
    251            copiedArtifactAbsolutePath.append("/");
    252            copiedArtifactAbsolutePath.append(artifactFile.getName());
    253            File copiedArtifact = new File(copiedArtifactAbsolutePath.toString());
    254            getLog().info("Copying: " + artifactFile.getName());
    255            SharedFunctions.copyFile(getLog(), artifactFile, copiedArtifact);
    256        }
    257    }
    258
    259    /**
    260     *  A helper method that creates md5, sha1, and sha256  signature files for our detached artifacts in the
    261     *  <code>target/commons-release-plugin</code> directory for the purpose of being uploaded by
    262     *  the {@link CommonsDistributionStagingMojo}.
    263     *
    264     * @throws MojoExecutionException if some form of an {@link IOException} occurs, we want it
    265     *                                properly wrapped so that Maven can handle it.
    266     */
    267    private void hashArtifacts() throws MojoExecutionException {
    268        for (Artifact artifact : detachedArtifacts) {
    269            if (!artifact.getFile().getName().contains("asc")) {
    270                String artifactKey = getArtifactKey(artifact);
    271                try {
    272                    String digest;
    273                    // SHA-1
    274                    digest = artifactSha1s.getProperty(artifactKey.toString());
    275                    getLog().info(artifact.getFile().getName() + " sha1: " + digest);
    276                    try (PrintWriter printWriter = new PrintWriter(
    277                            getSha1FilePath(workingDirectory, artifact.getFile()))) {
    278                        printWriter.println(digest);
    279                    }
    280                    // SHA-256
    281                    digest = artifactSha256s.getProperty(artifactKey.toString());
    282                    getLog().info(artifact.getFile().getName() + " sha256: " + digest);
    283                    try (PrintWriter printWriter = new PrintWriter(
    284                            getSha256FilePath(workingDirectory, artifact.getFile()))) {
    285                        printWriter.println(digest);
    286                    }
    287                } catch (IOException e) {
    288                    throw new MojoExecutionException("Could not sign file: " + artifact.getFile().getName(), e);
    289                }
    290            }
    291        }
    292    }
    293
    294    /**
    295     * A helper method to create a file path for the <code>md5</code> signature file from a given file.
    296     *
    297     * @param directory is the {@link File} for the directory in which to make the <code>.md5</code> file.
    298     * @param file the {@link File} whose name we should use to create the <code>.md5</code> file.
    299     * @return a {@link String} that is the absolute path to the <code>.md5</code> file.
    300     */
    301    private String getMd5FilePath(File directory, File file) {
    302        StringBuffer buffer = new StringBuffer(directory.getAbsolutePath());
    303        buffer.append("/");
    304        buffer.append(file.getName());
    305        buffer.append(".md5");
    306        return buffer.toString();
    307    }
    308
    309    /**
    310     * A helper method to create a file path for the <code>sha1</code> signature file from a given file.
    311     *
    312     * @param directory is the {@link File} for the directory in which to make the <code>.sha1</code> file.
    313     * @param file the {@link File} whose name we should use to create the <code>.sha1</code> file.
    314     * @return a {@link String} that is the absolute path to the <code>.sha1</code> file.
    315     */
    316    private String getSha1FilePath(File directory, File file) {
    317        StringBuffer buffer = new StringBuffer(directory.getAbsolutePath());
    318        buffer.append("/");
    319        buffer.append(file.getName());
    320        buffer.append(".sha1");
    321        return buffer.toString();
    322    }
    323
    324    /**
    325     * A helper method to create a file path for the <code>sha1</code> signature file from a given file.
    326     *
    327     * @param directory is the {@link File} for the directory in which to make the <code>.sha1</code> file.
    328     * @param file the {@link File} whose name we should use to create the <code>.sha1</code> file.
    329     * @return a {@link String} that is the absolute path to the <code>.sha1</code> file.
    330     */
    331    private String getSha256FilePath(File directory, File file) {
    332        StringBuffer buffer = new StringBuffer(directory.getAbsolutePath());
    333        buffer.append("/");
    334        buffer.append(file.getName());
    335        buffer.append(".sha256");
    336        return buffer.toString();
    337    }
    338
    339    /**
    340     * Generates the unique artifact key for storage in our sha1 map and sha256 map. For example,
    341     * commons-test-1.4-src.tar.gz should have it's name as the key.
    342     *
    343     * @param artifact the {@link Artifact} that we wish to generate a key for.
    344     * @return the generated key
    345     */
    346    private String getArtifactKey(Artifact artifact) {
    347        StringBuffer artifactKey = new StringBuffer();
    348        artifactKey.append(artifact.getArtifactId()).append('-')
    349                .append(artifact.getVersion()).append('-');
    350        if (artifact.hasClassifier()) {
    351            artifactKey.append(artifact.getClassifier()).append('-');
    352        }
    353        artifactKey.append(artifact.getType());
    354        return artifactKey.toString();
    355    }
    356}