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.isEmpty()) {
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 (final SourceInfo src : srcFiles) {
233             final FileObject srcFile = resolveFile(src.file);
234             if (!srcFile.exists()) {
235                 final String message = Messages.getString("vfs.tasks/sync.src-file-no-exist.warn", srcFile);
236 
237                 logOrDie(message, Project.MSG_WARN);
238             } else {
239                 srcs.add(srcFile);
240             }
241         }
242 
243         // Scan the source files
244         final Set<FileObject> destFiles = new HashSet<>();
245         for (final FileObject rootFile : srcs) {
246             final FileName rootName = rootFile.getName();
247 
248             if (rootFile.isFile()) {
249                 // Build the destination file name
250                 final String relName;
251                 if (srcDirName == null || !srcDirIsBase) {
252                     relName = rootName.getBaseName();
253                 } else {
254                     relName = srcDirName.getRelativeName(rootName);
255                 }
256                 final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
257 
258                 // Do the copy
259                 handleFile(destFiles, rootFile, destFile);
260             } else {
261                 // Find matching files
262                 // If srcDirIsBase is true, select also the sub-directories
263                 final FileObject[] files = rootFile
264                         .findFiles(srcDirIsBase ? Selectors.SELECT_ALL : Selectors.SELECT_FILES);
265 
266                 for (final FileObject srcFile : files) {
267                     // Build the destination file name
268                     final String relName;
269                     if (srcDirName == null || !srcDirIsBase) {
270                         relName = rootName.getRelativeName(srcFile.getName());
271                     } else {
272                         relName = srcDirName.getRelativeName(srcFile.getName());
273                     }
274 
275                     final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
276 
277                     // Do the copy
278                     handleFile(destFiles, srcFile, destFile);
279                 }
280             }
281         }
282 
283         // Scan the destination files for files with no source file
284         if (detectMissingSourceFiles()) {
285             final FileObject[] allDestFiles = destFolder.findFiles(Selectors.SELECT_FILES);
286             for (final FileObject destFile : allDestFiles) {
287                 if (!destFiles.contains(destFile)) {
288                     handleMissingSourceFile(destFile);
289                 }
290             }
291         }
292     }
293 
294     /**
295      * Handles a single file, checking for collisions where more than one source file maps to the same destination file.
296      */
297     private void handleFile(final Set<FileObject> destFiles, final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile)
298             throws Exception
299 
300     {
301         // Check for duplicate source files
302         if (destFiles.contains(destFile)) {
303             final String message = Messages.getString("vfs.tasks/sync.duplicate-source-files.warn", destFile);
304             logOrDie(message, Project.MSG_WARN);
305         } else {
306             destFiles.add(destFile);
307         }
308 
309         // Handle the file
310         handleFile(srcFile, destFile);
311     }
312 
313     /**
314      * Copies a single file.
315      */
316     private void handleSingleFile() throws Exception {
317         // Make sure there is exactly one source file, and that it exists
318         // and is a file.
319         if (srcFiles.size() > 1) {
320             final String message = Messages.getString("vfs.tasks/sync.too-many-source-files.error");
321             logOrDie(message, Project.MSG_WARN);
322             return;
323         }
324         final SourceInfo src = srcFiles.get(0);
325         final FileObject srcFile = resolveFile(src.file);
326         if (!srcFile.isFile()) {
327             final String message = Messages.getString("vfs.tasks/sync.source-not-file.error", srcFile);
328             logOrDie(message, Project.MSG_WARN);
329             return;
330         }
331 
332         // Locate the destination file
333         final FileObject destFile = resolveFile(destFileUrl);
334 
335         // Do the copy
336         handleFile(srcFile, destFile);
337     }
338 
339     /**
340      * Handles a single source file.
341      */
342     private void handleFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
343         if (!FileObjectUtils.exists(destFile)
344                 || srcFile.getContent().getLastModifiedTime() > destFile.getContent().getLastModifiedTime()) {
345             // Destination file is out-of-date
346             handleOutOfDateFile(srcFile, destFile);
347         } else {
348             // Destination file is up-to-date
349             handleUpToDateFile(srcFile, destFile);
350         }
351     }
352 
353     /**
354      * Handles an out-of-date file.
355      * <p>
356      * This is a file where the destination file either doesn't exist, or is older than the source file.
357      * </p>
358      * <p>
359      * This implementation does nothing.
360      * </p>
361      *
362      * @param srcFile The source file.
363      * @param destFile The destination file.
364      * @throws Exception Implementation can throw any Exception.
365      */
366     protected void handleOutOfDateFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
367         // noop
368     }
369 
370     /**
371      * Handles an up-to-date file.
372      * <p>
373      * This is where the destination file exists and is newer than the source file.
374      * </p>
375      * <p>
376      * This implementation does nothing.
377      * </p>
378      *
379      * @param srcFile The source file.
380      * @param destFile The destination file.
381      * @throws Exception Implementation can throw any Exception.
382      */
383     protected void handleUpToDateFile(final FileObjectect.html#FileObject">FileObject srcFile, final FileObject destFile) throws Exception {
384         // noop
385     }
386 
387     /**
388      * Handles a destination for which there is no corresponding source file.
389      * <p>
390      * This implementation does nothing.
391      * </p>
392      *
393      * @param destFile The existing destination file.
394      * @throws Exception Implementation can throw any Exception.
395      */
396     protected void handleMissingSourceFile(final FileObject destFile) throws Exception {
397         // noop
398     }
399 
400     /**
401      * Check if this task cares about destination files with a missing source file.
402      * <p>
403      * This implementation returns false.
404      * </p>
405      *
406      * @return True if missing file is detected.
407      */
408     protected boolean detectMissingSourceFiles() {
409         return false;
410     }
411 
412     /**
413      * Information about a source file.
414      */
415     public static class SourceInfo {
416         private String file;
417 
418         public void setFile(final String file) {
419             this.file = file;
420         }
421     }
422 
423 }