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>TOOD - 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      private final ArrayList<SourceInfo> srcFiles = new ArrayList<>();
55      private String destFileUrl;
56      private String destDirUrl;
57      private String srcDirUrl;
58      private boolean srcDirIsBase;
59      private boolean failonerror = true;
60      private String filesList;
61  
62      /**
63       * Sets the destination file.
64       *
65       * @param destFile The destination file name.
66       */
67      public void setDestFile(final String destFile) {
68          this.destFileUrl = destFile;
69      }
70  
71      /**
72       * Sets the destination directory.
73       *
74       * @param destDir The destination directory.
75       */
76      public void setDestDir(final String destDir) {
77          this.destDirUrl = destDir;
78      }
79  
80      /**
81       * Sets the source file.
82       *
83       * @param srcFile The source file name.
84       */
85      public void setSrc(final String srcFile) {
86          final SourceInfo src = new SourceInfo();
87          src.setFile(srcFile);
88          addConfiguredSrc(src);
89      }
90  
91      /**
92       * Sets the source directory.
93       *
94       * @param srcDir The source directory.
95       */
96      public void setSrcDir(final String srcDir) {
97          this.srcDirUrl = srcDir;
98      }
99  
100     /**
101      * Sets whether the source directory should be consider as the base directory.
102      *
103      * @param srcDirIsBase true if the source directory is the base directory.
104      */
105     public void setSrcDirIsBase(final boolean srcDirIsBase) {
106         this.srcDirIsBase = srcDirIsBase;
107     }
108 
109     /**
110      * Sets whether we should fail if there was an error or not.
111      *
112      * @param failonerror true if the operation should fail if there is an error.
113      */
114     public void setFailonerror(final boolean failonerror) {
115         this.failonerror = failonerror;
116     }
117 
118     /**
119      * Sets whether we should fail if there was an error or not.
120      *
121      * @return true if the operation should fail if there was an error.
122      */
123     public boolean isFailonerror() {
124         return failonerror;
125     }
126 
127     /**
128      * Sets the files to includes.
129      *
130      * @param filesList The list of files to include.
131      */
132     public void setIncludes(final String filesList) {
133         this.filesList = filesList;
134     }
135 
136     /**
137      * Adds a nested &lt;src&gt; element.
138      *
139      * @param srcInfo A nested source element.
140      * @throws BuildException if the SourceInfo doesn't reference a file.
141      */
142     public void addConfiguredSrc(final SourceInfo srcInfo) throws BuildException {
143         if (srcInfo.file == null) {
144             final String message = Messages.getString("vfs.tasks/sync.no-source-file.error");
145             throw new BuildException(message);
146         }
147         srcFiles.add(srcInfo);
148     }
149 
150     /**
151      * Executes this task.
152      *
153      * @throws BuildException if an error occurs.
154      */
155     @Override
156     public void execute() throws BuildException {
157         // Validate
158         if (destFileUrl == null && destDirUrl == null) {
159             final String message = Messages.getString("vfs.tasks/sync.no-destination.error");
160             logOrDie(message, Project.MSG_WARN);
161             return;
162         }
163 
164         if (destFileUrl != null && destDirUrl != null) {
165             final String message = Messages.getString("vfs.tasks/sync.too-many-destinations.error");
166             logOrDie(message, Project.MSG_WARN);
167             return;
168         }
169 
170         // Add the files of the includes attribute to the list
171         if (srcDirUrl != null && !srcDirUrl.equals(destDirUrl) && filesList != null && filesList.length() > 0) {
172             if (!srcDirUrl.endsWith("/")) {
173                 srcDirUrl += "/";
174             }
175             final StringTokenizer tok = new StringTokenizer(filesList, ", \t\n\r\f", false);
176             while (tok.hasMoreTokens()) {
177                 String nextFile = tok.nextToken();
178 
179                 // Basic compatibility with Ant fileset for directories
180                 if (nextFile.endsWith("/**")) {
181                     nextFile = nextFile.substring(0, nextFile.length() - 2);
182                 }
183 
184                 final SourceInfo src = new SourceInfo();
185                 src.setFile(srcDirUrl + nextFile);
186                 addConfiguredSrc(src);
187             }
188         }
189 
190         if (srcFiles.size() == 0) {
191             final String message = Messages.getString("vfs.tasks/sync.no-source-files.warn");
192             logOrDie(message, Project.MSG_WARN);
193             return;
194         }
195 
196         // Perform the sync
197         try {
198             if (destFileUrl != null) {
199                 handleSingleFile();
200             } else {
201                 handleFiles();
202             }
203         } catch (final BuildException e) {
204             throw e;
205         } catch (final Exception e) {
206             throw new BuildException(e.getMessage(), e);
207         }
208     }
209 
210     protected void logOrDie(final String message, final int level) {
211         if (!isFailonerror()) {
212             log(message, level);
213             return;
214         }
215         throw new BuildException(message);
216     }
217 
218     /**
219      * Copies the source files to the destination.
220      */
221     private void handleFiles() throws Exception {
222         // Locate the destination folder, and make sure it exists
223         final FileObject destFolder = resolveFile(destDirUrl);
224         destFolder.createFolder();
225 
226         // Locate the source files, and make sure they exist
227         FileName srcDirName = null;
228         if (srcDirUrl != null) {
229             srcDirName = resolveFile(srcDirUrl).getName();
230         }
231         final ArrayList<FileObject> srcs = new ArrayList<>();
232         for (int i = 0; i < srcFiles.size(); i++) {
233             // Locate the source file, and make sure it exists
234             final SourceInfo src = srcFiles.get(i);
235             final FileObject srcFile = resolveFile(src.file);
236             if (!srcFile.exists()) {
237                 final String message = Messages.getString("vfs.tasks/sync.src-file-no-exist.warn", srcFile);
238 
239                 logOrDie(message, Project.MSG_WARN);
240             } else {
241                 srcs.add(srcFile);
242             }
243         }
244 
245         // Scan the source files
246         final Set<FileObject> destFiles = new HashSet<>();
247         for (int i = 0; i < srcs.size(); i++) {
248             final FileObject rootFile = srcs.get(i);
249             final FileName rootName = rootFile.getName();
250 
251             if (rootFile.isFile()) {
252                 // Build the destination file name
253                 String relName = null;
254                 if (srcDirName == null || !srcDirIsBase) {
255                     relName = rootName.getBaseName();
256                 } else {
257                     relName = srcDirName.getRelativeName(rootName);
258                 }
259                 final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
260 
261                 // Do the copy
262                 handleFile(destFiles, rootFile, destFile);
263             } else {
264                 // Find matching files
265                 // If srcDirIsBase is true, select also the sub-directories
266                 final FileObject[] files = rootFile
267                         .findFiles(srcDirIsBase ? Selectors.SELECT_ALL : Selectors.SELECT_FILES);
268 
269                 for (final FileObject srcFile : files) {
270                     // Build the destination file name
271                     String relName = null;
272                     if (srcDirName == null || !srcDirIsBase) {
273                         relName = rootName.getRelativeName(srcFile.getName());
274                     } else {
275                         relName = srcDirName.getRelativeName(srcFile.getName());
276                     }
277 
278                     final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
279 
280                     // Do the copy
281                     handleFile(destFiles, srcFile, destFile);
282                 }
283             }
284         }
285 
286         // Scan the destination files for files with no source file
287         if (detectMissingSourceFiles()) {
288             final FileObject[] allDestFiles = destFolder.findFiles(Selectors.SELECT_FILES);
289             for (final FileObject destFile : allDestFiles) {
290                 if (!destFiles.contains(destFile)) {
291                     handleMissingSourceFile(destFile);
292                 }
293             }
294         }
295     }
296 
297     /**
298      * Handles a single file, checking for collisions where more than one source file maps to the same destination file.
299      */
300     private void handleFile(final Set<FileObject> destFiles, final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile)
301             throws Exception
302 
303     {
304         // Check for duplicate source files
305         if (destFiles.contains(destFile)) {
306             final String message = Messages.getString("vfs.tasks/sync.duplicate-source-files.warn", destFile);
307             logOrDie(message, Project.MSG_WARN);
308         } else {
309             destFiles.add(destFile);
310         }
311 
312         // Handle the file
313         handleFile(srcFile, destFile);
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 a single source file.
344      */
345     private void handleFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
346         if (!FileObjectUtils.exists(destFile)
347                 || srcFile.getContent().getLastModifiedTime() > destFile.getContent().getLastModifiedTime()) {
348             // Destination file is out-of-date
349             handleOutOfDateFile(srcFile, destFile);
350         } else {
351             // Destination file is up-to-date
352             handleUpToDateFile(srcFile, destFile);
353         }
354     }
355 
356     /**
357      * Handles an out-of-date file.
358      * <p>
359      * This is a file where the destination file either doesn't exist, or is older than the source file.
360      * </p>
361      * <p>
362      * This implementation does nothing.
363      * </p>
364      *
365      * @param srcFile The source file.
366      * @param destFile The destination file.
367      * @throws Exception Implementation can throw any Exception.
368      */
369     protected void handleOutOfDateFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
370         // noop
371     }
372 
373     /**
374      * Handles an up-to-date file.
375      * <p>
376      * This is where the destination file exists and is newer than the source file.
377      * </p>
378      * <p>
379      * This implementation does nothing.
380      * </p>
381      *
382      * @param srcFile The source file.
383      * @param destFile The destination file.
384      * @throws Exception Implementation can throw any Exception.
385      */
386     protected void handleUpToDateFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
387         // noop
388     }
389 
390     /**
391      * Handles a destination for which there is no corresponding source file.
392      * <p>
393      * This implementation does nothing.
394      * </p>
395      *
396      * @param destFile The existing destination file.
397      * @throws Exception Implementation can throw any Exception.
398      */
399     protected void handleMissingSourceFile(final FileObject destFile) throws Exception {
400         // noop
401     }
402 
403     /**
404      * Check if this task cares about destination files with a missing source file.
405      * <p>
406      * This implementation returns false.
407      * </p>
408      *
409      * @return True if missing file is detected.
410      */
411     protected boolean detectMissingSourceFiles() {
412         return false;
413     }
414 
415     /**
416      * Information about a source file.
417      */
418     public static class SourceInfo {
419         private String file;
420 
421         public void setFile(final String file) {
422             this.file = file;
423         }
424     }
425 
426 }