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