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