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 <src> 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 }