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 org.apache.commons.io.FileUtils;
020import org.apache.commons.lang3.StringUtils;
021import org.apache.commons.release.plugin.SharedFunctions;
022import org.apache.commons.release.plugin.velocity.HeaderHtmlVelocityDelegate;
023import org.apache.commons.release.plugin.velocity.ReadmeHtmlVelocityDelegate;
024import org.apache.maven.plugin.AbstractMojo;
025import org.apache.maven.plugin.MojoExecutionException;
026import org.apache.maven.plugin.MojoFailureException;
027import org.apache.maven.plugin.logging.Log;
028import org.apache.maven.plugins.annotations.LifecyclePhase;
029import org.apache.maven.plugins.annotations.Mojo;
030import org.apache.maven.plugins.annotations.Parameter;
031import org.apache.maven.project.MavenProject;
032import org.apache.maven.scm.ScmException;
033import org.apache.maven.scm.ScmFileSet;
034import org.apache.maven.scm.command.add.AddScmResult;
035import org.apache.maven.scm.command.checkin.CheckInScmResult;
036import org.apache.maven.scm.command.checkout.CheckOutScmResult;
037import org.apache.maven.scm.manager.BasicScmManager;
038import org.apache.maven.scm.manager.ScmManager;
039import org.apache.maven.scm.provider.ScmProvider;
040import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
041import org.apache.maven.scm.provider.svn.svnexe.SvnExeScmProvider;
042import org.apache.maven.scm.repository.ScmRepository;
043
044import java.io.File;
045import java.io.FileOutputStream;
046import java.io.IOException;
047import java.io.OutputStreamWriter;
048import java.io.Writer;
049import java.util.ArrayList;
050import java.util.Arrays;
051import java.util.List;
052
053/**
054 * This class checks out the dev distribution location, copies the distributions into that directory
055 * structure under the <code>target/commons-release-plugin/scm</code> directory. Then commits the
056 * distributions back up to SVN. Also, we include the built and zipped site as well as the RELEASE-NOTES.txt.
057 *
058 * @author chtompki
059 * @since 1.0
060 */
061@Mojo(name = "stage-distributions",
062        defaultPhase = LifecyclePhase.DEPLOY,
063        threadSafe = true,
064        aggregator = true)
065public class CommonsDistributionStagingMojo extends AbstractMojo {
066
067    /** The name of file generated from the README.vm velocity template to be checked into the dist svn repo. */
068    private static final String README_FILE_NAME = "README.html";
069    /** The name of file generated from the HEADER.vm velocity template to be checked into the dist svn repo. */
070    private static final String HEADER_FILE_NAME = "HEADER.html";
071
072    /**
073     * The {@link MavenProject} object is essentially the context of the maven build at
074     * a given time.
075     */
076    @Parameter(defaultValue = "${project}", required = true)
077    private MavenProject project;
078
079    /**
080     * The {@link File} that contains a file to the root directory of the working project. Typically
081     * this directory is where the <code>pom.xml</code> resides.
082     */
083    @Parameter(defaultValue = "${basedir}")
084    private File baseDir;
085
086    /** The location to which the site gets built during running <code>mvn site</code>. */
087    @Parameter(defaultValue = "${project.build.directory}/site", property = "commons.siteOutputDirectory")
088    private File siteDirectory;
089
090    /**
091     * The main working directory for the plugin, namely <code>target/commons-release-plugin</code>, but
092     * that assumes that we're using the default maven <code>${project.build.directory}</code>.
093     */
094    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin", property = "commons.outputDirectory")
095    private File workingDirectory;
096
097    /**
098     * The location to which to checkout the dist subversion repository under our working directory, which
099     * was given above.
100     */
101    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin/scm",
102            property = "commons.distCheckoutDirectory")
103    private File distCheckoutDirectory;
104
105    /**
106     * The location of the RELEASE-NOTES.txt file such that multi-module builds can configure it.
107     */
108    @Parameter(defaultValue = "${basedir}/RELEASE-NOTES.txt", property = "commons.releaseNotesLocation")
109    private File releaseNotesFile;
110
111    /**
112     * A boolean that determines whether or not we actually commit the files up to the subversion repository.
113     * If this is set to <code>true</code>, we do all but make the commits. We do checkout the repository in question
114     * though.
115     */
116    @Parameter(property = "commons.release.dryRun", defaultValue = "false")
117    private Boolean dryRun;
118
119    /**
120     * The url of the subversion repository to which we wish the artifacts to be staged. Typically this would need to
121     * be of the form: <code>scm:svn:https://dist.apache.org/repos/dist/dev/commons/foo/version-RC#</code>. Note. that
122     * the prefix to the substring <code>https</code> is a requirement.
123     */
124    @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl")
125    private String distSvnStagingUrl;
126
127    /**
128     * A parameter to generally avoid running unless it is specifically turned on by the consuming module.
129     */
130    @Parameter(defaultValue = "false", property = "commons.release.isDistModule")
131    private Boolean isDistModule;
132
133    /**
134     * The release version of the artifact to be built.
135     */
136    @Parameter(property = "commons.release.version")
137    private String commonsReleaseVersion;
138
139    /**
140     * The RC version of the release. For example the first voted on candidate would be "RC1".
141     */
142    @Parameter(property = "commons.rc.version")
143    private String commonsRcVersion;
144
145    /**
146     * The username for the distribution subversion repository. This is typically your Apache id.
147     */
148    @Parameter(property = "user.name")
149    private String username;
150
151    /**
152     * The password associated with {@link CommonsDistributionStagingMojo#username}.
153     */
154    @Parameter(property = "user.password")
155    private String password;
156
157    /**
158     * A subdirectory of the dist directory into which we are going to stage the release candidate. We
159     * build this up in the {@link CommonsDistributionStagingMojo#execute()} method. And, for example,
160     * the directory should look like <code>https://https://dist.apache.org/repos/dist/dev/commons/text/1.4-RC1</code>.
161     */
162    private File distVersionRcVersionDirectory;
163
164    @Override
165    public void execute() throws MojoExecutionException, MojoFailureException {
166        if (!isDistModule) {
167            getLog().info("This module is marked as a non distribution "
168                    + "or assembly module, and the plugin will not run.");
169            return;
170        }
171        if (StringUtils.isEmpty(distSvnStagingUrl)) {
172            getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run.");
173            return;
174        }
175        if (!workingDirectory.exists()) {
176            getLog().info("Current project contains no distributions. Not executing.");
177            return;
178        }
179        getLog().info("Preparing to stage distributions");
180        try {
181            ScmManager scmManager = new BasicScmManager();
182            scmManager.setScmProvider("svn", new SvnExeScmProvider());
183            ScmRepository repository = scmManager.makeScmRepository(distSvnStagingUrl);
184            ScmProvider provider = scmManager.getProviderByRepository(repository);
185            SvnScmProviderRepository providerRepository = (SvnScmProviderRepository) repository.getProviderRepository();
186            providerRepository.setUser(username);
187            providerRepository.setPassword(password);
188            distVersionRcVersionDirectory =
189                    new File(distCheckoutDirectory, commonsReleaseVersion + "-" + commonsRcVersion);
190            if (!distCheckoutDirectory.exists()) {
191                SharedFunctions.initDirectory(getLog(), distCheckoutDirectory);
192            }
193            ScmFileSet scmFileSet = new ScmFileSet(distCheckoutDirectory);
194            getLog().info("Checking out dist from: " + distSvnStagingUrl);
195            final CheckOutScmResult checkOutResult = provider.checkOut(repository, scmFileSet);
196            if (!checkOutResult.isSuccess()) {
197                throw new MojoExecutionException("Failed to checkout files from SCM: "
198                        + checkOutResult.getProviderMessage() + " [" + checkOutResult.getCommandOutput() + "]");
199            }
200            File copiedReleaseNotes = copyReleaseNotesToWorkingDirectory();
201            copyDistributionsIntoScmDirectoryStructureAndAddToSvn(copiedReleaseNotes,
202                    provider, repository);
203            List<File> filesToAdd = new ArrayList<>();
204            listNotHiddenFilesAndDirectories(distCheckoutDirectory, filesToAdd);
205            if (!dryRun) {
206                ScmFileSet fileSet = new ScmFileSet(distCheckoutDirectory, filesToAdd);
207                AddScmResult addResult = provider.add(
208                        repository,
209                        fileSet
210                );
211                if (!addResult.isSuccess()) {
212                    throw new MojoExecutionException("Failed to add files to SCM: " + addResult.getProviderMessage()
213                            + " [" + addResult.getCommandOutput() + "]");
214                }
215                getLog().info("Staging release: " + project.getArtifactId() + ", version: " + project.getVersion());
216                CheckInScmResult checkInResult = provider.checkIn(
217                        repository,
218                        fileSet,
219                        "Staging release: " + project.getArtifactId() + ", version: " + project.getVersion()
220                );
221                if (!checkInResult.isSuccess()) {
222                    getLog().error("Committing dist files failed: " + checkInResult.getCommandOutput());
223                    throw new MojoExecutionException(
224                            "Committing dist files failed: " + checkInResult.getCommandOutput()
225                    );
226                }
227                getLog().info("Committed revision " + checkInResult.getScmRevision());
228            } else {
229                getLog().info("[Dry run] Would have committed to: " + distSvnStagingUrl);
230                getLog().info(
231                        "[Dry run] Staging release: " + project.getArtifactId() + ", version: " + project.getVersion());
232            }
233        } catch (ScmException e) {
234            getLog().error("Could not commit files to dist: " + distSvnStagingUrl, e);
235            throw new MojoExecutionException("Could not commit files to dist: " + distSvnStagingUrl, e);
236        }
237    }
238
239    /**
240     * Lists all directories and files to a flat list.
241     * @param directory {@link File} containing directory to list
242     * @param files a {@link List} of {@link File} to which to append the files.
243     */
244    private void listNotHiddenFilesAndDirectories(File directory, List<File> files) {
245        // Get all the files and directories from a directory.
246        File[] fList = directory.listFiles();
247        for (File file : fList) {
248            if (file.isFile() && !file.isHidden()) {
249                files.add(file);
250            } else if (file.isDirectory() && !file.isHidden()) {
251                files.add(file);
252                listNotHiddenFilesAndDirectories(file, files);
253            }
254        }
255    }
256
257    /**
258     * A utility method that takes the <code>RELEASE-NOTES.txt</code> file from the base directory of the
259     * project and copies it into {@link CommonsDistributionStagingMojo#workingDirectory}.
260     *
261     * @return the RELEASE-NOTES.txt file that exists in the <code>target/commons-release-notes/scm</code>
262     *         directory for the purpose of adding it to the scm change set in the method
263     *         {@link CommonsDistributionStagingMojo#copyDistributionsIntoScmDirectoryStructureAndAddToSvn(File,
264     *         ScmProvider, ScmRepository)}.
265     * @throws MojoExecutionException if an {@link IOException} occurs as a wrapper so that maven
266     *                                can properly handle the exception.
267     */
268    private File copyReleaseNotesToWorkingDirectory() throws MojoExecutionException {
269        SharedFunctions.initDirectory(getLog(), distVersionRcVersionDirectory);
270        getLog().info("Copying RELEASE-NOTES.txt to working directory.");
271        File copiedReleaseNotes = new File(distVersionRcVersionDirectory, releaseNotesFile.getName());
272        SharedFunctions.copyFile(getLog(), releaseNotesFile, copiedReleaseNotes);
273        return copiedReleaseNotes;
274    }
275
276    /**
277     * Copies the list of files at the root of the {@link CommonsDistributionStagingMojo#workingDirectory} into
278     * the directory structure of the distribution staging repository. Specifically:
279     * <ul>
280     *   <li>root:
281     *     <ul>
282     *         <li>site</li>
283     *         <li>site.zip</li>
284     *         <li>RELEASE-NOTES.txt</li>
285     *         <li>source:
286     *           <ul>
287     *             <li>-src artifacts....</li>
288     *           </ul>
289     *         </li>
290     *         <li>binaries:
291     *           <ul>
292     *             <li>-bin artifacts....</li>
293     *           </ul>
294     *         </li>
295     *     </ul>
296     *   </li>
297     * </ul>
298     *
299     * @param copiedReleaseNotes is the RELEASE-NOTES.txt file that exists in the
300     *                           <code>target/commons-release-plugin/scm</code> directory.
301     * @param provider is the {@link ScmProvider} that we will use for adding the files we wish to commit.
302     * @param repository is the {@link ScmRepository} that we will use for adding the files that we wish to commit.
303     * @return a {@link List} of {@link File}'s in the directory for the purpose of adding them to the maven
304     *         {@link ScmFileSet}.
305     * @throws MojoExecutionException if an {@link IOException} occurs so that Maven can handle it properly.
306     */
307    private List<File> copyDistributionsIntoScmDirectoryStructureAndAddToSvn(File copiedReleaseNotes,
308                                                                             ScmProvider provider,
309                                                                             ScmRepository repository)
310            throws MojoExecutionException {
311        List<File> workingDirectoryFiles = Arrays.asList(workingDirectory.listFiles());
312        List<File> filesForMavenScmFileSet = new ArrayList<>();
313        File scmBinariesRoot = new File(distVersionRcVersionDirectory, "binaries");
314        File scmSourceRoot = new File(distVersionRcVersionDirectory, "source");
315        SharedFunctions.initDirectory(getLog(), scmBinariesRoot);
316        SharedFunctions.initDirectory(getLog(), scmSourceRoot);
317        File copy;
318        for (File file : workingDirectoryFiles) {
319            if (file.getName().contains("src")) {
320                copy = new File(scmSourceRoot,  file.getName());
321                SharedFunctions.copyFile(getLog(), file, copy);
322                filesForMavenScmFileSet.add(file);
323            } else if (file.getName().contains("bin")) {
324                copy = new File(scmBinariesRoot,  file.getName());
325                SharedFunctions.copyFile(getLog(), file, copy);
326                filesForMavenScmFileSet.add(file);
327            } else if (StringUtils.containsAny(file.getName(), "scm", "sha256.properties", "sha512.properties")) {
328                getLog().debug("Not copying scm directory over to the scm directory because it is the scm directory.");
329                //do nothing because we are copying into scm
330            } else {
331                copy = new File(distCheckoutDirectory.getAbsolutePath(),  file.getName());
332                SharedFunctions.copyFile(getLog(), file, copy);
333                filesForMavenScmFileSet.add(file);
334            }
335        }
336        filesForMavenScmFileSet.addAll(buildReadmeAndHeaderHtmlFiles());
337        filesForMavenScmFileSet.addAll(copySiteToScmDirectory());
338        return filesForMavenScmFileSet;
339    }
340
341    /**
342     * Copies <code>${basedir}/target/site</code> to <code>${basedir}/target/commons-release-plugin/scm/site</code>.
343     *
344     * @return the {@link List} of {@link File}'s contained in
345     *         <code>${basedir}/target/commons-release-plugin/scm/site</code>, after the copy is complete.
346     * @throws MojoExecutionException if the site copying fails for some reason.
347     */
348    private List<File> copySiteToScmDirectory() throws MojoExecutionException {
349        if (!siteDirectory.exists()) {
350            getLog().error("\"mvn site\" was not run before this goal, or a siteDirectory did not exist.");
351            throw new MojoExecutionException(
352                    "\"mvn site\" was not run before this goal, or a siteDirectory did not exist."
353            );
354        }
355        try {
356            FileUtils.copyDirectoryToDirectory(siteDirectory, distVersionRcVersionDirectory);
357        } catch (IOException e) {
358            throw new MojoExecutionException("Site copying failed", e);
359        }
360        File siteInScm = new File(distVersionRcVersionDirectory, "site");
361        return new ArrayList<>(FileUtils.listFiles(siteInScm, null, true));
362    }
363
364    /**
365     * Builds up <code>README.html</code> and <code>HEADER.html</code> that reside in following.
366     * <ul>
367     *     <li>distRoot
368     *     <ul>
369     *         <li>binaries/HEADER.html (symlink)</li>
370     *         <li>binaries/README.html (symlink)</li>
371     *         <li>source/HEADER.html (symlink)</li>
372     *         <li>source/README.html (symlink)</li>
373     *         <li>HEADER.html</li>
374     *         <li>README.html</li>
375     *     </ul>
376     *     </li>
377     * </ul>
378     * @return the {@link List} of created files above
379     * @throws MojoExecutionException if an {@link IOException} occurs in the creation of these
380     *                                files fails.
381     */
382    private List<File> buildReadmeAndHeaderHtmlFiles() throws MojoExecutionException {
383        List<File> headerAndReadmeFiles = new ArrayList<>();
384        File headerFile = new File(distVersionRcVersionDirectory, HEADER_FILE_NAME);
385        //
386        // HEADER file
387        //
388        try (Writer headerWriter = new OutputStreamWriter(new FileOutputStream(headerFile), "UTF-8")) {
389            HeaderHtmlVelocityDelegate.builder().build().render(headerWriter);
390        } catch (IOException e) {
391            final String message = "Could not build HEADER html file " + headerFile;
392            getLog().error(message, e);
393            throw new MojoExecutionException(message, e);
394        }
395        headerAndReadmeFiles.add(headerFile);
396        //
397        // README file
398        //
399        File readmeFile = new File(distVersionRcVersionDirectory, README_FILE_NAME);
400        try (Writer readmeWriter = new OutputStreamWriter(new FileOutputStream(readmeFile), "UTF-8")) {
401            // @formatter:off
402            ReadmeHtmlVelocityDelegate readmeHtmlVelocityDelegate = ReadmeHtmlVelocityDelegate.builder()
403                    .withArtifactId(project.getArtifactId())
404                    .withVersion(project.getVersion())
405                    .withSiteUrl(project.getUrl())
406                    .build();
407            // @formatter:on
408            readmeHtmlVelocityDelegate.render(readmeWriter);
409        } catch (IOException e) {
410            final String message = "Could not build README html file " + readmeFile;
411            getLog().error(message, e);
412            throw new MojoExecutionException(message, e);
413        }
414        headerAndReadmeFiles.add(readmeFile);
415        headerAndReadmeFiles.addAll(copyHeaderAndReadmeToSubdirectories(headerFile, readmeFile));
416        return headerAndReadmeFiles;
417    }
418
419    /**
420     * Copies <code>README.html</code> and <code>HEADER.html</code> to the source and binaries
421     * directories.
422     *
423     * @param headerFile The originally created <code>HEADER.html</code> file.
424     * @param readmeFile The originally created <code>README.html</code> file.
425     * @return a {@link List} of created files.
426     * @throws MojoExecutionException if the {@link SharedFunctions#copyFile(Log, File, File)}
427     *                                fails.
428     */
429    private List<File> copyHeaderAndReadmeToSubdirectories(File headerFile, File readmeFile)
430            throws MojoExecutionException {
431        List<File> symbolicLinkFiles = new ArrayList<>();
432        File sourceRoot = new File(distVersionRcVersionDirectory, "source");
433        File binariesRoot = new File(distVersionRcVersionDirectory, "binaries");
434        File sourceHeaderFile = new File(sourceRoot, HEADER_FILE_NAME);
435        File sourceReadmeFile = new File(sourceRoot, README_FILE_NAME);
436        File binariesHeaderFile = new File(binariesRoot, HEADER_FILE_NAME);
437        File binariesReadmeFile = new File(binariesRoot, README_FILE_NAME);
438        SharedFunctions.copyFile(getLog(), headerFile, sourceHeaderFile);
439        symbolicLinkFiles.add(sourceHeaderFile);
440        SharedFunctions.copyFile(getLog(), readmeFile, sourceReadmeFile);
441        symbolicLinkFiles.add(sourceReadmeFile);
442        SharedFunctions.copyFile(getLog(), headerFile, binariesHeaderFile);
443        symbolicLinkFiles.add(binariesHeaderFile);
444        SharedFunctions.copyFile(getLog(), readmeFile, binariesReadmeFile);
445        symbolicLinkFiles.add(binariesReadmeFile);
446        return symbolicLinkFiles;
447    }
448
449    /**
450     * This method is the setter for the {@link CommonsDistributionStagingMojo#baseDir} field, specifically
451     * for the usage in the unit tests.
452     *
453     * @param baseDir is the {@link File} to be used as the project's root directory when this mojo
454     *                is invoked.
455     */
456    protected void setBaseDir(File baseDir) {
457        this.baseDir = baseDir;
458    }
459}