View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.tasks;
18  
19  import java.util.ArrayList;
20  import java.util.HashSet;
21  import java.util.Set;
22  import java.util.StringTokenizer;
23  
24  import org.apache.commons.vfs2.FileName;
25  import org.apache.commons.vfs2.FileObject;
26  import org.apache.commons.vfs2.NameScope;
27  import org.apache.commons.vfs2.Selectors;
28  import org.apache.commons.vfs2.util.FileObjectUtils;
29  import org.apache.commons.vfs2.util.Messages;
30  import org.apache.tools.ant.BuildException;
31  import org.apache.tools.ant.Project;
32  
33  /**
34   * An abstract file synchronization task. Scans a set of source files and folders, and a destination folder, and
35   * performs actions on missing and out-of-date files. Specifically, performs actions on the following:
36   * <ul>
37   * <li>Missing destination file.
38   * <li>Missing source file.
39   * <li>Out-of-date destination file.
40   * <li>Up-to-date destination file.
41   * </ul>
42   *
43   * <ul>
44   * <li>TODO - Deal with case where dest file maps to a child of one of the source files.</li>
45   * <li>TODO - Deal with case where dest file already exists and is incorrect type (not file, not a folder).</li>
46   * <li>TODO - Use visitors.</li>
47   * <li>TODO - Add default excludes.</li>
48   * <li>TODO - Allow selector, mapper, filters, etc to be specified.</li>
49   * <li>TODO - Handle source/dest directories as well.</li>
50   * <li>TODO - Allow selector to be specified for choosing which dest files to sync.</li>
51   * </ul>
52   */
53  public abstract class AbstractSyncTask extends VfsTask {
54  
55      /**
56       * Information about a source file.
57       */
58      public static class SourceInfo {
59  
60          private String file;
61  
62          /**
63           * Constructs a new instance.
64           */
65          public SourceInfo() {
66              // empty
67          }
68  
69          /**
70           * Sets the file.
71           *
72           * @param file the file.
73           */
74          public void setFile(final String file) {
75              this.file = file;
76          }
77      }
78      private final ArrayList<SourceInfo> srcFiles = new ArrayList<>();
79      private String destFileUrl;
80      private String destDirUrl;
81      private String srcDirUrl;
82      private boolean srcDirIsBase;
83      private boolean failOnError = true;
84  
85      private String filesList;
86  
87      /**
88       * Constructs a new instance.
89       */
90      public AbstractSyncTask() {
91          // empty
92      }
93  
94      /**
95       * Adds a nested &lt;src&gt; element.
96       *
97       * @param srcInfo A nested source element.
98       * @throws BuildException if the SourceInfo doesn't reference a file.
99       */
100     public void addConfiguredSrc(final SourceInfo srcInfo) throws BuildException {
101         if (srcInfo.file == null) {
102             final String message = Messages.getString("vfs.tasks/sync.no-source-file.error");
103             throw new BuildException(message);
104         }
105         srcFiles.add(srcInfo);
106     }
107 
108     /**
109      * Check if this task cares about destination files with a missing source file.
110      * <p>
111      * This implementation returns false.
112      * </p>
113      *
114      * @return True if missing file is detected.
115      */
116     protected boolean detectMissingSourceFiles() {
117         return false;
118     }
119 
120     /**
121      * Executes this task.
122      *
123      * @throws BuildException if an error occurs.
124      */
125     @Override
126     public void execute() throws BuildException {
127         // Validate
128         if (destFileUrl == null && destDirUrl == null) {
129             final String message = Messages.getString("vfs.tasks/sync.no-destination.error");
130             logOrDie(message, Project.MSG_WARN);
131             return;
132         }
133 
134         if (destFileUrl != null && destDirUrl != null) {
135             final String message = Messages.getString("vfs.tasks/sync.too-many-destinations.error");
136             logOrDie(message, Project.MSG_WARN);
137             return;
138         }
139 
140         // Add the files of the includes attribute to the list
141         if (srcDirUrl != null && !srcDirUrl.equals(destDirUrl) && filesList != null && filesList.length() > 0) {
142             if (!srcDirUrl.endsWith("/")) {
143                 srcDirUrl += "/";
144             }
145             final StringTokenizer tok = new StringTokenizer(filesList, ", \t\n\r\f", false);
146             while (tok.hasMoreTokens()) {
147                 String nextFile = tok.nextToken();
148 
149                 // Basic compatibility with Ant fileset for directories
150                 if (nextFile.endsWith("/**")) {
151                     nextFile = nextFile.substring(0, nextFile.length() - 2);
152                 }
153 
154                 final SourceInfo src = new SourceInfo();
155                 src.setFile(srcDirUrl + nextFile);
156                 addConfiguredSrc(src);
157             }
158         }
159 
160         if (srcFiles.isEmpty()) {
161             final String message = Messages.getString("vfs.tasks/sync.no-source-files.warn");
162             logOrDie(message, Project.MSG_WARN);
163             return;
164         }
165 
166         // Perform the sync
167         try {
168             if (destFileUrl != null) {
169                 handleSingleFile();
170             } else {
171                 handleFiles();
172             }
173         } catch (final BuildException e) {
174             throw e;
175         } catch (final Exception e) {
176             throw new BuildException(e.getMessage(), e);
177         }
178     }
179 
180     /**
181      * Handles a single source file.
182      */
183     private void handleFile(final FileObject srcFile, final FileObject destFile) throws Exception {
184         if (!FileObjectUtils.exists(destFile)
185                 || srcFile.getContent().getLastModifiedTime() > destFile.getContent().getLastModifiedTime()) {
186             // Destination file is out-of-date
187             handleOutOfDateFile(srcFile, destFile);
188         } else {
189             // Destination file is up-to-date
190             handleUpToDateFile(srcFile, destFile);
191         }
192     }
193 
194     /**
195      * Handles a single file, checking for collisions where more than one source file maps to the same destination file.
196      */
197     private void handleFile(final Set<FileObject> destFiles, final FileObject srcFile, final FileObject destFile) throws Exception {
198         // Check for duplicate source files
199         if (destFiles.contains(destFile)) {
200             final String message = Messages.getString("vfs.tasks/sync.duplicate-source-files.warn", destFile);
201             logOrDie(message, Project.MSG_WARN);
202         } else {
203             destFiles.add(destFile);
204         }
205 
206         // Handle the file
207         handleFile(srcFile, destFile);
208     }
209 
210     /**
211      * Copies the source files to the destination.
212      */
213     private void handleFiles() throws Exception {
214         // Locate the destination folder, and make sure it exists
215         final FileObject destFolder = resolveFile(destDirUrl);
216         destFolder.createFolder();
217 
218         // Locate the source files, and make sure they exist
219         FileName srcDirName = null;
220         if (srcDirUrl != null) {
221             srcDirName = resolveFile(srcDirUrl).getName();
222         }
223         final ArrayList<FileObject> srcs = new ArrayList<>();
224         for (final SourceInfo src : srcFiles) {
225             final FileObject srcFile = resolveFile(src.file);
226             if (!srcFile.exists()) {
227                 final String message = Messages.getString("vfs.tasks/sync.src-file-no-exist.warn", srcFile);
228 
229                 logOrDie(message, Project.MSG_WARN);
230             } else {
231                 srcs.add(srcFile);
232             }
233         }
234 
235         // Scan the source files
236         final Set<FileObject> destFiles = new HashSet<>();
237         for (final FileObject rootFile : srcs) {
238             final FileName rootName = rootFile.getName();
239 
240             if (rootFile.isFile()) {
241                 // Build the destination file name
242                 final String relName;
243                 if (srcDirName == null || !srcDirIsBase) {
244                     relName = rootName.getBaseName();
245                 } else {
246                     relName = srcDirName.getRelativeName(rootName);
247                 }
248                 final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
249 
250                 // Do the copy
251                 handleFile(destFiles, rootFile, destFile);
252             } else {
253                 // Find matching files
254                 // If srcDirIsBase is true, select also the subdirectories
255                 final FileObject[] files = rootFile
256                         .findFiles(srcDirIsBase ? Selectors.SELECT_ALL : Selectors.SELECT_FILES);
257 
258                 for (final FileObject srcFile : files) {
259                     // Build the destination file name
260                     final String relName;
261                     if (srcDirName == null || !srcDirIsBase) {
262                         relName = rootName.getRelativeName(srcFile.getName());
263                     } else {
264                         relName = srcDirName.getRelativeName(srcFile.getName());
265                     }
266 
267                     final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
268 
269                     // Do the copy
270                     handleFile(destFiles, srcFile, destFile);
271                 }
272             }
273         }
274 
275         // Scan the destination files for files with no source file
276         if (detectMissingSourceFiles()) {
277             final FileObject[] allDestFiles = destFolder.findFiles(Selectors.SELECT_FILES);
278             for (final FileObject destFile : allDestFiles) {
279                 if (!destFiles.contains(destFile)) {
280                     handleMissingSourceFile(destFile);
281                 }
282             }
283         }
284     }
285 
286     /**
287      * Handles a destination for which there is no corresponding source file.
288      * <p>
289      * This implementation does nothing.
290      * </p>
291      *
292      * @param destFile The existing destination file.
293      * @throws Exception Implementation can throw any Exception.
294      */
295     protected void handleMissingSourceFile(final FileObject destFile) throws Exception {
296         // noop
297     }
298 
299     /**
300      * Handles an out-of-date file.
301      * <p>
302      * This is a file where the destination file either doesn't exist, or is older than the source file.
303      * </p>
304      * <p>
305      * This implementation does nothing.
306      * </p>
307      *
308      * @param srcFile The source file.
309      * @param destFile The destination file.
310      * @throws Exception Implementation can throw any Exception.
311      */
312     protected void handleOutOfDateFile(final FileObject srcFile, final FileObject destFile) throws Exception {
313         // noop
314     }
315 
316     /**
317      * Copies a single file.
318      */
319     private void handleSingleFile() throws Exception {
320         // Make sure there is exactly one source file, and that it exists
321         // and is a file.
322         if (srcFiles.size() > 1) {
323             final String message = Messages.getString("vfs.tasks/sync.too-many-source-files.error");
324             logOrDie(message, Project.MSG_WARN);
325             return;
326         }
327         final SourceInfo src = srcFiles.get(0);
328         final FileObject srcFile = resolveFile(src.file);
329         if (!srcFile.isFile()) {
330             final String message = Messages.getString("vfs.tasks/sync.source-not-file.error", srcFile);
331             logOrDie(message, Project.MSG_WARN);
332             return;
333         }
334 
335         // Locate the destination file
336         final FileObject destFile = resolveFile(destFileUrl);
337 
338         // Do the copy
339         handleFile(srcFile, destFile);
340     }
341 
342     /**
343      * Handles an up-to-date file.
344      * <p>
345      * This is where the destination file exists and is newer than the source file.
346      * </p>
347      * <p>
348      * This implementation does nothing.
349      * </p>
350      *
351      * @param srcFile The source file.
352      * @param destFile The destination file.
353      * @throws Exception Implementation can throw any Exception.
354      */
355     protected void handleUpToDateFile(final FileObject srcFile, final FileObject destFile) throws Exception {
356         // noop
357     }
358 
359     /**
360      * Sets whether we should fail if there was an error or not.
361      *
362      * @return true if the operation should fail if there was an error.
363      */
364     public boolean isFailonerror() {
365         return failOnError;
366     }
367 
368     /**
369      * Logs a message or throws a {@link BuildException} depending on {@link #isFailonerror()}.
370      *
371      * @param message The message to using in logging or BuildException.
372      * @param level The log level.
373      */
374     protected void logOrDie(final String message, final int level) {
375         if (!isFailonerror()) {
376             log(message, level);
377             return;
378         }
379         throw new BuildException(message);
380     }
381 
382     /**
383      * Sets the destination directory.
384      *
385      * @param destDirUrl The destination directory.
386      */
387     public void setDestDir(final String destDirUrl) {
388         this.destDirUrl = destDirUrl;
389     }
390 
391     /**
392      * Sets the destination file.
393      *
394      * @param destFileUrl The destination file name.
395      */
396     public void setDestFile(final String destFileUrl) {
397         this.destFileUrl = destFileUrl;
398     }
399 
400     /**
401      * Sets whether we should fail if there was an error or not.
402      *
403      * @param failOnError true if the operation should fail if there is an error.
404      */
405     public void setFailonerror(final boolean failOnError) {
406         this.failOnError = failOnError;
407     }
408 
409     /**
410      * Sets the files to includes.
411      *
412      * @param filesList The list of files to include.
413      */
414     public void setIncludes(final String filesList) {
415         this.filesList = filesList;
416     }
417 
418     /**
419      * Sets the source file.
420      *
421      * @param srcFile The source file name.
422      */
423     public void setSrc(final String srcFile) {
424         final SourceInfo src = new SourceInfo();
425         src.setFile(srcFile);
426         addConfiguredSrc(src);
427     }
428 
429     /**
430      * Sets the source directory.
431      *
432      * @param srcDirUrl The source directory.
433      */
434     public void setSrcDir(final String srcDirUrl) {
435         this.srcDirUrl = srcDirUrl;
436     }
437 
438     /**
439      * Sets whether the source directory should be considered as the base directory.
440      *
441      * @param srcDirIsBase true if the source directory is the base directory.
442      */
443     public void setSrcDirIsBase(final boolean srcDirIsBase) {
444         this.srcDirIsBase = srcDirIsBase;
445     }
446 
447 }