001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.util;
030
031import static java.nio.file.FileVisitResult.CONTINUE;
032
033import java.io.IOException;
034
035import java.nio.file.FileSystem;
036import java.nio.file.FileSystems;
037import java.nio.file.FileVisitOption;
038import java.nio.file.FileVisitResult;
039import java.nio.file.Files;
040import java.nio.file.Path;
041import java.nio.file.PathMatcher;
042import java.nio.file.Paths;
043import java.nio.file.SimpleFileVisitor;
044import java.nio.file.attribute.BasicFileAttributes;
045
046import java.util.ArrayList;
047import java.util.Collections;
048import java.util.EnumSet;
049import java.util.List;
050
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054/**
055 * Allows for easy searching of files matching {@literal "glob"} patterns
056 * (e.g. {@code *.py}) in a given directories and its subdirectories.
057 *
058 * <p>Note: the {@code findFiles(...)} methods will block until the search
059 * finishes! If this is a concern, for the time being, please consider using
060 * {@link #findFiles(String, String, int)} with a reasonable depth value.</p>
061 */
062public class FileFinder {
063
064    // TODO(jon): make this async somehow
065
066    // adapted from
067    // https://docs.oracle.com/javase/tutorial/essential/io/find.html
068
069    /** Logging object. */
070    private static final Logger logger =
071            LoggerFactory.getLogger(FileFinder.class);
072
073    /**
074     * Internal class used by the {@code findFiles(...)} methods to actually
075     * {@literal "walk"} the directory tree.
076     */
077    private static class Finder extends SimpleFileVisitor<Path> {
078
079        /** Pattern matcher. */
080        private final PathMatcher matcher;
081
082        /** {@code String} representations of matching {@link Path Paths}. */
083        private final List<String> matches;
084
085        /**
086         * Creates a new file searcher.
087         *
088         * <p>Please see {@link FileSystem#getPathMatcher(String)} for more
089         * details concerning patterns.</p>
090         *
091         * @param pattern Pattern to match against.
092         */
093        Finder(String pattern) {
094            matches = new ArrayList<>();
095            matcher =
096                FileSystems.getDefault().getPathMatcher("glob:" + pattern);
097        }
098
099        /**
100         * Compare the given file or directory against the glob pattern.
101         *
102         * <p>If {@code file} matches, it is added to {@link #matches}.</p>
103         *
104         * @param file File (or directory) to compare against the glob pattern.
105         *
106         * @see #results()
107         */
108        void find(Path file) {
109            Path name = file.getFileName();
110            if ((name != null) && matcher.matches(name)) {
111                matches.add(file.toString());
112            }
113        }
114
115        /**
116         * Prints the total number of matches to standard out.
117         */
118        void done() {
119            // TODO(jon): not the most useful method...
120            System.out.println("Matched: " + matches.size());
121        }
122
123        /**
124         * Returns the matching paths as strings.
125         *
126         * @return {@code List} of the matching paths as {@code String} values.
127         */
128        List<String> results() {
129            List<String> results = Collections.emptyList();
130            if (!matches.isEmpty()) {
131                results = new ArrayList<>(matches);
132            }
133            return results;
134        }
135
136        /**
137         * Invokes pattern matching method on the given file.
138         *
139         * @param file File in question.
140         * @param attrs Attributes of {@code dir}. Not currently used.
141         *
142         * @return Always returns {@link FileVisitResult#CONTINUE} (for now).
143         */
144        @Override public FileVisitResult visitFile(Path file,
145                                                   BasicFileAttributes attrs)
146        {
147            find(file);
148            return CONTINUE;
149        }
150
151        /**
152         * Invokes the pattern matching method on the given directory.
153         *
154         * @param dir Directory in question.
155         * @param attrs Attributes of {@code dir}. Not currently used.
156         *
157         * @return Always returns {@link FileVisitResult#CONTINUE} (for now).
158         */
159        @Override public FileVisitResult preVisitDirectory(
160            Path dir,
161            BasicFileAttributes attrs)
162        {
163            find(dir);
164            return CONTINUE;
165        }
166
167        /**
168         * Handle file {@literal "visitation"} errors.
169         *
170         * @param file File that could not be {@literal "visited"}.
171         * @param exc Exception associated with {@literal "visit"} to
172         *            {@code file}.
173         *
174         * @return Always returns {@link FileVisitResult#CONTINUE} (for now).
175         *
176         */
177        @Override public FileVisitResult visitFileFailed(Path file,
178                                                         IOException exc)
179        {
180            logger.warn("file='"+file+"'", exc);
181            return CONTINUE;
182        }
183    }
184
185    /**
186     * Find files matching the specified {@literal "glob"} pattern in the given
187     * directory (and all of its subdirectories).
188     *
189     * <p>Note: {@literal "glob"} patterns are simple DOS/UNIX style. Think
190     * {@literal "*.py"}.</p>
191     *
192     * @param path Directory to search.
193     * @param globPattern Pattern to match against.
194     *
195     * @return {@code List} of {@code String} versions of matching paths. The
196     * list will be empty ({@link Collections#emptyList()} if there were no
197     * matches.
198     */
199    public static List<String> findFiles(String path, String globPattern) {
200        return findFiles(path, globPattern, Integer.MAX_VALUE);
201    }
202
203    /**
204     * Find files matching the specified {@literal "glob"} pattern in the given
205     * directory (and not exceeding the given {@literal "depth"}).
206     *
207     * <p>Note: {@literal "glob"} patterns are simple DOS/UNIX style. Think
208     * {@literal "*.py"}.</p>
209     *
210     * @param path Directory to search.
211     * @param globPattern Pattern to match against.
212     * @param depth Maximum number of directory levels to visit.
213     *
214     * @return {@code List} of {@code String} versions of matching paths. The
215     * list will be empty ({@link Collections#emptyList()} if there were no
216     * matches.
217     */
218    public static List<String> findFiles(String path,
219                                         String globPattern,
220                                         int depth)
221    {
222        Finder f = new Finder(globPattern);
223        List<String> results = Collections.emptyList();
224        try {
225            Files.walkFileTree(Paths.get(path),
226                               EnumSet.of(FileVisitOption.FOLLOW_LINKS),
227                               depth,
228                               f);
229            results = f.results();
230        } catch (IOException e) {
231            logger.error("Could not search '"+path+"'", e);
232        }
233        return results;
234    }
235}