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("."));
49 * FileObject[] files;
50 * files = dir.findFiles(new FileFilterSelector(new WildcardFileFilter("*test*.java")));
51 * for (int i = 0; i < 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 }