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