blob: 9db6f937d07d1e7ca8b386000e0fcacc0c09df90 [file] [log] [blame]
chrismair00dc7bd2014-05-11 21:21:28 +00001/*
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}