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            copyReleaseNotesToWorkingDirectory();
160            List<File> filesToCommit = copyDistributionsIntoScmDirectoryStructure();
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     * @throws MojoExecutionException if an {@link IOException} occurrs as a wrapper so that maven
200     *                                can properly handle the exception.
201     */
202    private void copyReleaseNotesToWorkingDirectory() throws MojoExecutionException {
203        StringBuffer copiedReleaseNotesAbsolutePath;
204        getLog().info("Copying RELEASE-NOTES.txt to working directory.");
205        copiedReleaseNotesAbsolutePath = new StringBuffer(workingDirectory.getAbsolutePath());
206        copiedReleaseNotesAbsolutePath.append("/scm/");
207        copiedReleaseNotesAbsolutePath.append(releaseNotesFile.getName());
208        File copiedReleaseNotes = new File(copiedReleaseNotesAbsolutePath.toString());
209        SharedFunctions.copyFile(getLog(), releaseNotesFile, copiedReleaseNotes);
210    }
211
212    /**
213     * Copies the list of files at the root of the {@link CommonsDistributionStagingMojo#workingDirectory} into
214     * the directory structure of the distribution staging repository. Specifically:
215     * <ul>
216     *     <li>root:</li>
217     *     <li><ul>
218     *         <li>site.zip</li>
219     *         <li>RELEASE-NOTES.txt</li>
220     *         <li>source:</li>
221     *         <li><ul>
222     *             <li>-src artifacts....</li>
223     *         </ul></li>
224     *         <li>binaries:</li>
225     *         <li><ul>
226     *             <li>-bin artifacts....</li>
227     *         </ul></li>
228     *     </ul></li>
229     * </ul>
230     *
231     * @return a {@link List} of {@link File}'s in the directory for the purpose of adding them to the maven
232     *         {@link ScmFileSet}.
233     * @throws MojoExecutionException if an {@link IOException} occurrs so that Maven can handle it properly.
234     */
235    private List<File> copyDistributionsIntoScmDirectoryStructure() throws MojoExecutionException {
236        List<File> workingDirectoryFiles = Arrays.asList(workingDirectory.listFiles());
237        String scmBinariesRoot = buildDistBinariesRoot();
238        String scmSourceRoot = buildDistSourceRoot();
239        SharedFunctions.initDirectory(getLog(), new File(scmBinariesRoot));
240        SharedFunctions.initDirectory(getLog(), new File(scmSourceRoot));
241        List<File> filesForMavenScmFileSet = new ArrayList<>();
242        File copy;
243        for (File file : workingDirectoryFiles) {
244            if (file.getName().contains("src")) {
245                copy = new File(scmSourceRoot + "/" + file.getName());
246                SharedFunctions.copyFile(getLog(), file, copy);
247                filesForMavenScmFileSet.add(copy);
248            } else if (file.getName().contains("bin")) {
249                copy = new File(scmBinariesRoot + "/" + file.getName());
250                SharedFunctions.copyFile(getLog(), file, copy);
251                filesForMavenScmFileSet.add(copy);
252            } else if (file.getName().contains("scm")) {
253                getLog().debug("Not copying scm directory over to the scm directory because it is the scm directory.");
254                //do nothing because we are copying into scm
255            } else {
256                copy = new File(distCheckoutDirectory.getAbsolutePath() + "/" + file.getName());
257                SharedFunctions.copyFile(getLog(), file, copy);
258                filesForMavenScmFileSet.add(copy);
259            }
260        }
261        filesForMavenScmFileSet.add(releaseNotesFile);
262        return filesForMavenScmFileSet;
263    }
264
265    /**
266     * Build the path for the distribution binaries directory.
267     *
268     * @return the local absolute path into the checkedout subversion repository that is where
269     *         the binaries distributions are to be copied.
270     */
271    private String buildDistBinariesRoot() {
272        StringBuffer buffer = new StringBuffer(distCheckoutDirectory.getAbsolutePath());
273        buffer.append("/binaries");
274        return buffer.toString();
275    }
276
277    /**
278     * Build the path for the distribution source directory.
279     *
280     * @return the local absolute path into the checkedout subversion repository that is where
281     *         the source distributions are to be copied.
282     */
283    private String buildDistSourceRoot() {
284        StringBuffer buffer = new StringBuffer(distCheckoutDirectory.getAbsolutePath());
285        buffer.append("/source");
286        return buffer.toString();
287    }
288
289    /**
290     * This method is the setter for the {@link CommonsDistributionStagingMojo#basedir} field, specifically
291     * for the usage in the unit tests.
292     *
293     * @param basedir is the {@link File} to be used as the project's root directory when this mojo
294     *                is invoked.
295     */
296    protected void setBasedir(File basedir) {
297        this.basedir = basedir;
298    }
299}