View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.filter;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.List;
23  import java.util.Stack;
24  
25  import org.apache.commons.lang3.ArrayUtils;
26  import org.apache.commons.lang3.StringUtils;
27  import org.apache.commons.vfs2.FileFilter;
28  import org.apache.commons.vfs2.FileSelectInfo;
29  
30  /**
31   * Filters files using the supplied wildcards.
32   * <p>
33   * This filter selects files and directories based on one or more wildcards.
34   * Testing is case-sensitive by default, but this can be configured.
35   * </p>
36   * <p>
37   * The wildcard matcher uses the characters '?' and '*' to represent a single or
38   * multiple wildcard characters. This is the same as often found on Dos/Unix
39   * command lines.
40   * </p>
41   * <p>
42   * For example, to retrieve and print all Java files that have the expression
43   * test in the name in the current directory:
44   * </p>
45   *
46   * <pre>
47   * FileSystemManager fsManager = VFS.getManager();
48   * FileObject dir = fsManager.toFileObject(new File(&quot;.&quot;));
49   * FileObject[] files;
50   * files = dir.findFiles(new FileFilterSelector(new WildcardFileFilter(&quot;*test*.java&quot;)));
51   * for (int i = 0; i &lt; files.length; i++) {
52   *     System.out.println(files[i]);
53   * }
54   * </pre>
55   *
56   * @author This code was originally ported from Apache Commons IO File Filter
57   * @see "https://commons.apache.org/proper/commons-io/"
58   * @since 2.4
59   */
60  public class WildcardFileFilter implements FileFilter, Serializable {
61  
62      private static final long serialVersionUID = 1L;
63  
64      /**
65       * Splits a string into a number of tokens. The text is split by '?' and '*'.
66       * Where multiple '*' occur consecutively they are collapsed into a single '*'.
67       *
68       * @param text the text to split
69       * @return the array of tokens, never null
70       */
71      // CHECKSTYLE:OFF Cyclomatic complexity of 12 is OK here
72      static String[] splitOnTokens(final String text) {
73          // used by wildcardMatch
74          // package level so a unit test may run on this
75  
76          if (text.indexOf('?') == -1 && text.indexOf('*') == -1) {
77              return new String[] {text};
78          }
79  
80          final char[] array = text.toCharArray();
81          final ArrayList<String> list = new ArrayList<>();
82          final StringBuilder builder = new StringBuilder();
83          for (int i = 0; i < array.length; i++) {
84              if (array[i] == '?' || array[i] == '*') {
85                  if (StringUtils.isNotEmpty(builder)) {
86                      list.add(builder.toString());
87                      builder.setLength(0);
88                  }
89                  if (array[i] == '?') {
90                      list.add("?");
91                  } else if (list.isEmpty() || i > 0 && !list.get(list.size() - 1).equals("*")) {
92                      list.add("*");
93                  }
94              } else {
95                  builder.append(array[i]);
96              }
97          }
98          if (StringUtils.isNotEmpty(builder)) {
99              list.add(builder.toString());
100         }
101 
102         return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
103     }
104 
105     /**
106      * Checks a file name to see if it matches the specified wildcard matcher
107      * allowing control over case-sensitivity.
108      * <p>
109      * The wildcard matcher uses the characters '?' and '*' to represent a single or
110      * multiple (zero or more) wildcard characters. The sequence "*?" does not
111      * work properly at present in match strings.
112      * </p>
113      *
114      * @param fileName        the file name to match on
115      * @param wildcardMatcher the wildcard string to match against
116      * @param caseSensitivity what case sensitivity rule to use, null means
117      *                        case-sensitive
118      *
119      * @return true if the file name matches the wildcard string
120      */
121     // CHECKSTYLE:OFF TODO xxx Cyclomatic complexity of 19 should be refactored
122     static boolean wildcardMatch(final String fileName, final String wildcardMatcher, IOCase caseSensitivity) {
123         if (fileName == null && wildcardMatcher == null) {
124             return true;
125         }
126         if (fileName == null || wildcardMatcher == null) {
127             return false;
128         }
129         if (caseSensitivity == null) {
130             caseSensitivity = IOCase.SENSITIVE;
131         }
132         final String[] wcs = splitOnTokens(wildcardMatcher);
133         boolean anyChars = false;
134         int textIdx = 0;
135         int wcsIdx = 0;
136         final Stack<int[]> backtrack = new Stack<>();
137 
138         // loop around a backtrack stack, to handle complex * matching
139         do {
140             if (!backtrack.isEmpty()) {
141                 final int[] array = backtrack.pop();
142                 wcsIdx = array[0];
143                 textIdx = array[1];
144                 anyChars = true;
145             }
146 
147             // loop whilst tokens and text left to process
148             while (wcsIdx < wcs.length) {
149 
150                 if (wcs[wcsIdx].equals("?")) {
151                     // ? so move to next text char
152                     textIdx++;
153                     if (textIdx > fileName.length()) {
154                         break;
155                     }
156                     anyChars = false;
157 
158                 } else if (wcs[wcsIdx].equals("*")) {
159                     // set any chars status
160                     anyChars = true;
161                     if (wcsIdx == wcs.length - 1) {
162                         textIdx = fileName.length();
163                     }
164 
165                 } else {
166                     // matching text token
167                     if (anyChars) {
168                         // any chars then try to locate text token
169                         textIdx = caseSensitivity.checkIndexOf(fileName, textIdx, wcs[wcsIdx]);
170                         if (textIdx == -1) {
171                             // token not found
172                             break;
173                         }
174                         final int repeat = caseSensitivity.checkIndexOf(fileName, textIdx + 1, wcs[wcsIdx]);
175                         if (repeat >= 0) {
176                             backtrack.push(new int[] {wcsIdx, repeat});
177                         }
178                     } else if (!caseSensitivity.checkRegionMatches(fileName, textIdx, wcs[wcsIdx])) {
179                         // matching from current position
180                         // couldn't match token
181                         break;
182                     }
183 
184                     // matched text token, move text index to end of matched
185                     // token
186                     textIdx += wcs[wcsIdx].length();
187                     anyChars = false;
188                 }
189 
190                 wcsIdx++;
191             }
192 
193             // full match
194             if (wcsIdx == wcs.length && textIdx == fileName.length()) {
195                 return true;
196             }
197 
198         } while (!backtrack.isEmpty());
199 
200         return false;
201     }
202     // CHECKSTYLE:ON
203 
204     /** Whether the comparison is case-sensitive. */
205     private final IOCase caseSensitivity;
206 
207     /** The wildcards that will be used to match file names. */
208     private final List<String> wildcards;
209 
210     /**
211      * Constructs a new wildcard filter for a list of wildcards specifying
212      * case-sensitivity.
213      *
214      * @param caseSensitivity how to handle case sensitivity, null means
215      *                        case-sensitive
216      * @param wildcards       the list of wildcards to match, not null
217      */
218     public WildcardFileFilter(final IOCase caseSensitivity, final List<String> wildcards) {
219         if (wildcards == null) {
220             throw new IllegalArgumentException("The wildcard list must not be null");
221         }
222         this.wildcards = new ArrayList<>(wildcards);
223         this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
224     }
225 
226     /**
227      * Constructs a new wildcard filter for an array of wildcards specifying
228      * case-sensitivity.
229      *
230      * @param caseSensitivity how to handle case sensitivity, null means
231      *                        case-sensitive
232      * @param wildcards       the array of wildcards to match, not null
233      */
234     public WildcardFileFilter(final IOCase caseSensitivity, final String... wildcards) {
235         if (wildcards == null) {
236             throw new IllegalArgumentException("The wildcard array must not be null");
237         }
238         this.wildcards = new ArrayList<>(Arrays.asList(wildcards));
239         this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
240     }
241 
242     /**
243      * Constructs a new case-sensitive wildcard filter for a list of wildcards.
244      *
245      * @param wildcards the list of wildcards to match, not null
246      */
247     public WildcardFileFilter(final List<String> wildcards) {
248         this(null, wildcards);
249     }
250 
251     /**
252      * Constructs a new case-sensitive wildcard filter for an array of wildcards.
253      * <p>
254      * The array is not cloned, so could be changed after constructing the instance.
255      * This would be inadvisable however.
256      *
257      * @param wildcards the array of wildcards to match
258      */
259     public WildcardFileFilter(final String... wildcards) {
260         this(null, wildcards);
261     }
262 
263     /**
264      * Checks to see if the file name matches one of the wildcards.
265      *
266      * @param fileSelectInfo the file to check
267      * @return true if the file name matches one of the wildcards
268      */
269     @Override
270     public boolean accept(final FileSelectInfo fileSelectInfo) {
271         final String name = fileSelectInfo.getFile().getName().getBaseName();
272         return wildcards.stream().anyMatch(wildcard -> wildcardMatch(name, wildcard, caseSensitivity));
273     }
274 
275     // CHECKSTYLE:ON
276 
277     /**
278      * Provide a String representation of this file filter.
279      *
280      * @return a String representation
281      */
282     @Override
283     public String toString() {
284         final StringBuilder buffer = new StringBuilder();
285         buffer.append(super.toString());
286         buffer.append("(");
287         if (wildcards != null) {
288             buffer.append(String.join(",", wildcards));
289         }
290         buffer.append(")");
291         return buffer.toString();
292     }
293 
294 }