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