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     */
017    package org.apache.commons.vfs2.tasks;
018    
019    import java.util.ArrayList;
020    import java.util.HashSet;
021    import java.util.Set;
022    import java.util.StringTokenizer;
023    
024    import org.apache.commons.vfs2.FileName;
025    import org.apache.commons.vfs2.FileObject;
026    import org.apache.commons.vfs2.FileType;
027    import org.apache.commons.vfs2.NameScope;
028    import org.apache.commons.vfs2.Selectors;
029    import org.apache.commons.vfs2.util.Messages;
030    import org.apache.tools.ant.BuildException;
031    import org.apache.tools.ant.Project;
032    
033    /**
034     * An abstract file synchronization task.  Scans a set of source files and
035     * folders, and a destination folder, and performs actions on missing and
036     * out-of-date files.  Specifically, performs actions on the following:
037     * <ul>
038     * <li>Missing destination file.
039     * <li>Missing source file.
040     * <li>Out-of-date destination file.
041     * <li>Up-to-date destination file.
042     * </ul>
043     *
044     * @author <a href="http://commons.apache.org/vfs/team-list.html">Commons VFS team</a>
045     * @todo Deal with case where dest file maps to a child of one of the source files
046     * @todo Deal with case where dest file already exists and is incorrect type (not file, not a folder)
047     * @todo Use visitors
048     * @todo Add default excludes
049     * @todo Allow selector, mapper, filters, etc to be specified.
050     * @todo Handle source/dest directories as well
051     * @todo Allow selector to be specified for choosing which dest files to sync
052     */
053    public abstract class AbstractSyncTask
054        extends VfsTask
055    {
056        private final ArrayList<SourceInfo> srcFiles = new ArrayList<SourceInfo>();
057        private String destFileUrl;
058        private String destDirUrl;
059        private String srcDirUrl;
060        private boolean srcDirIsBase;
061        private boolean failonerror = true;
062        private String filesList;
063    
064        /**
065         * Sets the destination file.
066         * @param destFile The destination file name.
067         */
068        public void setDestFile(final String destFile)
069        {
070            this.destFileUrl = destFile;
071        }
072    
073        /**
074         * Sets the destination directory.
075         * @param destDir The destination directory.
076         */
077        public void setDestDir(final String destDir)
078        {
079            this.destDirUrl = destDir;
080        }
081    
082        /**
083         * Sets the source file.
084         * @param srcFile The source file name.
085         */
086        public void setSrc(final String srcFile)
087        {
088            final SourceInfo src = new SourceInfo();
089            src.setFile(srcFile);
090            addConfiguredSrc(src);
091        }
092    
093        /**
094         * Sets the source directory.
095         * @param srcDir The source directory.
096         */
097        public void setSrcDir(final String srcDir)
098        {
099            this.srcDirUrl = srcDir;
100        }
101    
102        /**
103         * Sets whether the source directory should be consider as the base directory.
104         * @param srcDirIsBase true if the source directory is the base directory.
105         */
106        public void setSrcDirIsBase(final boolean srcDirIsBase)
107        {
108            this.srcDirIsBase = srcDirIsBase;
109        }
110    
111        /**
112         * Sets whether we should fail if there was an error or not.
113         * @param failonerror true if the operation should fail if there is an error.
114         */
115        public void setFailonerror(final boolean failonerror)
116        {
117            this.failonerror = failonerror;
118        }
119    
120        /**
121         * Sets whether we should fail if there was an error or not.
122         * @return true if the operation should fail if there was an error.
123         */
124        public boolean isFailonerror()
125        {
126            return failonerror;
127        }
128    
129        /**
130         * Sets the files to includes.
131         * @param filesList The list of files to include.
132         */
133        public void setIncludes(final String filesList)
134        {
135            this.filesList = filesList;
136        }
137    
138        /**
139         * Adds a nested &lt;src&gt; element.
140         * @param srcInfo A nested source element.
141         * @throws BuildException if the SourceInfo doesn't reference a file.
142         */
143        public void addConfiguredSrc(final SourceInfo srcInfo)
144            throws BuildException
145        {
146            if (srcInfo.file == null)
147            {
148                final String message = Messages.getString("vfs.tasks/sync.no-source-file.error");
149                throw new BuildException(message);
150            }
151            srcFiles.add(srcInfo);
152        }
153    
154        /**
155         * Executes this task.
156         * @throws BuildException if an error occurs.
157         */
158        @Override
159        public void execute() throws BuildException
160        {
161            // Validate
162            if (destFileUrl == null && destDirUrl == null)
163            {
164                final String message =
165                    Messages.getString("vfs.tasks/sync.no-destination.error");
166                logOrDie(message, Project.MSG_WARN);
167                return;
168            }
169    
170            if (destFileUrl != null && destDirUrl != null)
171            {
172                final String message =
173                    Messages.getString("vfs.tasks/sync.too-many-destinations.error");
174                logOrDie(message, Project.MSG_WARN);
175                return;
176            }
177    
178            // Add the files of the includes attribute to the list
179            if (srcDirUrl != null && !srcDirUrl.equals(destDirUrl) && filesList != null && filesList.length() > 0)
180            {
181                if (!srcDirUrl.endsWith("/"))
182                {
183                    srcDirUrl += "/";
184                }
185                StringTokenizer tok = new StringTokenizer(filesList, ", \t\n\r\f", false);
186                while (tok.hasMoreTokens())
187                {
188                    String nextFile = tok.nextToken();
189    
190                    // Basic compatibility with Ant fileset for directories
191                    if (nextFile.endsWith("/**"))
192                    {
193                        nextFile = nextFile.substring(0, nextFile.length() - 2);
194                    }
195    
196                    final SourceInfo src = new SourceInfo();
197                    src.setFile(srcDirUrl + nextFile);
198                    addConfiguredSrc(src);
199                }
200            }
201    
202            if (srcFiles.size() == 0)
203            {
204                final String message = Messages.getString("vfs.tasks/sync.no-source-files.warn");
205                logOrDie(message, Project.MSG_WARN);
206                return;
207            }
208    
209            // Perform the sync
210            try
211            {
212                if (destFileUrl != null)
213                {
214                    handleSingleFile();
215                }
216                else
217                {
218                    handleFiles();
219                }
220            }
221            catch (final BuildException e)
222            {
223                throw e;
224            }
225            catch (final Exception e)
226            {
227                throw new BuildException(e.getMessage(), e);
228            }
229        }
230    
231        protected void logOrDie(final String message, int level)
232        {
233            if (!isFailonerror())
234            {
235                log(message, level);
236                return;
237            }
238            throw new BuildException(message);
239        }
240    
241        /**
242         * Copies the source files to the destination.
243         */
244        private void handleFiles() throws Exception
245        {
246            // Locate the destination folder, and make sure it exists
247            final FileObject destFolder = resolveFile(destDirUrl);
248            destFolder.createFolder();
249    
250            // Locate the source files, and make sure they exist
251            FileName srcDirName = null;
252            if (srcDirUrl != null)
253            {
254                srcDirName = resolveFile(srcDirUrl).getName();
255            }
256            final ArrayList<FileObject> srcs = new ArrayList<FileObject>();
257            for (int i = 0; i < srcFiles.size(); i++)
258            {
259                // Locate the source file, and make sure it exists
260                final SourceInfo src = srcFiles.get(i);
261                final FileObject srcFile = resolveFile(src.file);
262                if (!srcFile.exists())
263                {
264                    final String message =
265                        Messages.getString("vfs.tasks/sync.src-file-no-exist.warn", srcFile);
266    
267                    logOrDie(message, Project.MSG_WARN);
268                }
269                else
270                {
271                    srcs.add(srcFile);
272                }
273            }
274    
275            // Scan the source files
276            final Set<FileObject> destFiles = new HashSet<FileObject>();
277            for (int i = 0; i < srcs.size(); i++)
278            {
279                final FileObject rootFile = srcs.get(i);
280                final FileName rootName = rootFile.getName();
281    
282                if (rootFile.getType() == FileType.FILE)
283                {
284                    // Build the destination file name
285                    String relName = null;
286                    if (srcDirName == null || !srcDirIsBase)
287                    {
288                        relName = rootName.getBaseName();
289                    }
290                    else
291                    {
292                        relName = srcDirName.getRelativeName(rootName);
293                    }
294                    final FileObject destFile = destFolder.resolveFile(relName, NameScope.DESCENDENT);
295    
296                    // Do the copy
297                    handleFile(destFiles, rootFile, destFile);
298                }
299                else
300                {
301                    // Find matching files
302                    // If srcDirIsBase is true, select also the sub-directories
303                    final FileObject[] files = rootFile.findFiles(srcDirIsBase
304                        ? Selectors.SELECT_ALL : Selectors.SELECT_FILES);
305    
306                    for (int j = 0; j < files.length; j++)
307                    {
308                        final FileObject srcFile = files[j];
309    
310                        // Build the destination file name
311                        String relName = null;
312                        if (srcDirName == null || !srcDirIsBase)
313                        {
314                            relName = rootName.getRelativeName(srcFile.getName());
315                        }
316                        else
317                        {
318                            relName = srcDirName.getRelativeName(srcFile.getName());
319                        }
320    
321                        final FileObject destFile =
322                            destFolder.resolveFile(relName, NameScope.DESCENDENT);
323    
324                        // Do the copy
325                        handleFile(destFiles, srcFile, destFile);
326                    }
327                }
328            }
329    
330            // Scan the destination files for files with no source file
331            if (detectMissingSourceFiles())
332            {
333                final FileObject[] allDestFiles = destFolder.findFiles(Selectors.SELECT_FILES);
334                for (int i = 0; i < allDestFiles.length; i++)
335                {
336                    final FileObject destFile = allDestFiles[i];
337                    if (!destFiles.contains(destFile))
338                    {
339                        handleMissingSourceFile(destFile);
340                    }
341                }
342            }
343        }
344    
345        /**
346         * Handles a single file, checking for collisions where more than one
347         * source file maps to the same destination file.
348         */
349        private void handleFile(final Set<FileObject> destFiles,
350                                final FileObject srcFile,
351                                final FileObject destFile) throws Exception
352    
353        {
354            // Check for duplicate source files
355            if (destFiles.contains(destFile))
356            {
357                final String message = Messages.getString("vfs.tasks/sync.duplicate-source-files.warn", destFile);
358                logOrDie(message, Project.MSG_WARN);
359            }
360            else
361            {
362                destFiles.add(destFile);
363            }
364    
365            // Handle the file
366            handleFile(srcFile, destFile);
367        }
368    
369        /**
370         * Copies a single file.
371         */
372        private void handleSingleFile() throws Exception
373        {
374            // Make sure there is exactly one source file, and that it exists
375            // and is a file.
376            if (srcFiles.size() > 1)
377            {
378                final String message =
379                    Messages.getString("vfs.tasks/sync.too-many-source-files.error");
380                logOrDie(message, Project.MSG_WARN);
381                return;
382            }
383            final SourceInfo src = srcFiles.get(0);
384            final FileObject srcFile = resolveFile(src.file);
385            if (srcFile.getType() != FileType.FILE)
386            {
387                final String message =
388                    Messages.getString("vfs.tasks/sync.source-not-file.error", srcFile);
389                logOrDie(message, Project.MSG_WARN);
390                return;
391            }
392    
393            // Locate the destination file
394            final FileObject destFile = resolveFile(destFileUrl);
395    
396            // Do the copy
397            handleFile(srcFile, destFile);
398        }
399    
400        /**
401         * Handles a single source file.
402         */
403        private void handleFile(final FileObject srcFile,
404                                final FileObject destFile)
405            throws Exception
406        {
407            if (!destFile.exists()
408                || srcFile.getContent().getLastModifiedTime() > destFile.getContent().getLastModifiedTime())
409            {
410                // Destination file is out-of-date
411                handleOutOfDateFile(srcFile, destFile);
412            }
413            else
414            {
415                // Destination file is up-to-date
416                handleUpToDateFile(srcFile, destFile);
417            }
418        }
419    
420        /**
421         * Handles an out-of-date file (a file where the destination file
422         * either doesn't exist, or is older than the source file).
423         * This implementation does nothing.
424         */
425        protected void handleOutOfDateFile(final FileObject srcFile,
426                                           final FileObject destFile)
427            throws Exception
428        {
429        }
430    
431        /**
432         * Handles an up-to-date file (where the destination file exists and is
433         * newer than the source file).  This implementation does nothing.
434         */
435        protected void handleUpToDateFile(final FileObject srcFile,
436                                          final FileObject destFile)
437            throws Exception
438        {
439        }
440    
441        /**
442         * Handles a destination for which there is no corresponding source file.
443         * This implementation does nothing.
444         */
445        protected void handleMissingSourceFile(final FileObject destFile)
446            throws Exception
447        {
448        }
449    
450        /**
451         * Check if this task cares about destination files with a missing source
452         * file.  This implementation returns false.
453         */
454        protected boolean detectMissingSourceFiles()
455        {
456            return false;
457        }
458    
459        /**
460         * Information about a source file.
461         */
462        public static class SourceInfo
463        {
464            private String file;
465    
466            public void setFile(final String file)
467            {
468                this.file = file;
469            }
470        }
471    
472    }