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=sha256</code>.
083     */
084    private final Properties artifactSha256s = 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=sha512</code>.
090     */
091    private final Properties artifactSha512s = 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(
122                    "This module is marked as a non distribution 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            putAttachedArtifactInSha256Map((Artifact) attachedArtifact);
132            putAttachedArtifactInSha512Map((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        writeAllArtifactsInSha256PropertiesFile();
148        writeAllArtifactsInSha512PropertiesFile();
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 sha256 of the
158     *                                artifact.
159     */
160    private void putAttachedArtifactInSha256Map(Artifact artifact) throws MojoExecutionException {
161        try {
162            String artifactKey = getArtifactKey(artifact);
163            try (FileInputStream fis = new FileInputStream(artifact.getFile())) {
164                artifactSha256s.put(artifactKey, DigestUtils.sha256Hex(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 sha512 of the
184     *                                artifact.
185     */
186    private void putAttachedArtifactInSha512Map(Artifact artifact) throws MojoExecutionException {
187        try {
188            String artifactKey = getArtifactKey(artifact);
189            try (FileInputStream fis = new FileInputStream(artifact.getFile())) {
190                artifactSha512s.put(artifactKey, DigestUtils.sha512Hex(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/sha256.properties the artifact sha256's.
208     *
209     * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
210     */
211    private void writeAllArtifactsInSha256PropertiesFile() throws MojoExecutionException {
212        File propertiesFile = new File(workingDirectory, "sha256.properties");
213        getLog().info("Writting " + propertiesFile);
214        try (FileOutputStream fileWriter = new FileOutputStream(propertiesFile)) {
215            artifactSha256s.store(fileWriter, "Release SHA-256s");
216        } catch (IOException e) {
217            throw new MojoExecutionException("Failure to write SHA-256's", e);
218        }
219    }
220
221    /**
222     * Writes to ./target/commons-release-plugin/sha512.properties the artifact sha512's.
223     *
224     * @throws MojoExecutionException if we can't write the file due to an {@link IOException}.
225     */
226    private void writeAllArtifactsInSha512PropertiesFile() throws MojoExecutionException {
227        File propertiesFile = new File(workingDirectory, "sha512.properties");
228        getLog().info("Writting " + propertiesFile);
229        try (FileOutputStream fileWriter = new FileOutputStream(propertiesFile)) {
230            artifactSha512s.store(fileWriter, "Release SHA-512s");
231        } catch (IOException e) {
232            throw new MojoExecutionException("Failure to write SHA-512'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 sha256  and sha512 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-256
274                    digest = artifactSha256s.getProperty(artifactKey.toString());
275                    getLog().info(artifact.getFile().getName() + " sha256: " + digest);
276                    try (PrintWriter printWriter = new PrintWriter(
277                            getSha256FilePath(workingDirectory, artifact.getFile()))) {
278                        printWriter.println(digest);
279                    }
280                    // SHA-512
281                    digest = artifactSha512s.getProperty(artifactKey.toString());
282                    getLog().info(artifact.getFile().getName() + " sha512: " + digest);
283                    try (PrintWriter printWriter = new PrintWriter(
284                            getSha512FilePath(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>sha256</code> signature file from a given file.
296     *
297     * @param directory is the {@link File} for the directory in which to make the <code>.sha256</code> file.
298     * @param file the {@link File} whose name we should use to create the <code>.sha256</code> file.
299     * @return a {@link String} that is the absolute path to the <code>.sha256</code> file.
300     */
301    private String getSha256FilePath(File directory, File file) {
302        StringBuffer buffer = new StringBuffer(directory.getAbsolutePath());
303        buffer.append("/");
304        buffer.append(file.getName());
305        buffer.append(".sha256");
306        return buffer.toString();
307    }
308
309    /**
310     * A helper method to create a file path for the <code>sha512</code> signature file from a given file.
311     *
312     * @param directory is the {@link File} for the directory in which to make the <code>.sha512</code> file.
313     * @param file the {@link File} whose name we should use to create the <code>.sha512</code> file.
314     * @return a {@link String} that is the absolute path to the <code>.sha512</code> file.
315     */
316    private String getSha512FilePath(File directory, File file) {
317        StringBuffer buffer = new StringBuffer(directory.getAbsolutePath());
318        buffer.append("/");
319        buffer.append(file.getName());
320        buffer.append(".sha512");
321        return buffer.toString();
322    }
323
324    /**
325     * Generates the unique artifact key for storage in our sha256 map and sha512 map. For example,
326     * commons-test-1.4-src.tar.gz should have it's name as the key.
327     *
328     * @param artifact the {@link Artifact} that we wish to generate a key for.
329     * @return the generated key
330     */
331    private String getArtifactKey(Artifact artifact) {
332        StringBuffer artifactKey = new StringBuffer();
333        artifactKey.append(artifact.getArtifactId()).append('-')
334                .append(artifact.getVersion()).append('-');
335        if (artifact.hasClassifier()) {
336            artifactKey.append(artifact.getClassifier()).append('-');
337        }
338        artifactKey.append(artifact.getType());
339        return artifactKey.toString();
340    }
341}