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