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