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