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}