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    }