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 normalized 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 normalize(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 * @param path - the path 358 * @return The name of the file or directory denoted by this abstract pathname, or the 359 * empty string if this pathname's name sequence is empty 360 */ 361 public String getName(String path) { 362 Assert.notNull(path, "path"); 363 String normalized = normalize(path); 364 int separatorIndex = normalized.lastIndexOf(this.getSeparator()); 365 return (separatorIndex == -1) ? normalized : normalized.substring(separatorIndex + 1); 366 } 367 368 /** 369 * Returns the FileSystemEntry object representing the file system entry at the specified path, or null 370 * if the path does not specify an existing file or directory within this file system. 371 * 372 * @param path - the path of the file or directory within this file system 373 * @return the FileSystemEntry containing the information for the file or directory, or else null 374 * @see FileSystem#getEntry(String) 375 */ 376 public FileSystemEntry getEntry(String path) { 377 return (FileSystemEntry) entries.get(getFileSystemEntryKey(path)); 378 } 379 380 //------------------------------------------------------------------------- 381 // Abstract Methods 382 //------------------------------------------------------------------------- 383 384 /** 385 * @param path - the path 386 * @return true if the specified dir/file path name is valid according to the current filesystem. 387 */ 388 protected abstract boolean isValidName(String path); 389 390 /** 391 * @return the file system-specific file separator as a char 392 */ 393 protected abstract char getSeparatorChar(); 394 395 /** 396 * @param pathComponent - the component (piece) of the path to check 397 * @return true if the specified path component is a root for this filesystem 398 */ 399 protected abstract boolean isRoot(String pathComponent); 400 401 /** 402 * Return true if the specified char is a separator character for this filesystem 403 * 404 * @param c - the character to test 405 * @return true if the specified char is a separator character 406 */ 407 protected abstract boolean isSeparator(char c); 408 409 //------------------------------------------------------------------------- 410 // Internal Helper Methods 411 //------------------------------------------------------------------------- 412 413 /** 414 * @return the file system-specific file separator as a String 415 */ 416 protected String getSeparator() { 417 return Character.toString(getSeparatorChar()); 418 } 419 420 /** 421 * Return the normalized and unique key used to access the file system entry 422 * 423 * @param path - the path 424 * @return the corresponding normalized key 425 */ 426 protected String getFileSystemEntryKey(String path) { 427 return normalize(path); 428 } 429 430 /** 431 * Return the standard, normalized form of the path. 432 * 433 * @param path - the path 434 * @return the path in a standard, unique, canonical form 435 * @throws AssertionError - if path is null 436 */ 437 protected String normalize(String path) { 438 return componentsToPath(normalizedComponents(path)); 439 } 440 441 /** 442 * Throw an InvalidFilenameException if the specified path is not valid. 443 * 444 * @param path - the path 445 */ 446 protected void checkForInvalidFilename(String path) { 447 if (!isValidName(path)) { 448 throw new InvalidFilenameException(path); 449 } 450 } 451 452 /** 453 * Rename the file system entry to the specified path name 454 * 455 * @param entry - the file system entry 456 * @param toPath - the TO path (normalized) 457 */ 458 protected void renamePath(FileSystemEntry entry, String toPath) { 459 String normalizedFrom = normalize(entry.getPath()); 460 String normalizedTo = normalize(toPath); 461 LOG.info("renaming from [" + normalizedFrom + "] to [" + normalizedTo + "]"); 462 FileSystemEntry newEntry = entry.cloneWithNewPath(normalizedTo); 463 add(newEntry); 464 // Do this at the end, in case the addEntry() failed 465 removeEntry(normalizedFrom); 466 } 467 468 /** 469 * Return the FileSystemEntry for the specified path. Throw FileSystemException if the 470 * specified path does not exist. 471 * 472 * @param path - the path 473 * @return the FileSystemEntry 474 * @throws FileSystemException - if the specified path does not exist 475 */ 476 protected FileSystemEntry getRequiredEntry(String path) { 477 FileSystemEntry entry = getEntry(path); 478 if (entry == null) { 479 LOG.error("Path does not exist: " + path); 480 throw new FileSystemException(normalize(path), "filesystem.doesNotExist"); 481 } 482 return entry; 483 } 484 485 /** 486 * Return the components of the specified path as a List. The components are normalized, and 487 * the returned List does not include path separator characters. 488 * 489 * @param path - the path 490 * @return the List of normalized components 491 */ 492 protected List normalizedComponents(String path) { 493 Assert.notNull(path, "path"); 494 char otherSeparator = this.getSeparatorChar() == '/' ? '\\' : '/'; 495 String p = path.replace(otherSeparator, this.getSeparatorChar()); 496 497 // TODO better way to do this 498 if (p.equals(this.getSeparator())) { 499 return Collections.singletonList(""); 500 } 501 List result = new ArrayList(); 502 if (p.length() > 0) { 503 String[] parts = p.split("\\" + this.getSeparator()); 504 for (int i = 0; i < parts.length; i++) { 505 String part = parts[i]; 506 if (part.equals("..")) { 507 result.remove(result.size() - 1); 508 } else if (!part.equals(".")) { 509 result.add(part); 510 } 511 } 512 } 513 return result; 514 } 515 516 /** 517 * Build a path from the specified list of path components 518 * 519 * @param components - the list of path components 520 * @return the resulting path 521 */ 522 protected String componentsToPath(List components) { 523 if (components.size() == 1) { 524 String first = (String) components.get(0); 525 if (first.length() == 0 || isRoot(first)) { 526 return first + this.getSeparator(); 527 } 528 } 529 return StringUtil.join(components, this.getSeparator()); 530 } 531 532 /** 533 * Return true if the specified path designates an absolute file path. 534 * 535 * @param path - the path 536 * @return true if path is absolute, false otherwise 537 * @throws AssertionError - if path is null 538 */ 539 public boolean isAbsolute(String path) { 540 return isValidName(path); 541 } 542 543 /** 544 * Return true if the specified path exists 545 * 546 * @param path - the path 547 * @return true if the path exists 548 */ 549 private boolean pathExists(String path) { 550 return getEntry(path) != null; 551 } 552 553 /** 554 * If the specified path has a parent, then verify that the parent exists 555 * 556 * @param path - the path 557 * @return true if the parent of the specified path exists 558 */ 559 private boolean parentDirectoryExists(String path) { 560 String parent = getParent(path); 561 return parent == null || pathExists(parent); 562 } 563 564 /** 565 * Return true if the specified path represents a directory that contains one or more files or subdirectories 566 * 567 * @param path - the path 568 * @return true if the path has child entries 569 */ 570 private boolean hasChildren(String path) { 571 if (!isDirectory(path)) { 572 return false; 573 } 574 String key = getFileSystemEntryKey(path); 575 Iterator iter = entries.keySet().iterator(); 576 while (iter.hasNext()) { 577 String p = (String) iter.next(); 578 if (p.startsWith(key) && !key.equals(p)) { 579 return true; 580 } 581 } 582 return false; 583 } 584 585 /** 586 * Return the List of files or subdirectory paths that are descendents of the specified path 587 * 588 * @param path - the path 589 * @return the List of the paths for the files and subdirectories that are children, grandchildren, etc. 590 */ 591 private List descendents(String path) { 592 if (isDirectory(path)) { 593 String normalizedPath = getFileSystemEntryKey(path); 594 String separator = (normalizedPath.endsWith(getSeparator())) ? "" : getSeparator(); 595 String normalizedDirPrefix = normalizedPath + separator; 596 List descendents = new ArrayList(); 597 Iterator iter = entries.entrySet().iterator(); 598 while (iter.hasNext()) { 599 Map.Entry mapEntry = (Map.Entry) iter.next(); 600 String p = (String) mapEntry.getKey(); 601 if (p.startsWith(normalizedDirPrefix) && !normalizedPath.equals(p)) { 602 FileSystemEntry fileSystemEntry = (FileSystemEntry) mapEntry.getValue(); 603 descendents.add(fileSystemEntry.getPath()); 604 } 605 } 606 return descendents; 607 } 608 return Collections.EMPTY_LIST; 609 } 610 611 /** 612 * Return the List of files or subdirectory paths that are children of the specified path 613 * 614 * @param path - the path 615 * @return the List of the paths for the files and subdirectories that are children 616 */ 617 private List children(String path) { 618 String lastComponent = getName(path); 619 boolean containsWildcards = PatternUtil.containsWildcards(lastComponent); 620 String dir = containsWildcards ? getParent(path) : path; 621 String pattern = containsWildcards ? PatternUtil.convertStringWithWildcardsToRegex(getName(path)) : null; 622 LOG.debug("path=" + path + " lastComponent=" + lastComponent + " containsWildcards=" + containsWildcards + " dir=" + dir + " pattern=" + pattern); 623 624 List descendents = descendents(dir); 625 List children = new ArrayList(); 626 String normalizedDir = normalize(dir); 627 Iterator iter = descendents.iterator(); 628 while (iter.hasNext()) { 629 String descendentPath = (String) iter.next(); 630 631 boolean patternEmpty = pattern == null || pattern.length() == 0; 632 if (normalizedDir.equals(getParent(descendentPath)) && 633 (patternEmpty || (getName(descendentPath).matches(pattern)))) { 634 children.add(descendentPath); 635 } 636 } 637 return children; 638 } 639 640 private void removeEntry(String path) { 641 entries.remove(getFileSystemEntryKey(path)); 642 } 643 644}