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 folders, and a destination folder, and
034 * performs actions on missing and out-of-date files. Specifically, performs actions on the following:
035 * <ul>
036 * <li>Missing destination file.
037 * <li>Missing source file.
038 * <li>Out-of-date destination file.
039 * <li>Up-to-date destination file.
040 * </ul>
041 *
042 * TODO - Deal with case where dest file maps to a child of one of the source files.<br>
043 * TODO - Deal with case where dest file already exists and is incorrect type (not file, not a folder).<br>
044 * TODO - Use visitors.<br>
045 * TODO - Add default excludes.<br>
046 * TOOD - Allow selector, mapper, filters, etc to be specified.<br>
047 * TODO - Handle source/dest directories as well.<br>
048 * TODO - Allow selector to be specified for choosing which dest files to sync.
049 */
050public abstract class AbstractSyncTask extends VfsTask {
051    private final ArrayList<SourceInfo> srcFiles = new ArrayList<>();
052    private String destFileUrl;
053    private String destDirUrl;
054    private String srcDirUrl;
055    private boolean srcDirIsBase;
056    private boolean failonerror = true;
057    private String filesList;
058
059    /**
060     * Sets the destination file.
061     *
062     * @param destFile The destination file name.
063     */
064    public void setDestFile(final String destFile) {
065        this.destFileUrl = destFile;
066    }
067
068    /**
069     * Sets the destination directory.
070     *
071     * @param destDir The destination directory.
072     */
073    public void setDestDir(final String destDir) {
074        this.destDirUrl = destDir;
075    }
076
077    /**
078     * Sets the source file.
079     *
080     * @param srcFile The source file name.
081     */
082    public void setSrc(final String srcFile) {
083        final SourceInfo src = new SourceInfo();
084        src.setFile(srcFile);
085        addConfiguredSrc(src);
086    }
087
088    /**
089     * Sets the source directory.
090     *
091     * @param srcDir The source directory.
092     */
093    public void setSrcDir(final String srcDir) {
094        this.srcDirUrl = srcDir;
095    }
096
097    /**
098     * Sets whether the source directory should be consider as the base directory.
099     *
100     * @param srcDirIsBase true if the source directory is the base directory.
101     */
102    public void setSrcDirIsBase(final boolean srcDirIsBase) {
103        this.srcDirIsBase = srcDirIsBase;
104    }
105
106    /**
107     * Sets whether we should fail if there was an error or not.
108     *
109     * @param failonerror true if the operation should fail if there is an error.
110     */
111    public void setFailonerror(final boolean failonerror) {
112        this.failonerror = failonerror;
113    }
114
115    /**
116     * Sets whether we should fail if there was an error or not.
117     *
118     * @return true if the operation should fail if there was an error.
119     */
120    public boolean isFailonerror() {
121        return failonerror;
122    }
123
124    /**
125     * Sets the files to includes.
126     *
127     * @param filesList The list of files to include.
128     */
129    public void setIncludes(final String filesList) {
130        this.filesList = filesList;
131    }
132
133    /**
134     * Adds a nested &lt;src&gt; element.
135     *
136     * @param srcInfo A nested source element.
137     * @throws BuildException if the SourceInfo doesn't reference a file.
138     */
139    public void addConfiguredSrc(final SourceInfo srcInfo) throws BuildException {
140        if (srcInfo.file == null) {
141            final String message = Messages.getString("vfs.tasks/sync.no-source-file.error");
142            throw new BuildException(message);
143        }
144        srcFiles.add(srcInfo);
145    }
146
147    /**
148     * Executes this task.
149     *
150     * @throws BuildException if an error occurs.
151     */
152    @Override
153    public void execute() throws BuildException {
154        // Validate
155        if (destFileUrl == null && destDirUrl == null) {
156            final String message = Messages.getString("vfs.tasks/sync.no-destination.error");
157            logOrDie(message, Project.MSG_WARN);
158            return;
159        }
160
161        if (destFileUrl != null && destDirUrl != null) {
162            final String message = Messages.getString("vfs.tasks/sync.too-many-destinations.error");
163            logOrDie(message, Project.MSG_WARN);
164            return;
165        }
166
167        // Add the files of the includes attribute to the list
168        if (srcDirUrl != null && !srcDirUrl.equals(destDirUrl) && filesList != null && filesList.length() > 0) {
169            if (!srcDirUrl.endsWith("/")) {
170                srcDirUrl += "/";
171            }
172            final StringTokenizer tok = new StringTokenizer(filesList, ", \t\n\r\f", false);
173            while (tok.hasMoreTokens()) {
174                String nextFile = tok.nextToken();
175
176                // Basic compatibility with Ant fileset for directories
177                if (nextFile.endsWith("/**")) {
178                    nextFile = nextFile.substring(0, nextFile.length() - 2);
179                }
180
181                final SourceInfo src = new SourceInfo();
182                src.setFile(srcDirUrl + nextFile);
183                addConfiguredSrc(src);
184            }
185        }
186
187        if (srcFiles.size() == 0) {
188            final String message = Messages.getString("vfs.tasks/sync.no-source-files.warn");
189            logOrDie(message, Project.MSG_WARN);
190            return;
191        }
192
193        // Perform the sync
194        try {
195            if (destFileUrl != null) {
196                handleSingleFile();
197            } else {
198                handleFiles();
199            }
200        } catch (final BuildException e) {
201            throw e;
202        } catch (final Exception e) {
203            throw new BuildException(e.getMessage(), e);
204        }
205    }
206
207    protected void logOrDie(final String message, final int level) {
208        if (!isFailonerror()) {
209            log(message, level);
210            return;
211        }
212        throw new BuildException(message);
213    }
214
215    /**
216     * Copies the source files to the destination.
217     */
218    private void handleFiles() throws Exception {
219        // Locate the destination folder, and make sure it exists
220        final FileObject destFolder = resolveFile(destDirUrl);
221        destFolder.createFolder();
222
223        // Locate the source files, and make sure they exist
224        FileName srcDirName = null;
225        if (srcDirUrl != null) {
226            srcDirName = resolveFile(srcDirUrl).getName();
227        }
228        final ArrayList<FileObject> srcs = new ArrayList<>();
229        for (int i = 0; i < srcFiles.size(); i++) {
230            // Locate the source file, and make sure it exists
231            final SourceInfo src = srcFiles.get(i);
232            final FileObject srcFile = resolveFile(src.file);
233            if (!srcFile.exists()) {
234                final String message = Messages.getString("vfs.tasks/sync.src-file-no-exist.warn", srcFile);
235
236                logOrDie(message, Project.MSG_WARN);
237            } else {
238                srcs.add(srcFile);
239            }
240        }
241
242        // Scan the source files
243        final Set<FileObject> destFiles = new HashSet<>();
244        for (int i = 0; i < srcs.size(); i++) {
245            final FileObject rootFile = srcs.get(i);
246            final FileName rootName = rootFile.getName();
247
248            if (rootFile.isFile()) {
249                // Build the destination file name
250                String relName = null;
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                    String relName = null;
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 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 FileObject srcFile, final FileObject destFile) throws Exception {
343        if (!destFile.exists()
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     * This implementation does nothing.
359     *
360     * @param srcFile The source file.
361     * @param destFile The destination file.
362     * @throws Exception Implementation can throw any Exception.
363     */
364    protected void handleOutOfDateFile(final FileObject srcFile, final FileObject destFile) throws Exception {
365    }
366
367    /**
368     * Handles an up-to-date file.
369     * <p>
370     * This is where the destination file exists and is newer than the source file.
371     * <p>
372     * This implementation does nothing.
373     *
374     * @param srcFile The source file.
375     * @param destFile The destination file.
376     * @throws Exception Implementation can throw any Exception.
377     */
378    protected void handleUpToDateFile(final FileObject srcFile, final FileObject destFile) throws Exception {
379    }
380
381    /**
382     * Handles a destination for which there is no corresponding source file.
383     * <p>
384     * This implementation does nothing.
385     *
386     * @param destFile The existing destination file.
387     * @throws Exception Implementation can throw any Exception.
388     */
389    protected void handleMissingSourceFile(final FileObject destFile) throws Exception {
390    }
391
392    /**
393     * Check if this task cares about destination files with a missing source file.
394     * <p>
395     * This implementation returns false.
396     *
397     * @return True if missing file is detected.
398     */
399    protected boolean detectMissingSourceFiles() {
400        return false;
401    }
402
403    /**
404     * Information about a source file.
405     */
406    public static class SourceInfo {
407        private String file;
408
409        public void setFile(final String file) {
410            this.file = file;
411        }
412    }
413
414}