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.FileObjectUtils;
029import org.apache.commons.vfs2.util.Messages;
030import org.apache.tools.ant.BuildException;
031import org.apache.tools.ant.Project;
032
033/**
034 * An abstract file synchronization task. Scans a set of source files and folders, and a destination folder, and
035 * performs actions on missing and 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 * <ul>
044 * <li>TODO - Deal with case where dest file maps to a child of one of the source files.</li>
045 * <li>TODO - Deal with case where dest file already exists and is incorrect type (not file, not a folder).</li>
046 * <li>TODO - Use visitors.</li>
047 * <li>TODO - Add default excludes.</li>
048 * <li>TODO - Allow selector, mapper, filters, etc to be specified.</li>
049 * <li>TODO - Handle source/dest directories as well.</li>
050 * <li>TODO - Allow selector to be specified for choosing which dest files to sync.</li>
051 * </ul>
052 */
053public abstract class AbstractSyncTask extends VfsTask {
054
055    /**
056     * Information about a source file.
057     */
058    public static class SourceInfo {
059
060        private String file;
061
062        /**
063         * Constructs a new instance.
064         */
065        public SourceInfo() {
066            // empty
067        }
068
069        /**
070         * Sets the file.
071         *
072         * @param file the file.
073         */
074        public void setFile(final String file) {
075            this.file = file;
076        }
077    }
078    private final ArrayList<SourceInfo> srcFiles = new ArrayList<>();
079    private String destFileUrl;
080    private String destDirUrl;
081    private String srcDirUrl;
082    private boolean srcDirIsBase;
083    private boolean failOnError = true;
084
085    private String filesList;
086
087    /**
088     * Constructs a new instance.
089     */
090    public AbstractSyncTask() {
091        // empty
092    }
093
094    /**
095     * Adds a nested &lt;src&gt; element.
096     *
097     * @param srcInfo A nested source element.
098     * @throws BuildException if the SourceInfo doesn't reference a file.
099     */
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}