AbstractFakeFileSystem.java revision 2a0a3f946dba517a01cc26278f905156857c9c91
1/* 2 * Copyright 2008 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package org.mockftpserver.fake.filesystem; 17 18import org.apache.log4j.Logger; 19import org.mockftpserver.core.util.Assert; 20import org.mockftpserver.core.util.PatternUtil; 21import org.mockftpserver.core.util.StringUtil; 22 23import java.util.ArrayList; 24import java.util.Collections; 25import java.util.Date; 26import java.util.HashMap; 27import java.util.Iterator; 28import java.util.List; 29import java.util.Map; 30 31/** 32 * Abstract superclass for implementation of the FileSystem interface that manage the files 33 * and directories in memory, simulating a real file system. 34 * <p/> 35 * If the <code>createParentDirectoriesAutomatically</code> property is set to <code>true</code>, 36 * then creating a directory or file will automatically create any parent directories (recursively) 37 * that do not already exist. If <code>false</code>, then creating a directory or file throws an 38 * exception if its parent directory does not exist. This value defaults to <code>true</code>. 39 * <p/> 40 * The <code>directoryListingFormatter</code> property holds an instance of {@link DirectoryListingFormatter} , 41 * used by the <code>formatDirectoryListing</code> method to format directory listings in a 42 * filesystem-specific manner. This property must be initialized by concrete subclasses. 43 * 44 * @author Chris Mair 45 * @version $Revision$ - $Date$ 46 */ 47public abstract class AbstractFakeFileSystem implements FileSystem { 48 49 private static final Logger LOG = Logger.getLogger(AbstractFakeFileSystem.class); 50 51 /** 52 * If <code>true</code>, creating a directory or file will automatically create 53 * any parent directories (recursively) that do not already exist. If <code>false</code>, 54 * then creating a directory or file throws an exception if its parent directory 55 * does not exist. This value defaults to <code>true</code>. 56 */ 57 private boolean createParentDirectoriesAutomatically = true; 58 59 /** 60 * The {@link DirectoryListingFormatter} used by the {@link #formatDirectoryListing(FileSystemEntry)} 61 * method. This must be initialized by concrete subclasses. 62 */ 63 private DirectoryListingFormatter directoryListingFormatter; 64 65 private Map entries = new HashMap(); 66 67 //------------------------------------------------------------------------- 68 // Public API 69 //------------------------------------------------------------------------- 70 71 public boolean isCreateParentDirectoriesAutomatically() { 72 return createParentDirectoriesAutomatically; 73 } 74 75 public void setCreateParentDirectoriesAutomatically(boolean createParentDirectoriesAutomatically) { 76 this.createParentDirectoriesAutomatically = createParentDirectoriesAutomatically; 77 } 78 79 public DirectoryListingFormatter getDirectoryListingFormatter() { 80 return directoryListingFormatter; 81 } 82 83 public void setDirectoryListingFormatter(DirectoryListingFormatter directoryListingFormatter) { 84 this.directoryListingFormatter = directoryListingFormatter; 85 } 86 87 /** 88 * Add each of the entries in the specified List to this filesystem. Note that this does not affect 89 * entries already existing within this filesystem. 90 * 91 * @param entriesToAdd - the List of FileSystemEntry entries to add 92 */ 93 public void setEntries(List entriesToAdd) { 94 for (Iterator iter = entriesToAdd.iterator(); iter.hasNext();) { 95 FileSystemEntry entry = (FileSystemEntry) iter.next(); 96 add(entry); 97 } 98 } 99 100 /** 101 * Add the specified file system entry (file or directory) to this file system 102 * 103 * @param entry - the FileSystemEntry to add 104 */ 105 public void add(FileSystemEntry entry) { 106 String path = entry.getPath(); 107 checkForInvalidFilename(path); 108 if (getEntry(path) != null) { 109 throw new FileSystemException(path, "filesystem.pathAlreadyExists"); 110 } 111 112 if (!parentDirectoryExists(path)) { 113 String parent = getParent(path); 114 if (createParentDirectoriesAutomatically) { 115 add(new DirectoryEntry(parent)); 116 } else { 117 throw new FileSystemException(parent, "filesystem.parentDirectoryDoesNotExist"); 118 } 119 } 120 121 // Set lastModified, if not already set 122 if (entry.getLastModified() == null) { 123 entry.setLastModified(new Date()); 124 } 125 126 entries.put(getFileSystemEntryKey(path), entry); 127 entry.lockPath(); 128 } 129 130 /** 131 * Delete the file or directory specified by the path. Return true if the file is successfully 132 * deleted, false otherwise. If the path refers to a directory, it must be empty. Return false 133 * if the path does not refer to a valid file or directory or if it is a non-empty directory. 134 * 135 * @param path - the path of the file or directory to delete 136 * @return true if the file or directory is successfully deleted 137 * @throws org.mockftpserver.core.util.AssertFailedException 138 * - if path is null 139 * @see org.mockftpserver.fake.filesystem.FileSystem#delete(java.lang.String) 140 */ 141 public boolean delete(String path) { 142 Assert.notNull(path, "path"); 143 144 if (getEntry(path) != null && !hasChildren(path)) { 145 removeEntry(path); 146 return true; 147 } 148 return false; 149 } 150 151 /** 152 * Return true if there exists a file or directory at the specified path 153 * 154 * @param path - the path 155 * @return true if the file/directory exists 156 * @throws AssertionError - if path is null 157 * @see org.mockftpserver.fake.filesystem.FileSystem#exists(java.lang.String) 158 */ 159 public boolean exists(String path) { 160 Assert.notNull(path, "path"); 161 return getEntry(path) != null; 162 } 163 164 /** 165 * Return true if the specified path designates an existing directory, false otherwise 166 * 167 * @param path - the path 168 * @return true if path is a directory, false otherwise 169 * @throws AssertionError - if path is null 170 * @see org.mockftpserver.fake.filesystem.FileSystem#isDirectory(java.lang.String) 171 */ 172 public boolean isDirectory(String path) { 173 Assert.notNull(path, "path"); 174 FileSystemEntry entry = getEntry(path); 175 return entry != null && entry.isDirectory(); 176 } 177 178 /** 179 * Return true if the specified path designates an existing file, false otherwise 180 * 181 * @param path - the path 182 * @return true if path is a file, false otherwise 183 * @throws AssertionError - if path is null 184 * @see org.mockftpserver.fake.filesystem.FileSystem#isFile(java.lang.String) 185 */ 186 public boolean isFile(String path) { 187 Assert.notNull(path, "path"); 188 FileSystemEntry entry = getEntry(path); 189 return entry != null && !entry.isDirectory(); 190 } 191 192 /** 193 * Return the List of FileSystemEntry objects for the files in the specified directory or group of 194 * files. If the path specifies a single file, then return a list with a single FileSystemEntry 195 * object representing that file. If the path does not refer to an existing directory or 196 * group of files, then an empty List is returned. 197 * 198 * @param path - the path specifying a directory or group of files; may contain wildcards (? or *) 199 * @return the List of FileSystemEntry objects for the specified directory or file; may be empty 200 * @see org.mockftpserver.fake.filesystem.FileSystem#listFiles(java.lang.String) 201 */ 202 public List listFiles(String path) { 203 if (isFile(path)) { 204 return Collections.singletonList(getEntry(path)); 205 } 206 207 List entryList = new ArrayList(); 208 List children = children(path); 209 Iterator iter = children.iterator(); 210 while (iter.hasNext()) { 211 String childPath = (String) iter.next(); 212 FileSystemEntry fileSystemEntry = getEntry(childPath); 213 entryList.add(fileSystemEntry); 214 } 215 return entryList; 216 } 217 218 /** 219 * Return the List of filenames in the specified directory path or file path. If the path specifies 220 * a single file, then return that single filename. The returned filenames do not 221 * include a path. If the path does not refer to a valid directory or file path, then an empty List 222 * is returned. 223 * 224 * @param path - the path specifying a directory or group of files; may contain wildcards (? or *) 225 * @return the List of filenames (not including paths) for all files in the specified directory 226 * or file path; may be empty 227 * @throws AssertionError - if path is null 228 * @see org.mockftpserver.fake.filesystem.FileSystem#listNames(java.lang.String) 229 */ 230 public List listNames(String path) { 231 if (isFile(path)) { 232 return Collections.singletonList(getName(path)); 233 } 234 235 List filenames = new ArrayList(); 236 List children = children(path); 237 Iterator iter = children.iterator(); 238 while (iter.hasNext()) { 239 String childPath = (String) iter.next(); 240 FileSystemEntry fileSystemEntry = getEntry(childPath); 241 filenames.add(fileSystemEntry.getName()); 242 } 243 return filenames; 244 } 245 246 /** 247 * Rename the file or directory. Specify the FROM path and the TO path. Throw an exception if the FROM path or 248 * the parent directory of the TO path do not exist; or if the rename fails for another reason. 249 * 250 * @param fromPath - the source (old) path + filename 251 * @param toPath - the target (new) path + filename 252 * @throws AssertionError - if fromPath or toPath is null 253 * @throws FileSystemException - if the rename fails. 254 */ 255 public void rename(String fromPath, String toPath) { 256 Assert.notNull(toPath, "toPath"); 257 Assert.notNull(fromPath, "fromPath"); 258 259 FileSystemEntry entry = getRequiredEntry(fromPath); 260 261 String normalizedFromPath = normalize(fromPath); 262 String normalizedToPath = normalize(toPath); 263 264 if (!entry.isDirectory()) { 265 renamePath(entry, normalizedToPath); 266 return; 267 } 268 269 // Create the TO directory entry first so that the destination path exists when you 270 // move the children. Remove the FROM path after all children have been moved 271 add(new DirectoryEntry(normalizedToPath)); 272 273 List children = descendents(fromPath); 274 Iterator iter = children.iterator(); 275 while (iter.hasNext()) { 276 String childPath = (String) iter.next(); 277 FileSystemEntry child = getRequiredEntry(childPath); 278 String normalizedChildPath = normalize(child.getPath()); 279 Assert.isTrue(normalizedChildPath.startsWith(normalizedFromPath), "Starts with FROM path"); 280 String childToPath = normalizedToPath + normalizedChildPath.substring(normalizedFromPath.length()); 281 renamePath(child, childToPath); 282 } 283 Assert.isTrue(children(normalizedFromPath).isEmpty(), "Must have no children: " + normalizedFromPath); 284 removeEntry(normalizedFromPath); 285 } 286 287 /** 288 * @see java.lang.Object#toString() 289 */ 290 public String toString() { 291 return this.getClass().getName() + entries; 292 } 293 294 /** 295 * Return the formatted directory listing entry for the file represented by the specified FileSystemEntry 296 * 297 * @param fileSystemEntry - the FileSystemEntry representing the file or directory entry to be formatted 298 * @return the the formatted directory listing entry 299 */ 300 public String formatDirectoryListing(FileSystemEntry fileSystemEntry) { 301 Assert.notNull(directoryListingFormatter, "directoryListingFormatter"); 302 Assert.notNull(fileSystemEntry, "fileSystemEntry"); 303 return directoryListingFormatter.format(fileSystemEntry); 304 } 305 306 /** 307 * Build a path from the two path components. Concatenate path1 and path2. Insert the path 308 * separator character in between if necessary (i.e., if both are non-empty and path1 does not already 309 * end with a separator character AND path2 does not begin with one). 310 * 311 * @param path1 - the first path component may be null or empty 312 * @param path2 - the second path component may be null or empty 313 * @return the path resulting from concatenating path1 to path2 314 */ 315 public String path(String path1, String path2) { 316 StringBuffer buf = new StringBuffer(); 317 if (path1 != null && path1.length() > 0) { 318 buf.append(path1); 319 } 320 if (path2 != null && path2.length() > 0) { 321 if ((path1 != null && path1.length() > 0) 322 && (!isSeparator(path1.charAt(path1.length() - 1))) 323 && (!isSeparator(path2.charAt(0)))) { 324 buf.append(this.getSeparator()); 325 } 326 buf.append(path2); 327 } 328 return buf.toString(); 329 } 330 331 /** 332 * Return the parent path of the specified path. If <code>path</code> specifies a filename, 333 * then this method returns the path of the directory containing that file. If <code>path</code> 334 * specifies a directory, the this method returns its parent directory. If <code>path</code> is 335 * empty or does not have a parent component, then return an empty string. 336 * <p/> 337 * All path separators in the returned path are converted to the system-dependent separator character. 338 * 339 * @param path - the path 340 * @return the parent of the specified path, or null if <code>path</code> has no parent 341 * @throws AssertionError - if path is null 342 */ 343 public String getParent(String path) { 344 List parts = normalizedComponents(path); 345 if (parts.size() < 2) { 346 return null; 347 } 348 parts.remove(parts.size() - 1); 349 return componentsToPath(parts); 350 } 351 352 /** 353 * Returns the name of the file or directory denoted by this abstract 354 * pathname. This is just the last name in the pathname's name 355 * sequence. If the pathname's name sequence is empty, then the empty string is returned. 356 * 357 * @return The name of the file or directory denoted by this abstract pathname, or the 358 * empty string if this pathname's name sequence is empty 359 */ 360 public String getName(String path) { 361 Assert.notNull(path, "path"); 362 String normalized = normalize(path); 363 int separatorIndex = normalized.lastIndexOf(this.getSeparator()); 364 return (separatorIndex == -1) ? normalized : normalized.substring(separatorIndex + 1); 365 } 366 367 /** 368 * Returns the FileSystemEntry object representing the file system entry at the specified path, or null 369 * if the path does not specify an existing file or directory within this file system. 370 * 371 * @param path - the path of the file or directory within this file system 372 * @return the FileSystemEntry containing the information for the file or directory, or else null 373 * @see FileSystem#getEntry(String) 374 */ 375 public FileSystemEntry getEntry(String path) { 376 return (FileSystemEntry) entries.get(getFileSystemEntryKey(path)); 377 } 378 379 //------------------------------------------------------------------------- 380 // Abstract Methods 381 //------------------------------------------------------------------------- 382 383 /** 384 * @return true if the specified dir/file path name is valid according to the current filesystem. 385 */ 386 protected abstract boolean isValidName(String path); 387 388 /** 389 * @return the file system-specific file separator as a char 390 */ 391 protected abstract char getSeparatorChar(); 392 393 /** 394 * @return true if the specified path component is a root for this filesystem 395 */ 396 protected abstract boolean isRoot(String pathComponent); 397 398 /** 399 * Return true if the specified char is a separator character for this filesystem 400 * 401 * @param c - the character to test 402 * @return true if the specified char is a separator character 403 */ 404 protected abstract boolean isSeparator(char c); 405 406 //------------------------------------------------------------------------- 407 // Internal Helper Methods 408 //------------------------------------------------------------------------- 409 410 /** 411 * @return the file system-specific file separator as a String 412 */ 413 protected String getSeparator() { 414 return Character.toString(getSeparatorChar()); 415 } 416 417 /** 418 * Return the normalized and unique key used to access the file system entry 419 * 420 * @param path - the path 421 * @return the corresponding normalized key 422 */ 423 protected String getFileSystemEntryKey(String path) { 424 return normalize(path); 425 } 426 427 /** 428 * Return the standard, normalized form of the path. 429 * 430 * @param path - the path 431 * @return the path in a standard, unique, canonical form 432 * @throws AssertionError - if path is null 433 */ 434 protected String normalize(String path) { 435 return componentsToPath(normalizedComponents(path)); 436 } 437 438 /** 439 * Throw an InvalidFilenameException if the specified path is not valid. 440 */ 441 protected void checkForInvalidFilename(String path) { 442 if (!isValidName(path)) { 443 throw new InvalidFilenameException(path); 444 } 445 } 446 447 /** 448 * Rename the file system entry to the specified path name 449 * 450 * @param entry - the file system entry 451 * @param toPath - the TO path (normalized) 452 */ 453 protected void renamePath(FileSystemEntry entry, String toPath) { 454 String normalizedFrom = normalize(entry.getPath()); 455 String normalizedTo = normalize(toPath); 456 LOG.info("renaming from [" + normalizedFrom + "] to [" + normalizedTo + "]"); 457 FileSystemEntry newEntry = entry.cloneWithNewPath(normalizedTo); 458 add(newEntry); 459 // Do this at the end, in case the addEntry() failed 460 removeEntry(normalizedFrom); 461 } 462 463 /** 464 * Return the FileSystemEntry for the specified path. Throw FileSystemException if the 465 * specified path does not exist. 466 * 467 * @param path - the path 468 * @return the FileSystemEntry 469 * @throws FileSystemException - if the specified path does not exist 470 */ 471 protected FileSystemEntry getRequiredEntry(String path) { 472 FileSystemEntry entry = getEntry(path); 473 if (entry == null) { 474 LOG.error("Path does not exist: " + path); 475 throw new FileSystemException(normalize(path), "filesystem.pathDoesNotExist"); 476 } 477 return entry; 478 } 479 480 /** 481 * Return the components of the specified path as a List. The components are normalized, and 482 * the returned List does not include path separator characters. 483 */ 484 protected List normalizedComponents(String path) { 485 Assert.notNull(path, "path"); 486 char otherSeparator = this.getSeparatorChar() == '/' ? '\\' : '/'; 487 String p = path.replace(otherSeparator, this.getSeparatorChar()); 488 489 // TODO better way to do this 490 if (p.equals(this.getSeparator())) { 491 return Collections.singletonList(""); 492 } 493 494 String[] parts = p.split("\\" + this.getSeparator()); 495 List result = new ArrayList(); 496 for (int i = 0; i < parts.length; i++) { 497 String part = parts[i]; 498 if (part.equals("..")) { 499 result.remove(result.size() - 1); 500 } else if (!part.equals(".")) { 501 result.add(part); 502 } 503 } 504 return result; 505 } 506 507 /** 508 * Build a path from the specified list of path components 509 * 510 * @param components - the list of path components 511 * @return the resulting path 512 */ 513 protected String componentsToPath(List components) { 514 if (components.size() == 1) { 515 String first = (String) components.get(0); 516 if (first.length() == 0 || isRoot(first)) { 517 return first + this.getSeparator(); 518 } 519 } 520 return StringUtil.join(components, this.getSeparator()); 521 } 522 523 /** 524 * Return true if the specified path designates an absolute file path. 525 * 526 * @param path - the path 527 * @return true if path is absolute, false otherwise 528 * @throws AssertionError - if path is null 529 */ 530 public boolean isAbsolute(String path) { 531 return isValidName(path); 532 } 533 534 /** 535 * Return true if the specified path exists 536 * 537 * @param path - the path 538 * @return true if the path exists 539 */ 540 private boolean pathExists(String path) { 541 return getEntry(path) != null; 542 } 543 544 /** 545 * Throw AssertionError if the path is null. Throw FileSystemException if the specified 546 * path does not exist. 547 * 548 * @param path - the path 549 * @throws AssertionError - if the specified path is null 550 * @throws FileSystemException - if the specified path does not exist 551 */ 552 private void verifyPathExists(String path) { 553 Assert.notNull(path, "path"); 554 getRequiredEntry(path); 555 } 556 557 /** 558 * Verify that the path refers to an existing directory. Throw AssertionError if the path is null. Throw 559 * FileSystemException if the specified path does not exist or is not a directory. 560 * 561 * @param path - the path 562 * @throws AssertionError - if the specified path is null 563 * @throws FileSystemException - if the specified path does not exist or is not a directory 564 */ 565 private void verifyIsDirectory(String path) { 566 Assert.notNull(path, "path"); 567 FileSystemEntry entry = getRequiredEntry(path); 568 if (!entry.isDirectory()) { 569 throw new FileSystemException(path, "filesystem.isDirectory"); 570 } 571 } 572 573 /** 574 * Verify that the path refers to an existing file. Throw AssertionError if the path is null. Throw 575 * FileSystemException if the specified path does not exist or is not a file. 576 * 577 * @param path - the path 578 * @throws AssertionError - if the specified path is null 579 * @throws FileSystemException - if the specified path does not exist or is not a file 580 */ 581 private void verifyIsFile(String path) { 582 Assert.notNull(path, "path"); 583 FileSystemEntry entry = getRequiredEntry(path); 584 if (entry.isDirectory()) { 585 throw new FileSystemException(path, "filesystem.isFile"); 586 } 587 } 588 589 /** 590 * Throw a FileSystemException if the parent directory for the specified path does not exist. 591 * 592 * @param path - the path 593 * @throws FileSystemException - if the parent directory of the path does not exist 594 */ 595 private void verifyParentDirectoryExists(String path) throws FileSystemException { 596 if (!parentDirectoryExists(path)) { 597 throw new FileSystemException(getParent(path), "filesystem.parentDirectoryDoesNotExist"); 598 } 599 } 600 601 /** 602 * If the specified path has a parent, then verify that the parent exists 603 * 604 * @param path - the path 605 */ 606 private boolean parentDirectoryExists(String path) { 607 String parent = getParent(path); 608 if (parent != null) { 609 return pathExists(parent); 610 } 611 return true; 612 } 613 614 /** 615 * Return true if the specified path represents a directory that contains one or more files or subdirectories 616 * 617 * @param path - the path 618 * @return true if the path has child entries 619 */ 620 private boolean hasChildren(String path) { 621 if (!isDirectory(path)) { 622 return false; 623 } 624 String key = getFileSystemEntryKey(path); 625 Iterator iter = entries.keySet().iterator(); 626 while (iter.hasNext()) { 627 String p = (String) iter.next(); 628 if (p.startsWith(key) && !key.equals(p)) { 629 return true; 630 } 631 } 632 return false; 633 } 634 635 /** 636 * Return the List of files or subdirectory paths that are descendents of the specified path 637 * 638 * @param path - the path 639 * @return the List of the paths for the files and subdirectories that are children, grandchildren, etc. 640 */ 641 private List descendents(String path) { 642 if (isDirectory(path)) { 643 String normalizedPath = getFileSystemEntryKey(path); 644 String separator = (normalizedPath.endsWith(getSeparator())) ? "" : getSeparator(); 645 String normalizedDirPrefix = normalizedPath + separator; 646 List descendents = new ArrayList(); 647 Iterator iter = entries.keySet().iterator(); 648 while (iter.hasNext()) { 649 String p = (String) iter.next(); 650 if (p.startsWith(normalizedDirPrefix) && !normalizedPath.equals(p)) { 651 descendents.add(p); 652 } 653 } 654 return descendents; 655 } 656 return Collections.EMPTY_LIST; 657 } 658 659 /** 660 * Return the List of files or subdirectory paths that are children of the specified path 661 * 662 * @param path - the path 663 * @return the List of the paths for the files and subdirectories that are children 664 */ 665 private List children(String path) { 666 String lastComponent = getName(path); 667 boolean containsWildcards = PatternUtil.containsWildcards(lastComponent); 668 String dir = containsWildcards ? getParent(path) : path; 669 String pattern = containsWildcards ? PatternUtil.convertStringWithWildcardsToRegex(getName(path)) : null; 670 LOG.debug("path=" + path + " lastComponent=" + lastComponent + " containsWildcards=" + containsWildcards + " dir=" + dir + " pattern=" + pattern); 671 672 List descendents = descendents(dir); 673 List children = new ArrayList(); 674 String normalizedDir = normalize(dir); 675 Iterator iter = descendents.iterator(); 676 while (iter.hasNext()) { 677 String descendentPath = (String) iter.next(); 678 679 boolean patternEmpty = pattern == null || pattern.length() == 0; 680 if (normalizedDir.equals(getParent(descendentPath))) { 681 if (patternEmpty 682 || (!patternEmpty && getName(descendentPath).matches(pattern))) { 683 children.add(descendentPath); 684 } 685 } 686 } 687 return children; 688 } 689 690 private void removeEntry(String path) { 691 entries.remove(getFileSystemEntryKey(path)); 692 } 693 694}