001 /**
002 * Copyright 2008-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.codehaus.mojo.wagon.shared;
017
018 /*
019 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
020 * agreements. See the NOTICE file distributed with this work for additional information regarding
021 * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
022 * "License"); you may not use this file except in compliance with the License. You may obtain a
023 * copy of the License at
024 *
025 * http://www.apache.org/licenses/LICENSE-2.0
026 *
027 * Unless required by applicable law or agreed to in writing, software distributed under the License
028 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
029 * or implied. See the License for the specific language governing permissions and limitations under
030 * the License.
031 */
032
033 import static org.codehaus.plexus.util.StringUtils.isBlank;
034
035 import java.util.ArrayList;
036 import java.util.Collections;
037 import java.util.Iterator;
038 import java.util.List;
039
040 import org.apache.maven.plugin.logging.Log;
041 import org.apache.maven.wagon.Wagon;
042 import org.apache.maven.wagon.WagonException;
043 import org.codehaus.plexus.util.StringUtils;
044 import org.kuali.common.util.inform.Inform;
045 import org.kuali.common.util.inform.PercentCompleteInformer;
046
047 public class WagonDirectoryScanner {
048
049 private final static String[] NOT_DIRECTORIES = new String[] { ".jar", ".zip", ".md5", ".sha1", ".pom", ".xml", ".war" };
050
051 /**
052 * Patterns which should be excluded by default.
053 *
054 * @see #addDefaultExcludes()
055 */
056 public static final String[] DEFAULTEXCLUDES = org.codehaus.plexus.util.DirectoryScanner.DEFAULTEXCLUDES;
057
058 /**
059 * The wagon
060 */
061 private Wagon wagon;
062
063 /**
064 * Relative to wagon url
065 */
066 private String directory;
067
068 /**
069 * Print a dot to the console each time we scan a directory
070 */
071 private PercentCompleteInformer informer = new PercentCompleteInformer(100);
072
073 /** The patterns for the wagon files to be included. */
074 private String[] includes;
075
076 /** The patterns for the wagon files to be excluded. */
077 private String[] excludes;
078
079 /**
080 * Whether or not the file system should be treated as a case sensitive one.
081 */
082 private boolean isCaseSensitive = true;
083
084 /**
085 * The files which matched at least one include and at least one exclude and relative to directory
086 */
087 private List<String> filesIncluded = new ArrayList<String>();
088
089 private Log logger;
090
091 /**
092 * 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
093 * <code>File.separatorChar</code>.
094 * <p>
095 * When a pattern ends with a '/' or '\', "**" is appended.
096 *
097 * @param includes
098 * 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
099 * 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 }