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