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 if (exists(toPath)) { 262 throw new FileSystemException(toPath, "filesystem.alreadyExists"); 263 } 264 265 String normalizedFromPath = normalize(fromPath); 266 String normalizedToPath = normalize(toPath); 267 268 if (!entry.isDirectory()) { 269 renamePath(entry, normalizedToPath); 270 return; 271 } 272 273 if (normalizedToPath.startsWith(normalizedFromPath + this.getSeparator())) { 274 throw new FileSystemException(toPath, "filesystem.renameFailed"); 275 } 276 277 // Create the TO directory entry first so that the destination path exists when you 278 // move the children. Remove the FROM path after all children have been moved 279 add(new DirectoryEntry(normalizedToPath)); 280 281 List children = descendents(fromPath); 282 Iterator iter = children.iterator(); 283 while (iter.hasNext()) { 284 String childPath = (String) iter.next(); 285 FileSystemEntry child = getRequiredEntry(childPath); 286 String normalizedChildPath = normalize(child.getPath()); 287 Assert.isTrue(normalizedChildPath.startsWith(normalizedFromPath), "Starts with FROM path"); 288 String childToPath = normalizedToPath + normalizedChildPath.substring(normalizedFromPath.length()); 289 renamePath(child, childToPath); 290 } 291 Assert.isTrue(children(normalizedFromPath).isEmpty(), "Must have no children: " + normalizedFromPath); 292 removeEntry(normalizedFromPath); 293 } 294 295 /** 296 * @see java.lang.Object#toString() 297 */ 298 public String toString() { 299 return this.getClass().getName() + entries; 300 } 301 302 /** 303 * Return the formatted directory listing entry for the file represented by the specified FileSystemEntry 304 * 305 * @param fileSystemEntry - the FileSystemEntry representing the file or directory entry to be formatted 306 * @return the the formatted directory listing entry 307 */ 308 public String formatDirectoryListing(FileSystemEntry fileSystemEntry) { 309 Assert.notNull(directoryListingFormatter, "directoryListingFormatter"); 310 Assert.notNull(fileSystemEntry, "fileSystemEntry"); 311 return directoryListingFormatter.format(fileSystemEntry); 312 } 313 314 /** 315 * Build a path from the two path components. Concatenate path1 and path2. Insert the path 316 * separator character in between if necessary (i.e., if both are non-empty and path1 does not already 317 * end with a separator character AND path2 does not begin with one). 318 * 319 * @param path1 - the first path component may be null or empty 320 * @param path2 - the second path component may be null or empty 321 * @return the normalized path resulting from concatenating path1 to path2 322 */ 323 public String path(String path1, String path2) { 324 StringBuffer buf = new StringBuffer(); 325 if (path1 != null && path1.length() > 0) { 326 buf.append(path1); 327 } 328 if (path2 != null && path2.length() > 0) { 329 if ((path1 != null && path1.length() > 0) 330 && (!isSeparator(path1.charAt(path1.length() - 1))) 331 && (!isSeparator(path2.charAt(0)))) { 332 buf.append(this.getSeparator()); 333 } 334 buf.append(path2); 335 } 336 return normalize(buf.toString()); 337 } 338 339 /** 340 * Return the parent path of the specified path. If <code>path</code> specifies a filename, 341 * then this method returns the path of the directory containing that file. If <code>path</code> 342 * specifies a directory, the this method returns its parent directory. If <code>path</code> is 343 * empty or does not have a parent component, then return an empty string. 344 * <p/> 345 * All path separators in the returned path are converted to the system-dependent separator character. 346 * 347 * @param path - the path 348 * @return the parent of the specified path, or null if <code>path</code> has no parent 349 * @throws AssertionError - if path is null 350 */ 351 public String getParent(String path) { 352 List parts = normalizedComponents(path); 353 if (parts.size() < 2) { 354 return null; 355 } 356 parts.remove(parts.size() - 1); 357 return componentsToPath(parts); 358 } 359 360 /** 361 * Returns the name of the file or directory denoted by this abstract 362 * pathname. This is just the last name in the pathname's name 363 * sequence. If the pathname's name sequence is empty, then the empty string is returned. 364 * 365 * @param path - the path 366 * @return The name of the file or directory denoted by this abstract pathname, or the 367 * empty string if this pathname's name sequence is empty 368 */ 369 public String getName(String path) { 370 Assert.notNull(path, "path"); 371 String normalized = normalize(path); 372 int separatorIndex = normalized.lastIndexOf(this.getSeparator()); 373 return (separatorIndex == -1) ? normalized : normalized.substring(separatorIndex + 1); 374 } 375 376 /** 377 * Returns the FileSystemEntry object representing the file system entry at the specified path, or null 378 * if the path does not specify an existing file or directory within this file system. 379 * 380 * @param path - the path of the file or directory within this file system 381 * @return the FileSystemEntry containing the information for the file or directory, or else null 382 * @see FileSystem#getEntry(String) 383 */ 384 public FileSystemEntry getEntry(String path) { 385 return (FileSystemEntry) entries.get(getFileSystemEntryKey(path)); 386 } 387 388 //------------------------------------------------------------------------- 389 // Abstract Methods 390 //------------------------------------------------------------------------- 391 392 /** 393 * @param path - the path 394 * @return true if the specified dir/file path name is valid according to the current filesystem. 395 */ 396 protected abstract boolean isValidName(String path); 397 398 /** 399 * @return the file system-specific file separator as a char 400 */ 401 protected abstract char getSeparatorChar(); 402 403 /** 404 * @param pathComponent - the component (piece) of the path to check 405 * @return true if the specified path component is a root for this filesystem 406 */ 407 protected abstract boolean isRoot(String pathComponent); 408 409 /** 410 * Return true if the specified char is a separator character for this filesystem 411 * 412 * @param c - the character to test 413 * @return true if the specified char is a separator character 414 */ 415 protected abstract boolean isSeparator(char c); 416 417 //------------------------------------------------------------------------- 418 // Internal Helper Methods 419 //------------------------------------------------------------------------- 420 421 /** 422 * @return the file system-specific file separator as a String 423 */ 424 protected String getSeparator() { 425 return Character.toString(getSeparatorChar()); 426 } 427 428 /** 429 * Return the normalized and unique key used to access the file system entry 430 * 431 * @param path - the path 432 * @return the corresponding normalized key 433 */ 434 protected String getFileSystemEntryKey(String path) { 435 return normalize(path); 436 } 437 438 /** 439 * Return the standard, normalized form of the path. 440 * 441 * @param path - the path 442 * @return the path in a standard, unique, canonical form 443 * @throws AssertionError - if path is null 444 */ 445 protected String normalize(String path) { 446 return componentsToPath(normalizedComponents(path)); 447 } 448 449 /** 450 * Throw an InvalidFilenameException if the specified path is not valid. 451 * 452 * @param path - the path 453 */ 454 protected void checkForInvalidFilename(String path) { 455 if (!isValidName(path)) { 456 throw new InvalidFilenameException(path); 457 } 458 } 459 460 /** 461 * Rename the file system entry to the specified path name 462 * 463 * @param entry - the file system entry 464 * @param toPath - the TO path (normalized) 465 */ 466 protected void renamePath(FileSystemEntry entry, String toPath) { 467 String normalizedFrom = normalize(entry.getPath()); 468 String normalizedTo = normalize(toPath); 469 LOG.info("renaming from [" + normalizedFrom + "] to [" + normalizedTo + "]"); 470 FileSystemEntry newEntry = entry.cloneWithNewPath(normalizedTo); 471 add(newEntry); 472 // Do this at the end, in case the addEntry() failed 473 removeEntry(normalizedFrom); 474 } 475 476 /** 477 * Return the FileSystemEntry for the specified path. Throw FileSystemException if the 478 * specified path does not exist. 479 * 480 * @param path - the path 481 * @return the FileSystemEntry 482 * @throws FileSystemException - if the specified path does not exist 483 */ 484 protected FileSystemEntry getRequiredEntry(String path) { 485 FileSystemEntry entry = getEntry(path); 486 if (entry == null) { 487 LOG.error("Path does not exist: " + path); 488 throw new FileSystemException(normalize(path), "filesystem.doesNotExist"); 489 } 490 return entry; 491 } 492 493 /** 494 * Return the components of the specified path as a List. The components are normalized, and 495 * the returned List does not include path separator characters. 496 * 497 * @param path - the path 498 * @return the List of normalized components 499 */ 500 protected List normalizedComponents(String path) { 501 Assert.notNull(path, "path"); 502 char otherSeparator = this.getSeparatorChar() == '/' ? '\\' : '/'; 503 String p = path.replace(otherSeparator, this.getSeparatorChar()); 504 505 // TODO better way to do this 506 if (p.equals(this.getSeparator())) { 507 return Collections.singletonList(""); 508 } 509 List result = new ArrayList(); 510 if (p.length() > 0) { 511 String[] parts = p.split("\\" + this.getSeparator()); 512 for (int i = 0; i < parts.length; i++) { 513 String part = parts[i]; 514 if (part.equals("..")) { 515 result.remove(result.size() - 1); 516 } else if (!part.equals(".")) { 517 result.add(part); 518 } 519 } 520 } 521 return result; 522 } 523 524 /** 525 * Build a path from the specified list of path components 526 * 527 * @param components - the list of path components 528 * @return the resulting path 529 */ 530 protected String componentsToPath(List components) { 531 if (components.size() == 1) { 532 String first = (String) components.get(0); 533 if (first.length() == 0 || isRoot(first)) { 534 return first + this.getSeparator(); 535 } 536 } 537 return StringUtil.join(components, this.getSeparator()); 538 } 539 540 /** 541 * Return true if the specified path designates an absolute file path. 542 * 543 * @param path - the path 544 * @return true if path is absolute, false otherwise 545 * @throws AssertionError - if path is null 546 */ 547 public boolean isAbsolute(String path) { 548 return isValidName(path); 549 } 550 551 /** 552 * Return true if the specified path exists 553 * 554 * @param path - the path 555 * @return true if the path exists 556 */ 557 private boolean pathExists(String path) { 558 return getEntry(path) != null; 559 } 560 561 /** 562 * If the specified path has a parent, then verify that the parent exists 563 * 564 * @param path - the path 565 * @return true if the parent of the specified path exists 566 */ 567 private boolean parentDirectoryExists(String path) { 568 String parent = getParent(path); 569 return parent == null || pathExists(parent); 570 } 571 572 /** 573 * Return true if the specified path represents a directory that contains one or more files or subdirectories 574 * 575 * @param path - the path 576 * @return true if the path has child entries 577 */ 578 private boolean hasChildren(String path) { 579 if (!isDirectory(path)) { 580 return false; 581 } 582 String key = getFileSystemEntryKey(path); 583 Iterator iter = entries.keySet().iterator(); 584 while (iter.hasNext()) { 585 String p = (String) iter.next(); 586 if (p.startsWith(key) && !key.equals(p)) { 587 return true; 588 } 589 } 590 return false; 591 } 592 593 /** 594 * Return the List of files or subdirectory paths that are descendents of the specified path 595 * 596 * @param path - the path 597 * @return the List of the paths for the files and subdirectories that are children, grandchildren, etc. 598 */ 599 private List descendents(String path) { 600 if (isDirectory(path)) { 601 String normalizedPath = getFileSystemEntryKey(path); 602 String separator = (normalizedPath.endsWith(getSeparator())) ? "" : getSeparator(); 603 String normalizedDirPrefix = normalizedPath + separator; 604 List descendents = new ArrayList(); 605 Iterator iter = entries.entrySet().iterator(); 606 while (iter.hasNext()) { 607 Map.Entry mapEntry = (Map.Entry) iter.next(); 608 String p = (String) mapEntry.getKey(); 609 if (p.startsWith(normalizedDirPrefix) && !normalizedPath.equals(p)) { 610 FileSystemEntry fileSystemEntry = (FileSystemEntry) mapEntry.getValue(); 611 descendents.add(fileSystemEntry.getPath()); 612 } 613 } 614 return descendents; 615 } 616 return Collections.EMPTY_LIST; 617 } 618 619 /** 620 * Return the List of files or subdirectory paths that are children of the specified path 621 * 622 * @param path - the path 623 * @return the List of the paths for the files and subdirectories that are children 624 */ 625 private List children(String path) { 626 String lastComponent = getName(path); 627 boolean containsWildcards = PatternUtil.containsWildcards(lastComponent); 628 String dir = containsWildcards ? getParent(path) : path; 629 String pattern = containsWildcards ? PatternUtil.convertStringWithWildcardsToRegex(getName(path)) : null; 630 LOG.debug("path=" + path + " lastComponent=" + lastComponent + " containsWildcards=" + containsWildcards + " dir=" + dir + " pattern=" + pattern); 631 632 List descendents = descendents(dir); 633 List children = new ArrayList(); 634 String normalizedDir = normalize(dir); 635 Iterator iter = descendents.iterator(); 636 while (iter.hasNext()) { 637 String descendentPath = (String) iter.next(); 638 639 boolean patternEmpty = pattern == null || pattern.length() == 0; 640 if (normalizedDir.equals(getParent(descendentPath)) && 641 (patternEmpty || (getName(descendentPath).matches(pattern)))) { 642 children.add(descendentPath); 643 } 644 } 645 return children; 646 } 647 648 private void removeEntry(String path) { 649 entries.remove(getFileSystemEntryKey(path)); 650 } 651 652}