1 package org.codehaus.mojo.wagon.shared;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
5 * agreements. See the NOTICE file distributed with this work for additional information regarding
6 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance with the License. You may obtain a
8 * copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
15 * the License.
16 */
17
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22
23 import org.apache.maven.plugin.logging.Log;
24 import org.apache.maven.wagon.Wagon;
25 import org.apache.maven.wagon.WagonException;
26 import org.codehaus.plexus.util.StringUtils;
27
28 public class WagonDirectoryScanner {
29 private final static String[] NOT_DIRECTORIES = new String[] { ".jar", ".zip", ".md5", ".sha1", ".pom", ".xml",
30 ".war" };
31 /**
32 * Patterns which should be excluded by default.
33 *
34 * @see #addDefaultExcludes()
35 */
36 public static final String[] DEFAULTEXCLUDES = org.codehaus.plexus.util.DirectoryScanner.DEFAULTEXCLUDES;
37
38 /**
39 * The wagon
40 */
41 private Wagon wagon;
42
43 /**
44 * Relative to wagon url
45 */
46 private String directory;
47
48 /** The patterns for the wagon files to be included. */
49 private String[] includes;
50
51 /** The patterns for the wagon files to be excluded. */
52 private String[] excludes;
53
54 /**
55 * Whether or not the file system should be treated as a case sensitive one.
56 */
57 private boolean isCaseSensitive = true;
58
59 /**
60 * The files which matched at least one include and at least one exclude and relative to directory
61 */
62 private List<String> filesIncluded = new ArrayList<String>();
63
64 private Log logger;
65
66 /**
67 * Sets the list of include patterns to use. All '/' and '\' characters are replaced by
68 * <code>File.separatorChar</code>, so the separator used need not match <code>File.separatorChar</code>.
69 * <p>
70 * When a pattern ends with a '/' or '\', "**" is appended.
71 *
72 * @param includes
73 * A list of include patterns. May be <code>null</code>, indicating that all files should be included. If
74 * a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
75 */
76 public void setIncludes(String[] includes) {
77 if (includes == null) {
78 this.includes = null;
79 } else {
80 this.includes = new String[includes.length];
81 for (int i = 0; i < includes.length; i++) {
82 String pattern = includes[i].trim();
83
84 if (pattern.endsWith("/")) {
85 pattern += "**";
86 }
87 this.includes[i] = pattern;
88 }
89 }
90 }
91
92 /**
93 * Sets the list of exclude patterns to use. All '\' characters are replaced by '/'
94 * <p>
95 * When a pattern ends with a '/' or '\', "**" is appended.
96 *
97 * @param excludes
98 * A list of exclude patterns. May be <code>null</code>, indicating that no files should be excluded. If
99 * a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
100 */
101 public void setExcludes(String[] excludes) {
102 if (excludes == null) {
103 this.excludes = null;
104 } else {
105 this.excludes = new String[excludes.length];
106 for (int i = 0; i < excludes.length; i++) {
107 String pattern = excludes[i].trim();
108
109 if (pattern.endsWith("/")) {
110 pattern += "**";
111 }
112 this.excludes[i] = pattern;
113 }
114 }
115 }
116
117 /**
118 * Tests whether or not a name matches against at least one include pattern.
119 *
120 * @param name
121 * The name to match. Must not be <code>null</code>.
122 * @return <code>true</code> when the name matches against at least one include pattern, or <code>false</code>
123 * otherwise.
124 */
125 private boolean isIncluded(String name) {
126 for (int i = 0; i < includes.length; i++) {
127 if (matchPath(includes[i], name, isCaseSensitive)) {
128 return true;
129 }
130 }
131 return false;
132 }
133
134 /**
135 * Tests whether or not a name matches against at least one exclude pattern.
136 *
137 * @param name
138 * The name to match. Must not be <code>null</code>.
139 * @return <code>true</code> when the name matches against at least one exclude pattern, or <code>false</code>
140 * otherwise.
141 */
142 protected boolean isExcluded(String name) {
143 for (int i = 0; i < excludes.length; i++) {
144 if (matchPath(excludes[i], name, isCaseSensitive)) {
145 return true;
146 }
147 }
148 return false;
149 }
150
151 /**
152 * Tests whether or not a name matches the start of at least one include pattern.
153 *
154 * @param name
155 * The name to match. Must not be <code>null</code>.
156 * @return <code>true</code> when the name matches against the start of at least one include pattern, or
157 * <code>false</code> otherwise.
158 */
159 protected boolean couldHoldIncluded(String name) {
160 for (int i = 0; i < includes.length; i++) {
161 if (matchPatternStart(includes[i], name, isCaseSensitive)) {
162 return true;
163 }
164 }
165 return false;
166 }
167
168 /**
169 * Tests whether or not a given path matches the start of a given pattern up to the first "**".
170 * <p>
171 * This is not a general purpose test and should only be used if you can live with false positives. For example,
172 * <code>pattern=**\a</code> and <code>str=b</code> will yield <code>true</code>.
173 *
174 * @param pattern
175 * The pattern to match against. Must not be <code>null</code>.
176 * @param str
177 * The path to match, as a String. Must not be <code>null</code>.
178 * @param isCaseSensitive
179 * Whether or not matching should be performed case sensitively.
180 *
181 * @return whether or not a given path matches the start of a given pattern up to the first "**".
182 */
183 protected static boolean matchPatternStart(String pattern, String str, boolean isCaseSensitive) {
184 return SelectorUtils.matchPatternStart(pattern, str, isCaseSensitive);
185 }
186
187 /**
188 * Tests whether or not a given path matches a given pattern.
189 *
190 * @param pattern
191 * The pattern to match against. Must not be <code>null</code>.
192 * @param str
193 * The path to match, as a String. Must not be <code>null</code>.
194 * @param isCaseSensitive
195 * Whether or not matching should be performed case sensitively.
196 *
197 * @return <code>true</code> if the pattern matches against the string, or <code>false</code> otherwise.
198 */
199 private static boolean matchPath(String pattern, String str, boolean isCaseSensitive) {
200 return SelectorUtils.matchPath(pattern, str, isCaseSensitive);
201 }
202
203 public void scan() throws WagonException {
204 if (wagon == null) {
205 throw new IllegalStateException("No wagon set");
206 }
207
208 if (StringUtils.isBlank(directory)) {
209 directory = "";
210 }
211
212 if (includes == null) {
213 // No includes supplied, so set it to 'matches all'
214 includes = new String[1];
215 includes[0] = "**";
216 }
217
218 if (excludes == null) {
219 excludes = new String[0];
220 }
221
222 filesIncluded = new ArrayList<String>();
223
224 scandir(directory, "");
225
226 Collections.sort(filesIncluded);
227
228 }
229
230 /**
231 * Adds default exclusions to the current exclusions set.
232 */
233 public void addDefaultExcludes() {
234 int excludesLength = excludes == null ? 0 : excludes.length;
235 String[] newExcludes;
236 newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
237 if (excludesLength > 0) {
238 System.arraycopy(excludes, 0, newExcludes, 0, excludesLength);
239 }
240 for (int i = 0; i < DEFAULTEXCLUDES.length; i++) {
241 newExcludes[i + excludesLength] = DEFAULTEXCLUDES[i];
242 }
243 excludes = newExcludes;
244 }
245
246 /**
247 * Jenkins, if nothing else, will return pathnames with * characters in them that lead to infinite recursion down
248 * here. Given the impoverished API to the wagons, some ad-hoc filtration is called for. The filters in here are
249 * just culled from strange stuff we see from Jenkins.
250 *
251 * @param fileName
252 * supposed file name
253 * @return true if it seems like a bad idea.
254 */
255 private boolean isRidiculousFile(String fileName) {
256 return fileName.endsWith(".") || fileName.contains("*") || fileName.startsWith("?") || fileName.startsWith("#");
257 }
258
259 // //////////////////////////////////////////////////////////////////////////////////
260 /**
261 * Scans the given directory for files and directories. Found files are placed in a collection, based on the
262 * matching of includes, excludes, and the selectors. When a directory is found, it is scanned recursively.
263 *
264 * @throws WagonException
265 *
266 * @see #filesIncluded
267 */
268 private void scandir(String dir, String vpath) throws WagonException {
269 logger.debug("scandir: dir: " + dir + " vpath: " + vpath);
270 List<?> files = wagon.getFileList(dir);
271
272 for (Iterator<?> iterator = files.iterator(); iterator.hasNext();) {
273 String fileName = (String) iterator.next();
274
275 if (isRidiculousFile(fileName)) {
276 continue;
277 }
278
279 String file = fileName;
280
281 if (!StringUtils.isBlank(dir)) {
282 if (dir.endsWith("/")) {
283 file = dir + fileName;
284 } else {
285 file = dir + "/" + fileName;
286 }
287 }
288
289 String name = vpath + fileName;
290
291 if (this.isDirectory(file)) {
292
293 if (!name.endsWith("/")) {
294 name += "/";
295 }
296
297 if (isIncluded(name)) {
298 if (!isExcluded(name)) {
299 scandir(file, name);
300 } else {
301 if (couldHoldIncluded(name)) {
302 scandir(file, name);
303 }
304 }
305 } else {
306 if (couldHoldIncluded(name)) {
307 scandir(file, name);
308 }
309 }
310
311 } else {
312
313 if (isIncluded(name)) {
314 if (!isExcluded(name)) {
315 filesIncluded.add(name);
316 }
317 }
318 }
319 }
320 }
321
322 private boolean isDirectory(String existedRemotePath) throws WagonException {
323 for (int x = 0; x < NOT_DIRECTORIES.length; x++) {
324 if (existedRemotePath.endsWith(NOT_DIRECTORIES[x])) {
325 return false;
326 }
327 }
328 if (existedRemotePath.endsWith("/")) {
329 return true;
330 }
331
332 return wagon.resourceExists(existedRemotePath + "/");
333 }
334
335 // ///////////////////////////////////////////////////////////////////////////////
336 public List<String> getFilesIncluded() {
337 return filesIncluded;
338 }
339
340 public void setWagon(Wagon wagon) {
341 this.wagon = wagon;
342 }
343
344 public void setCaseSensitive(boolean isCaseSensitive) {
345 this.isCaseSensitive = isCaseSensitive;
346 }
347
348 public void setDirectory(String basePath) {
349 this.directory = basePath;
350 }
351
352 public Log getLogger() {
353 return logger;
354 }
355
356 public void setLogger(Log logger) {
357 this.logger = logger;
358 }
359
360 }