View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.scmpublish;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.OutputStreamWriter;
26  import java.io.PrintWriter;
27  import java.nio.file.Files;
28  import java.nio.file.LinkOption;
29  import java.nio.file.StandardCopyOption;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Set;
37  
38  import org.apache.commons.io.FileUtils;
39  import org.apache.commons.io.IOUtils;
40  import org.apache.commons.io.filefilter.NameFileFilter;
41  import org.apache.commons.io.filefilter.NotFileFilter;
42  import org.apache.maven.plugin.MojoExecutionException;
43  import org.apache.maven.plugin.MojoFailureException;
44  import org.apache.maven.plugins.annotations.Mojo;
45  import org.apache.maven.plugins.annotations.Parameter;
46  import org.apache.maven.project.MavenProject;
47  import org.apache.maven.shared.utils.logging.MessageUtils;
48  import org.codehaus.plexus.util.MatchPatterns;
49  
50  /**
51   * Publish a content to scm. By default, content is taken from default site staging directory
52   * <code>${project.build.directory}/staging</code>.
53   * Can be used without project, so usable to update any SCM with any content.
54   */
55  @Mojo(name = "publish-scm", aggregator = true, requiresProject = false)
56  public class ScmPublishPublishScmMojo extends AbstractScmPublishMojo {
57      /**
58       * The content to be published.
59       */
60      @Parameter(property = "scmpublish.content", defaultValue = "${project.build.directory}/staging")
61      private File content;
62  
63      /**
64       */
65      @Parameter(defaultValue = "${project}", readonly = true, required = true)
66      protected MavenProject project;
67  
68      private List<File> deleted = new ArrayList<>();
69  
70      private List<File> added = new ArrayList<>();
71  
72      private List<File> updated = new ArrayList<>();
73  
74      private int directories = 0;
75      private int files = 0;
76      private long size = 0;
77  
78      /**
79       * Update scm checkout directory with content.
80       *
81       * @param checkout        the scm checkout directory
82       * @param dir             the content to put in scm (can be <code>null</code>)
83       * @param doNotDeleteDirs directory names that should not be deleted from scm even if not in new content:
84       *                        used for modules, which content is available only when staging
85       * @throws IOException
86       */
87      private void update(File checkout, File dir, List<String> doNotDeleteDirs) throws IOException {
88          String scmSpecificFilename = scmProvider.getScmSpecificFilename();
89          String[] files = scmSpecificFilename != null
90                  ? checkout.list(new NotFileFilter(new NameFileFilter(scmSpecificFilename)))
91                  : checkout.list();
92  
93          Set<String> checkoutContent = new HashSet<>(Arrays.asList(files));
94          List<String> dirContent = (dir != null) ? Arrays.asList(dir.list()) : Collections.emptyList();
95  
96          Set<String> deleted = new HashSet<>(checkoutContent);
97          deleted.removeAll(dirContent);
98  
99          MatchPatterns ignoreDeleteMatchPatterns = null;
100         List<String> pathsAsList = new ArrayList<>(0);
101         if (ignorePathsToDelete != null && ignorePathsToDelete.length > 0) {
102             ignoreDeleteMatchPatterns = MatchPatterns.from(ignorePathsToDelete);
103             pathsAsList = Arrays.asList(ignorePathsToDelete);
104         }
105 
106         for (String name : deleted) {
107             if (ignoreDeleteMatchPatterns != null && ignoreDeleteMatchPatterns.matches(name, true)) {
108                 getLog().debug(name + " match one of the patterns '" + pathsAsList + "': do not add to deleted files");
109                 continue;
110             }
111             getLog().debug("file marked for deletion: " + name);
112             File file = new File(checkout, name);
113 
114             if ((doNotDeleteDirs != null) && file.isDirectory() && (doNotDeleteDirs.contains(name))) {
115                 // ignore directory not available
116                 continue;
117             }
118 
119             if (file.isDirectory()) {
120                 update(file, null, null);
121             }
122             this.deleted.add(file);
123         }
124 
125         for (String name : dirContent) {
126             File file = new File(checkout, name);
127             File source = new File(dir, name);
128 
129             if (Files.isSymbolicLink(source.toPath())) {
130                 if (!checkoutContent.contains(name)) {
131                     this.added.add(file);
132                 }
133 
134                 // copy symbolic link (Java 7 only)
135                 copySymLink(source, file);
136             } else if (source.isDirectory()) {
137                 directories++;
138                 if (!checkoutContent.contains(name)) {
139                     this.added.add(file);
140                     file.mkdir();
141                 }
142 
143                 update(file, source, null);
144             } else {
145                 if (checkoutContent.contains(name)) {
146                     this.updated.add(file);
147                 } else {
148                     this.added.add(file);
149                 }
150 
151                 copyFile(source, file);
152             }
153         }
154     }
155 
156     /**
157      * Copy a symbolic link.
158      *
159      * @param srcFile the source file (expected to be a symbolic link)
160      * @param destFile the destination file (which will be a symbolic link)
161      * @throws IOException
162      */
163     private void copySymLink(File srcFile, File destFile) throws IOException {
164         Files.copy(
165                 srcFile.toPath(),
166                 destFile.toPath(),
167                 StandardCopyOption.REPLACE_EXISTING,
168                 StandardCopyOption.COPY_ATTRIBUTES,
169                 LinkOption.NOFOLLOW_LINKS);
170     }
171 
172     /**
173      * Copy a file content, normalizing newlines when necessary.
174      *
175      * @param srcFile  the source file
176      * @param destFile the destination file
177      * @throws IOException
178      * @see #requireNormalizeNewlines(File)
179      */
180     private void copyFile(File srcFile, File destFile) throws IOException {
181         if (requireNormalizeNewlines(srcFile)) {
182             copyAndNormalizeNewlines(srcFile, destFile);
183         } else {
184             FileUtils.copyFile(srcFile, destFile);
185         }
186         files++;
187         size += destFile.length();
188     }
189 
190     /**
191      * Copy and normalize newlines.
192      *
193      * @param srcFile  the source file
194      * @param destFile the destination file
195      * @throws IOException
196      */
197     private void copyAndNormalizeNewlines(File srcFile, File destFile) throws IOException {
198         BufferedReader in = null;
199         PrintWriter out = null;
200         try {
201             in = new BufferedReader(new InputStreamReader(Files.newInputStream(srcFile.toPath()), siteOutputEncoding));
202             out = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(destFile.toPath()), siteOutputEncoding));
203 
204             for (String line = in.readLine(); line != null; line = in.readLine()) {
205                 if (in.ready()) {
206                     out.println(line);
207                 } else {
208                     out.print(line);
209                 }
210             }
211 
212             out.close();
213             out = null;
214             in.close();
215             in = null;
216         } finally {
217             IOUtils.closeQuietly(out);
218             IOUtils.closeQuietly(in);
219         }
220     }
221 
222     public void scmPublishExecute() throws MojoExecutionException, MojoFailureException {
223         if (siteOutputEncoding == null) {
224             getLog().warn("No output encoding, defaulting to UTF-8.");
225             siteOutputEncoding = "utf-8";
226         }
227 
228         if (!content.exists()) {
229             throw new MojoExecutionException("Configured content directory does not exist: " + content);
230         }
231 
232         if (!content.canRead()) {
233             throw new MojoExecutionException("Can't read content directory: " + content);
234         }
235 
236         checkoutExisting();
237 
238         final File updateDirectory;
239         if (subDirectory == null) {
240             updateDirectory = checkoutDirectory;
241         } else {
242             updateDirectory = new File(checkoutDirectory, subDirectory);
243 
244             // Security check for subDirectory with .. inside
245             if (!updateDirectory
246                     .toPath()
247                     .normalize()
248                     .startsWith(checkoutDirectory.toPath().normalize())) {
249                 logError("Try to acces outside of the checkout directory with sub-directory: %s", subDirectory);
250                 return;
251             }
252 
253             if (!updateDirectory.exists()) {
254                 updateDirectory.mkdirs();
255             }
256 
257             logInfo("Will copy content in sub-directory: %s", subDirectory);
258         }
259 
260         try {
261             logInfo("Updating checkout directory with actual content in %s", content);
262             update(
263                     updateDirectory,
264                     content,
265                     (project == null) ? null : project.getModel().getModules());
266             String displaySize = org.apache.commons.io.FileUtils.byteCountToDisplaySize(size);
267             logInfo(
268                     "Content consists of " + MessageUtils.buffer().strong("%d directories and %d files = %s"),
269                     directories,
270                     files,
271                     displaySize);
272         } catch (IOException ioe) {
273             throw new MojoExecutionException("Could not copy content to SCM checkout", ioe);
274         }
275 
276         logInfo(
277                 "Publishing content to SCM will result in "
278                         + MessageUtils.buffer().strong("%d addition(s), %d update(s), %d delete(s)"),
279                 added.size(),
280                 updated.size(),
281                 deleted.size());
282 
283         if (isDryRun()) {
284             int pos = checkoutDirectory.getAbsolutePath().length() + 1;
285             for (File addedFile : added) {
286                 logInfo("- addition %s", addedFile.getAbsolutePath().substring(pos));
287             }
288             for (File updatedFile : updated) {
289                 logInfo("- update   %s", updatedFile.getAbsolutePath().substring(pos));
290             }
291             for (File deletedFile : deleted) {
292                 logInfo("- delete   %s", deletedFile.getAbsolutePath().substring(pos));
293             }
294             return;
295         }
296 
297         if (!added.isEmpty()) {
298             addFiles(added);
299         }
300 
301         if (!deleted.isEmpty()) {
302             deleteFiles(deleted);
303         }
304 
305         logInfo("Checking in SCM, starting at " + new Date() + "...");
306         checkinFiles();
307     }
308 }