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.lang3.StringUtils;
020import org.apache.commons.release.plugin.SharedFunctions;
021import org.apache.maven.plugin.AbstractMojo;
022import org.apache.maven.plugin.MojoExecutionException;
023import org.apache.maven.plugin.MojoFailureException;
024import org.apache.maven.plugins.annotations.LifecyclePhase;
025import org.apache.maven.plugins.annotations.Mojo;
026import org.apache.maven.plugins.annotations.Parameter;
027import org.apache.maven.project.MavenProject;
028import org.apache.maven.scm.ScmException;
029import org.apache.maven.scm.ScmFileSet;
030import org.apache.maven.scm.command.add.AddScmResult;
031import org.apache.maven.scm.command.checkin.CheckInScmResult;
032import org.apache.maven.scm.manager.BasicScmManager;
033import org.apache.maven.scm.manager.ScmManager;
034import org.apache.maven.scm.provider.ScmProvider;
035import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
036import org.apache.maven.scm.provider.svn.svnexe.SvnExeScmProvider;
037import org.apache.maven.scm.repository.ScmRepository;
038
039import java.io.File;
040import java.io.IOException;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.List;
044
045/**
046 * This class checks out the dev distribution location, copies the distributions into that directory
047 * structure under the <code>target/commons-release-plugin/scm</code> directory. Then commits the
048 * distributions back up to SVN. Also, we include the built and zipped site as well as the RELEASE-NOTES.txt.
049 *
050 * @author chtompki
051 * @since 1.0
052 */
053@Mojo(name = "stage-distributions",
054        defaultPhase = LifecyclePhase.DEPLOY,
055        threadSafe = true,
056        aggregator = true)
057public class CommonsDistributionStagingMojo extends AbstractMojo {
058
059    /**
060     * The {@link MavenProject} object is essentially the context of the maven build at
061     * a given time.
062     */
063    @Parameter(defaultValue = "${project}", required = true)
064    private MavenProject project;
065
066    /**
067     * The {@link File} that contains a file to the root directory of the working project. Typically
068     * this directory is where the <code>pom.xml</code> resides.
069     */
070    @Parameter(defaultValue = "${basedir}")
071    private File basedir;
072
073    /**
074     * The main working directory for the plugin, namely <code>target/commons-release-plugin</code>, but
075     * that assumes that we're using the default maven <code>${project.build.directory}</code>.
076     */
077    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin", property = "commons.outputDirectory")
078    private File workingDirectory;
079
080    /**
081     * The location to which to checkout the dist subversion repository under our working directory, which
082     * was given above.
083     */
084    @Parameter(defaultValue = "${project.build.directory}/commons-release-plugin/scm",
085            property = "commons.distCheckoutDirectory")
086    private File distCheckoutDirectory;
087
088    /**
089     * The location of the RELEASE-NOTES.txt file such that multimodule builds can configure it.
090     */
091    @Parameter(defaultValue = "${basedir}/RELEASE-NOTES.txt", property = "commons.releaseNotesLocation")
092    private File releaseNotesFile;
093
094    /**
095     * A boolean that determines whether or not we actually commit the files up to the subversion repository.
096     * If this is set to <code>true</code>, we do all but make the commits. We do checkout the repository in question
097     * though.
098     */
099    @Parameter(property = "commons.release.dryRun", defaultValue = "false")
100    private Boolean dryRun;
101
102    /**
103     * The url of the subversion repository to which we wish the artifacts to be staged. Typicallly
104     * this would need to be of the form:
105     * <code>scm:svn:https://dist.apache.org/repos/dist/dev/commons/foo</code>. Note. that the prefix to the
106     * substring <code>https</code> is a requirement.
107     */
108    @Parameter(defaultValue = "", property = "commons.distSvnStagingUrl")
109    private String distSvnStagingUrl;
110
111    /**
112     * A parameter to generally avoid running unless it is specifically turned on by the consuming module.
113     */
114    @Parameter(defaultValue = "false", property = "commons.release.isDistModule")
115    private Boolean isDistModule;
116
117    /**
118     * The username for the distribution subversion repository. This is typically your apache id.
119     */
120    @Parameter(property = "user.name")
121    private String username;
122
123    /**
124     * The password associated with {@link CommonsDistributionStagingMojo#username}.
125     */
126    @Parameter(property = "user.password")
127    private String password;
128
129    @Override
130    public void execute() throws MojoExecutionException, MojoFailureException {
131        if (!isDistModule) {
132            getLog().info("This module is marked as a non distribution "
133                    + "or assembly module, and the plugin will not run.");
134            return;
135        }
136        if (StringUtils.isEmpty(distSvnStagingUrl)) {
137            getLog().warn("commons.distSvnStagingUrl is not set, the commons-release-plugin will not run.");
138            return;
139        }
140        if (!workingDirectory.exists()) {
141            getLog().info("Current project contains no distributions. Not executing.");
142            return;
143        }
144        getLog().info("Preparing to stage distributions");
145        try {
146            ScmManager scmManager = new BasicScmManager();
147            scmManager.setScmProvider("svn", new SvnExeScmProvider());
148            ScmRepository repository = scmManager.makeScmRepository(distSvnStagingUrl);
149            ScmProvider provider = scmManager.getProviderByRepository(repository);
150            SvnScmProviderRepository providerRepository = (SvnScmProviderRepository) repository.getProviderRepository();
151            providerRepository.setUser(username);
152            providerRepository.setPassword(password);
153            if (!distCheckoutDirectory.exists()) {
154                SharedFunctions.initDirectory(getLog(), distCheckoutDirectory);
155            }
156            ScmFileSet scmFileSet = new ScmFileSet(distCheckoutDirectory);
157            getLog().info("Checking out dist from: " + distSvnStagingUrl);
158            provider.checkOut(repository, scmFileSet);
159            File copiedReleaseNotes = copyReleaseNotesToWorkingDirectory();
160            List<File> filesToCommit = copyDistributionsIntoScmDirectoryStructure(copiedReleaseNotes);
161            if (!dryRun) {
162                ScmFileSet scmFileSetToCommit = new ScmFileSet(distCheckoutDirectory, filesToCommit);
163                AddScmResult addResult = provider.add(
164                        repository,
165                        scmFileSetToCommit,
166                        "Staging release: " + project.getArtifactId() + ", version: " + project.getVersion()
167                );
168                if (addResult.isSuccess()) {
169                    getLog().info("Staging release: " + project.getArtifactId() + ", version: " + project.getVersion());
170                    CheckInScmResult checkInResult = provider.checkIn(
171                            repository,
172                            scmFileSetToCommit,
173                            "Staging release: " + project.getArtifactId() + ", version: " + project.getVersion()
174                    );
175                    if (!checkInResult.isSuccess()) {
176                        getLog().error("Committing dist files failed: " + checkInResult.getCommandOutput());
177                        throw new MojoExecutionException(
178                                "Committing dist files failed: " + checkInResult.getCommandOutput()
179                        );
180                    }
181                } else {
182                    getLog().error("Adding dist files failed: " + addResult.getCommandOutput());
183                    throw new MojoExecutionException("Adding dist files failed: " + addResult.getCommandOutput());
184                }
185            } else {
186                getLog().info("Would have committed to: " + distSvnStagingUrl);
187                getLog().info("Staging release: " + project.getArtifactId() + ", version: " + project.getVersion());
188            }
189        } catch (ScmException e) {
190            getLog().error("Could not commit files to dist: " + distSvnStagingUrl, e);
191            throw new MojoExecutionException("Could not commit files to dist: " + distSvnStagingUrl, e);
192        }
193    }
194
195    /**
196     * A utility method that takes the <code>RELEASE-NOTES.txt</code> file from the base directory of the
197     * project and copies it into {@link CommonsDistributionStagingMojo#workingDirectory}.
198     *
199     * @return the RELEASE-NOTES.txt file that exists in the <code>target/commons-release-notes/scm</code>
200     *         directory for the purpose of adding it to the scm change set in the method
201     *         {@link CommonsDistributionStagingMojo#copyDistributionsIntoScmDirectoryStructure(File)}.
202     * @throws MojoExecutionException if an {@link IOException} occurrs as a wrapper so that maven
203     *                                can properly handle the exception.
204     */
205    private File copyReleaseNotesToWorkingDirectory() throws MojoExecutionException {
206        StringBuffer copiedReleaseNotesAbsolutePath;
207        getLog().info("Copying RELEASE-NOTES.txt to working directory.");
208        copiedReleaseNotesAbsolutePath = new StringBuffer(workingDirectory.getAbsolutePath());
209        copiedReleaseNotesAbsolutePath.append("/scm/");
210        copiedReleaseNotesAbsolutePath.append(releaseNotesFile.getName());
211        File copiedReleaseNotes = new File(copiedReleaseNotesAbsolutePath.toString());
212        SharedFunctions.copyFile(getLog(), releaseNotesFile, copiedReleaseNotes);
213        return copiedReleaseNotes;
214    }
215
216    /**
217     * Copies the list of files at the root of the {@link CommonsDistributionStagingMojo#workingDirectory} into
218     * the directory structure of the distribution staging repository. Specifically:
219     * <ul>
220     *     <li>root:</li>
221     *     <li><ul>
222     *         <li>site.zip</li>
223     *         <li>RELEASE-NOTES.txt</li>
224     *         <li>source:</li>
225     *         <li><ul>
226     *             <li>-src artifacts....</li>
227     *         </ul></li>
228     *         <li>binaries:</li>
229     *         <li><ul>
230     *             <li>-bin artifacts....</li>
231     *         </ul></li>
232     *     </ul></li>
233     * </ul>
234     *
235     * @param copiedReleaseNotes is the RELEASE-NOTES.txt file that exists in the
236     *                           <code>target/commons-release-plugin/scm</code> directory.
237     * @return a {@link List} of {@link File}'s in the directory for the purpose of adding them to the maven
238     *         {@link ScmFileSet}.
239     * @throws MojoExecutionException if an {@link IOException} occurrs so that Maven can handle it properly.
240     */
241    private List<File> copyDistributionsIntoScmDirectoryStructure(File copiedReleaseNotes)
242            throws MojoExecutionException {
243        List<File> workingDirectoryFiles = Arrays.asList(workingDirectory.listFiles());
244        String scmBinariesRoot = buildDistBinariesRoot();
245        String scmSourceRoot = buildDistSourceRoot();
246        SharedFunctions.initDirectory(getLog(), new File(scmBinariesRoot));
247        SharedFunctions.initDirectory(getLog(), new File(scmSourceRoot));
248        List<File> filesForMavenScmFileSet = new ArrayList<>();
249        File copy;
250        for (File file : workingDirectoryFiles) {
251            if (file.getName().contains("src")) {
252                copy = new File(scmSourceRoot + "/" + file.getName());
253                SharedFunctions.copyFile(getLog(), file, copy);
254                filesForMavenScmFileSet.add(copy);
255            } else if (file.getName().contains("bin")) {
256                copy = new File(scmBinariesRoot + "/" + file.getName());
257                SharedFunctions.copyFile(getLog(), file, copy);
258                filesForMavenScmFileSet.add(copy);
259            } else if (file.getName().contains("scm")) {
260                getLog().debug("Not copying scm directory over to the scm directory because it is the scm directory.");
261                //do nothing because we are copying into scm
262            } else {
263                copy = new File(distCheckoutDirectory.getAbsolutePath() + "/" + file.getName());
264                SharedFunctions.copyFile(getLog(), file, copy);
265                filesForMavenScmFileSet.add(copy);
266            }
267        }
268        filesForMavenScmFileSet.add(copiedReleaseNotes);
269        return filesForMavenScmFileSet;
270    }
271
272    /**
273     * Build the path for the distribution binaries directory.
274     *
275     * @return the local absolute path into the checkedout subversion repository that is where
276     *         the binaries distributions are to be copied.
277     */
278    private String buildDistBinariesRoot() {
279        StringBuffer buffer = new StringBuffer(distCheckoutDirectory.getAbsolutePath());
280        buffer.append("/binaries");
281        return buffer.toString();
282    }
283
284    /**
285     * Build the path for the distribution source directory.
286     *
287     * @return the local absolute path into the checkedout subversion repository that is where
288     *         the source distributions are to be copied.
289     */
290    private String buildDistSourceRoot() {
291        StringBuffer buffer = new StringBuffer(distCheckoutDirectory.getAbsolutePath());
292        buffer.append("/source");
293        return buffer.toString();
294    }
295
296    /**
297     * This method is the setter for the {@link CommonsDistributionStagingMojo#basedir} field, specifically
298     * for the usage in the unit tests.
299     *
300     * @param basedir is the {@link File} to be used as the project's root directory when this mojo
301     *                is invoked.
302     */
303    protected void setBasedir(File basedir) {
304        this.basedir = basedir;
305    }
306}