001/* An actor that produces an array that lists the contents of a directory.
002
003   @Copyright (c) 2003-2018 The Regents of the University of California.
004   All rights reserved.
005
006   Permission is hereby granted, without written agreement and without
007   license or royalty fees, to use, copy, modify, and distribute this
008   software and its documentation for any purpose, provided that the
009   above copyright notice and the following two paragraphs appear in all
010   copies of this software.
011
012   IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
013   FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
014   ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
015   THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
016   SUCH DAMAGE.
017
018   THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
019   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
020   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
021   PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
022   CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
023   ENHANCEMENTS, OR MODIFICATIONS.
024
025   PT_COPYRIGHT_VERSION 2
026   COPYRIGHTENDKEY
027 */
028package ptolemy.util;
029
030import java.io.File;
031import java.io.FilenameFilter;
032import java.util.Collections;
033import java.util.Comparator;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039///////////////////////////////////////////////////////////////////
040//// RecursiveFileFilter
041
042/**
043   A file name filter that can recursively list files in a directory,
044   including those in subdirectories. When a file name referring to a
045   directory is found, this filter lists the files within that directory again
046   with a new filter in this class.
047
048   @author Thomas Huining Feng, Christopher Brooks
049   @version $Id$
050   @since Ptolemy II 10.0
051   @Pt.ProposedRating Yellow (tfeng)
052   @Pt.AcceptedRating Red (tfeng)
053 */
054public class RecursiveFileFilter implements FilenameFilter {
055
056    /** Construct a recursive file filter.
057     *
058     *  @param recursive Whether the filter should recursively list
059     *   subdirectories.
060     *  @param includeFiles Whether files should be included.
061     *  @param includeDirectories Whether directories should be included.
062     */
063    public RecursiveFileFilter(boolean recursive, boolean includeFiles,
064            boolean includeDirectories) {
065        this(recursive, includeFiles, includeDirectories, false, false, null);
066    }
067
068    /** Construct a recursive file filter.  This method has four
069     *  parameters to control whether files and directories are
070     *  included.  Not all combinations make sense, but are required
071     *  because this code was refactored from two classes that had
072     *  similar functionality.
073     *  @param recursive Whether the filter should recursively list
074     *   subdirectories.
075     *  @param includeFiles Whether files should be included.
076     *  @param includeDirectories Whether directories should be included.
077     *  @param filesOnly Whether only files should be included
078     *  @param directoriesOnly Whether only directories should be included.
079     *  @param fileFilter The filter (with ? and * as wildcards) to filter
080     *   the accepted file names.
081     */
082    public RecursiveFileFilter(boolean recursive, boolean includeFiles,
083            boolean includeDirectories, boolean filesOnly,
084            boolean directoriesOnly, String fileFilter) {
085        this(recursive, includeFiles, includeDirectories, false, false, null,
086                false);
087    }
088
089    /** Construct a recursive file filter.  This method has four
090     *  parameters to control whether files and directories are
091     *  included.  Not all combinations make sense, but are required
092     *  because this code was refactored from two classes that had
093     *  similar functionality.
094     *  @param recursive Whether the filter should recursively list
095     *   subdirectories.
096     *  @param includeFiles Whether files should be included.
097     *  @param includeDirectories Whether directories should be included.
098     *  @param filesOnly Whether only files should be included
099     *  @param directoriesOnly Whether only directories should be included.
100     *  @param fileFilter The filter (with ? and * as wildcards) to filter
101     *   the accepted file names.
102     *  @param escape True if a string with ? and * as wildcards is to
103     *  be converted into a Java regular expression.  The DirectoryListing
104     *  actor calls this with a false value.
105     */
106    public RecursiveFileFilter(boolean recursive, boolean includeFiles,
107            boolean includeDirectories, boolean filesOnly,
108            boolean directoriesOnly, String fileFilter, boolean escape) {
109        _recursive = recursive;
110        _includeFiles = includeFiles;
111        _includeDirectories = includeDirectories;
112        _filesOnly = filesOnly;
113        _directoriesOnly = directoriesOnly;
114        if (fileFilter != null && !fileFilter.equals("")) {
115            if (escape) {
116                // gt style, but gt calls listFiles()
117                _pattern = Pattern.compile(_escape(fileFilter));
118            } else {
119                // DirectoryListin style, but DirectoryListing calls listFiles()
120                _pattern = Pattern.compile(fileFilter);
121            }
122        } else {
123            // Empty pattern for glob, which is .*
124            _pattern = Pattern.compile(".*");
125        }
126
127    }
128
129    /** Return whether the file or directory name in the given directory is
130     *  accepted.
131     *
132     *  @param dir The directory.  If directory is null, then it is
133     *  likely that this method is being called to accept a URL and no
134     *  File object is instantiated.  If directory is not null, then a
135     *  File object is instantiated.
136     *  @param name The file or directory name within the given directory.
137     *  @return Whether the name is accepted.
138     */
139    @Override
140    public boolean accept(File dir, String name) {
141        File file = null;
142        boolean isDirectory = false;
143        boolean isFile = false;
144        if (dir != null) {
145            file = new File(dir, name);
146            isDirectory = file.isDirectory();
147            isFile = file.isFile();
148        } else {
149            // Could be a URL.
150            file = new File(name);
151            if (name.endsWith("/")) {
152                isDirectory = true;
153            } else {
154                isFile = true;
155            }
156        }
157        if (_match(isDirectory, isFile, name, file)) {
158            return true;
159        }
160
161        // file will be null if we are trying to accept a URL.
162        if (file != null && _recursive && isDirectory) {
163            file.list(this);
164        }
165        return false;
166    }
167
168    /** Return the list of all files and directories after the filtering.
169     *  This must be called after all the directories are traversed.
170     *
171     *  @return The list of files and directories.
172     */
173    public List<File> getFiles() {
174        return _files;
175    }
176
177    /** List all the files and directories within the given directory.
178     *  This method has four parameters to control whether
179     *  files and directories are included.  Not all combinations make sense.
180     *  @param directory The directory.
181     *  @param recursive Whether the filter should recursively list
182     *   subdirectories.
183     *  @param includeFiles Whether files should be included.
184     *  @param includeDirectories Whether directories should be included.
185     *  @param fileFilter The filter (with ? and * as wildcards) to filter
186     *   the accepted file names.
187     *  @return The array of all the files and directories found.
188     */
189    public static File[] listFiles(File directory, boolean recursive,
190            boolean includeFiles, boolean includeDirectories,
191            String fileFilter) {
192        // gt uses this method.
193        return RecursiveFileFilter.listFiles(directory, recursive, includeFiles,
194                includeDirectories, false, false, fileFilter, true);
195    }
196
197    /** List all the files and directories within the given directory.
198     *  This method has four parameters to control whether
199     *  files and directories are included.  Not all combinations make sense.
200     *  @param directory The directory.
201     *  @param recursive Whether the filter should recursively list
202     *   subdirectories.
203     *  @param includeFiles Whether files should be included.
204     *  @param includeDirectories Whether directories should be included.
205     *  @param fileFilter The filter (with ? and * as wildcards) to filter
206     *   the accepted file names.
207     *  @param escape True if a string with ? and * as wildcards is to
208     *  be converted into a Java regular expression.  The DirectoryListing
209     *  actor calls this with a false value.
210     *  @return The array of all the files and directories found.
211     */
212    public static File[] listFiles(File directory, boolean recursive,
213            boolean includeFiles, boolean includeDirectories, String fileFilter,
214            boolean escape) {
215        // DirectoryListing uses this method.
216        return RecursiveFileFilter.listFiles(directory, recursive, includeFiles,
217                includeDirectories, false, false, fileFilter, escape);
218    }
219
220    /** List all the files and directories within the given directory.
221     *  This method has four parameters to control whether
222     *  files and directories are included.  Not all combinations make sense.
223     *  @param directory The directory.
224     *  @param recursive Whether the filter should recursively list
225     *   subdirectories.
226     *  @param includeFiles Whether files should be included.
227     *  @param includeDirectories Whether directories should be included.
228     *  @param filesOnly Whether only files should be included
229     *  @param directoriesOnly Whether only directories should be included.
230     *  @param fileFilter The filter (with ? and * as wildcards) to filter
231     *   the accepted file names.
232     *  @return The array of all the files and directories found.
233     */
234    public static File[] listFiles(File directory, boolean recursive,
235            boolean includeFiles, boolean includeDirectories, boolean filesOnly,
236            boolean directoriesOnly, String fileFilter) {
237        return RecursiveFileFilter.listFiles(directory, recursive, includeFiles,
238                includeDirectories, filesOnly, directoriesOnly, fileFilter,
239                true);
240
241    }
242
243    /** List all the files and directories within the given directory.
244     *  This method has four parameters to control whether
245     *  files and directories are included.  Not all combinations make sense.
246     *  @param directory The directory.
247     *  @param recursive Whether the filter should recursively list
248     *   subdirectories.
249     *  @param includeFiles Whether files should be included.
250     *  @param includeDirectories Whether directories should be included.
251     *  @param filesOnly Whether only files should be included
252     *  @param directoriesOnly Whether only directories should be included.
253     *  @param fileFilter The filter (with ? and * as wildcards) to filter
254     *   the accepted file names.
255     *  @param escape True if a string with ? and * as wildcards is to
256     *  be converted into a Java regular expression.  The DirectoryListing
257     *  actor calls this with a false value.
258     *  @return The array of all the files and directories found.
259     */
260    public static File[] listFiles(File directory, boolean recursive,
261            boolean includeFiles, boolean includeDirectories, boolean filesOnly,
262            boolean directoriesOnly, String fileFilter, boolean escape) {
263        RecursiveFileFilter filter = new RecursiveFileFilter(recursive,
264                includeFiles, includeDirectories, filesOnly, directoriesOnly,
265                fileFilter, escape);
266        directory.list(filter);
267        List<File> files = filter.getFiles();
268        Collections.sort(files, new FileComparator());
269        return files.toArray(new File[files.size()]);
270    }
271
272    /** Convert a string with ? and * as wildcards into a Java regular
273     *  expression.
274     *
275     *  @param string The string with file name wildcards.
276     *  @return A regular expression.
277     */
278    private String _escape(String string) {
279        String escaped = _ESCAPER.matcher(string).replaceAll("\\\\$1");
280        return escaped.replaceAll("\\\\\\*", ".*").replaceAll("\\\\\\?", ".?");
281    }
282
283    /** Return true if there is a match.
284     *  @param isDirectory True if the name is a directory.
285     *  @param isFile True if the name is a file
286     *  @param name The name to check.
287     *  @param file The file to be added if there is a match.
288     */
289    private boolean _match(boolean isDirectory, boolean isFile, String name,
290            File file) {
291        if (((!_directoriesOnly && !_filesOnly) && ((isFile && _includeFiles)
292                || (isDirectory && _includeDirectories)))
293                || ((_filesOnly && isFile) || (_directoriesOnly && isDirectory)
294                        || (!_directoriesOnly && !_filesOnly))) {
295            // ptolemy/domains/sdf/test/auto/filePortParameter.xml wants match.matches() here.
296            // ptolemy/actor/lib/test/auto/ExecRunDemos.xml wants match.find() here
297
298            // Avoid a NPE if the pattern is empty.
299            // See ptolemy/actor/lib/io/test/auto/DirectoryListingEmptyPattern.xml
300            // and https://projects.ecoinformatics.org/ecoinfo/issues/6233
301            if (_pattern != null) {
302                Matcher match = _pattern.matcher(name);
303                if (match.matches() || match.find()) {
304                    _files.add(file);
305                    return true;
306                }
307            } else {
308                return true;
309            }
310        }
311        return false;
312    }
313
314    /** The pattern for the wildcard conversion.
315     */
316    private final Pattern _ESCAPER = Pattern.compile("([^a-zA-z0-9])");
317
318    /** Whether only directories should be included.
319     */
320    private boolean _directoriesOnly;
321
322    /** The list the recently found files and directories.
323     */
324    private List<File> _files = new LinkedList<File>();
325
326    /** Whether only files should be included.
327     */
328    private boolean _filesOnly;
329
330    /** Whether directories should be included.
331     */
332    private boolean _includeDirectories;
333
334    /** Whether files should be included.
335     */
336    private boolean _includeFiles;
337
338    /** The pattern for the filter.
339     */
340    private Pattern _pattern;
341
342    /** Whether the filter should recursively list subdirectories.
343     */
344    private boolean _recursive;
345
346    ///////////////////////////////////////////////////////////////////
347    //// FileComparator
348
349    /**
350       A comparator to sort file names.
351
352       @author Thomas Huining Feng
353       @version $Id$
354       @since Ptolemy II 10.0
355       @Pt.ProposedRating Yellow (tfeng)
356       @Pt.AcceptedRating Red (tfeng)
357     */
358    private static class FileComparator implements Comparator<File> {
359
360        /** Compare two files with their names.
361         *
362         *  @param file1 The first file.
363         *  @param file2 The second file.
364         *  @return -1, 0 or 1 if file1 is less than, equal to or greater
365         *   than file2.
366         */
367        @Override
368        public int compare(File file1, File file2) {
369            return file1.getAbsolutePath().compareTo(file2.getAbsolutePath());
370        }
371    }
372}