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 }