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