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 "http://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      /** Whether the comparison is case sensitive. */
65      private final IOCase caseSensitivity;
66  
67      /** The wildcards that will be used to match file names. */
68      private final List<String> wildcards;
69  
70      /**
71       * Construct a new case-sensitive wildcard filter for a list of wildcards.
72       *
73       * @param wildcards the list of wildcards to match, not null
74       */
75      public WildcardFileFilter(final List<String> wildcards) {
76          this((IOCase) null, wildcards);
77      }
78  
79      /**
80       * Construct a new wildcard filter for a list of wildcards specifying
81       * case-sensitivity.
82       *
83       * @param caseSensitivity how to handle case sensitivity, null means
84       *                        case-sensitive
85       * @param wildcards       the list of wildcards to match, not null
86       */
87      public WildcardFileFilter(final IOCase caseSensitivity, final List<String> wildcards) {
88          if (wildcards == null) {
89              throw new IllegalArgumentException("The wildcard list must not be null");
90          }
91          this.wildcards = new ArrayList<>(wildcards);
92          this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
93      }
94  
95      /**
96       * Construct a new case-sensitive wildcard filter for an array of wildcards.
97       * <p>
98       * The array is not cloned, so could be changed after constructing the instance.
99       * This would be inadvisable however.
100      *
101      * @param wildcards the array of wildcards to match
102      */
103     public WildcardFileFilter(final String... wildcards) {
104         this((IOCase) null, wildcards);
105     }
106 
107     /**
108      * Construct a new wildcard filter for an array of wildcards specifying
109      * case-sensitivity.
110      *
111      * @param caseSensitivity how to handle case sensitivity, null means
112      *                        case-sensitive
113      * @param wildcards       the array of wildcards to match, not null
114      */
115     public WildcardFileFilter(final IOCase caseSensitivity, final String... wildcards) {
116         if (wildcards == null) {
117             throw new IllegalArgumentException("The wildcard array must not be null");
118         }
119         this.wildcards = new ArrayList<>(Arrays.asList(wildcards));
120         this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity;
121     }
122 
123     /**
124      * Checks to see if the file name matches one of the wildcards.
125      *
126      * @param fileSelectInfo the file to check
127      *
128      * @return true if the file name matches one of the wildcards
129      */
130     @Override
131     public boolean accept(final FileSelectInfo fileSelectInfo) {
132         final String name = fileSelectInfo.getFile().getName().getBaseName();
133         for (final String wildcard : wildcards) {
134             if (wildcardMatch(name, wildcard, caseSensitivity)) {
135                 return true;
136             }
137         }
138         return false;
139     }
140 
141     /**
142      * Provide a String representation of this file filter.
143      *
144      * @return a String representation
145      */
146     @Override
147     public String toString() {
148         final StringBuilder buffer = new StringBuilder();
149         buffer.append(super.toString());
150         buffer.append("(");
151         if (wildcards != null) {
152             for (int i = 0; i < wildcards.size(); i++) {
153                 if (i > 0) {
154                     buffer.append(",");
155                 }
156                 buffer.append(wildcards.get(i));
157             }
158         }
159         buffer.append(")");
160         return buffer.toString();
161     }
162 
163     /**
164      * Splits a string into a number of tokens. The text is split by '?' and '*'.
165      * Where multiple '*' occur consecutively they are collapsed into a single '*'.
166      *
167      * @param text the text to split
168      * @return the array of tokens, never null
169      */
170     // CHECKSTYLE:OFF Cyclomatic complexity of 12 is OK here
171     static String[] splitOnTokens(final String text) {
172         // used by wildcardMatch
173         // package level so a unit test may run on this
174 
175         if (text.indexOf('?') == -1 && text.indexOf('*') == -1) {
176             return new String[] { text };
177         }
178 
179         final char[] array = text.toCharArray();
180         final ArrayList<String> list = new ArrayList<>();
181         final StringBuilder builder = new StringBuilder();
182         for (int i = 0; i < array.length; i++) {
183             if (array[i] == '?' || array[i] == '*') {
184                 if (StringUtils.isNotEmpty(builder)) {
185                     list.add(builder.toString());
186                     builder.setLength(0);
187                 }
188                 if (array[i] == '?') {
189                     list.add("?");
190                 } else if (list.isEmpty() || i > 0 && !list.get(list.size() - 1).equals("*")) {
191                     list.add("*");
192                 }
193             } else {
194                 builder.append(array[i]);
195             }
196         }
197         if (StringUtils.isNotEmpty(builder)) {
198             list.add(builder.toString());
199         }
200 
201         return list.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
202     }
203 
204     // CHECKSTYLE:ON
205 
206     /**
207      * Checks a file name to see if it matches the specified wildcard matcher
208      * allowing control over case-sensitivity.
209      * <p>
210      * The wildcard matcher uses the characters '?' and '*' to represent a single or
211      * multiple (zero or more) wildcard characters. N.B. the sequence "*?" does not
212      * work properly at present in match strings.
213      * </p>
214      *
215      * @param fileName        the file name to match on
216      * @param wildcardMatcher the wildcard string to match against
217      * @param caseSensitivity what case sensitivity rule to use, null means
218      *                        case-sensitive
219      *
220      * @return true if the file name matches the wilcard string
221      */
222     // CHECKSTYLE:OFF TODO xxx Cyclomatic complexity of 19 should be refactored
223     static boolean wildcardMatch(final String fileName, final String wildcardMatcher, IOCase caseSensitivity) {
224         if (fileName == null && wildcardMatcher == null) {
225             return true;
226         }
227         if (fileName == null || wildcardMatcher == null) {
228             return false;
229         }
230         if (caseSensitivity == null) {
231             caseSensitivity = IOCase.SENSITIVE;
232         }
233         final String[] wcs = splitOnTokens(wildcardMatcher);
234         boolean anyChars = false;
235         int textIdx = 0;
236         int wcsIdx = 0;
237         final Stack<int[]> backtrack = new Stack<>();
238 
239         // loop around a backtrack stack, to handle complex * matching
240         do {
241             if (!backtrack.isEmpty()) {
242                 final int[] array = backtrack.pop();
243                 wcsIdx = array[0];
244                 textIdx = array[1];
245                 anyChars = true;
246             }
247 
248             // loop whilst tokens and text left to process
249             while (wcsIdx < wcs.length) {
250 
251                 if (wcs[wcsIdx].equals("?")) {
252                     // ? so move to next text char
253                     textIdx++;
254                     if (textIdx > fileName.length()) {
255                         break;
256                     }
257                     anyChars = false;
258 
259                 } else if (wcs[wcsIdx].equals("*")) {
260                     // set any chars status
261                     anyChars = true;
262                     if (wcsIdx == wcs.length - 1) {
263                         textIdx = fileName.length();
264                     }
265 
266                 } else {
267                     // matching text token
268                     if (anyChars) {
269                         // any chars then try to locate text token
270                         textIdx = caseSensitivity.checkIndexOf(fileName, textIdx, wcs[wcsIdx]);
271                         if (textIdx == -1) {
272                             // token not found
273                             break;
274                         }
275                         final int repeat = caseSensitivity.checkIndexOf(fileName, textIdx + 1, wcs[wcsIdx]);
276                         if (repeat >= 0) {
277                             backtrack.push(new int[] { wcsIdx, repeat });
278                         }
279                     } else if (!caseSensitivity.checkRegionMatches(fileName, textIdx, wcs[wcsIdx])) {
280                         // matching from current position
281                         // couldnt match token
282                         break;
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.isEmpty());
300 
301         return false;
302     }
303     // CHECKSTYLE:ON
304 
305 }