001    package org.codehaus.mojo.wagon.shared;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
005     * agreements. See the NOTICE file distributed with this work for additional information regarding
006     * copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance with the License. You may obtain a
008     * copy of the License at
009     *
010     * http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software distributed under the License
013     * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
014     * or implied. See the License for the specific language governing permissions and limitations under
015     * the License.
016     */
017    
018    import java.util.ArrayList;
019    import java.util.Collections;
020    import java.util.Iterator;
021    import java.util.List;
022    
023    import org.apache.maven.plugin.logging.Log;
024    import org.apache.maven.wagon.Wagon;
025    import org.apache.maven.wagon.WagonException;
026    import org.codehaus.plexus.util.StringUtils;
027    
028    public class WagonDirectoryScanner {
029        private final static String[] NOT_DIRECTORIES = new String[] { ".jar", ".zip", ".md5", ".sha1", ".pom", ".xml",
030                ".war" };
031        /**
032         * Patterns which should be excluded by default.
033         *
034         * @see #addDefaultExcludes()
035         */
036        public static final String[] DEFAULTEXCLUDES = org.codehaus.plexus.util.DirectoryScanner.DEFAULTEXCLUDES;
037    
038        /**
039         * The wagon
040         */
041        private Wagon wagon;
042    
043        /**
044         * Relative to wagon url
045         */
046        private String directory;
047    
048        /** The patterns for the wagon files to be included. */
049        private String[] includes;
050    
051        /** The patterns for the wagon files to be excluded. */
052        private String[] excludes;
053    
054        /**
055         * Whether or not the file system should be treated as a case sensitive one.
056         */
057        private boolean isCaseSensitive = true;
058    
059        /**
060         * The files which matched at least one include and at least one exclude and relative to directory
061         */
062        private List<String> filesIncluded = new ArrayList<String>();
063    
064        private Log logger;
065    
066        /**
067         * Sets the list of include patterns to use. All '/' and '\' characters are replaced by
068         * <code>File.separatorChar</code>, so the separator used need not match <code>File.separatorChar</code>.
069         * <p>
070         * When a pattern ends with a '/' or '\', "**" is appended.
071         *
072         * @param includes
073         *            A list of include patterns. May be <code>null</code>, indicating that all files should be included. If
074         *            a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
075         */
076        public void setIncludes(String[] includes) {
077            if (includes == null) {
078                this.includes = null;
079            } else {
080                this.includes = new String[includes.length];
081                for (int i = 0; i < includes.length; i++) {
082                    String pattern = includes[i].trim();
083    
084                    if (pattern.endsWith("/")) {
085                        pattern += "**";
086                    }
087                    this.includes[i] = pattern;
088                }
089            }
090        }
091    
092        /**
093         * Sets the list of exclude patterns to use. All '\' characters are replaced by '/'
094         * <p>
095         * When a pattern ends with a '/' or '\', "**" is appended.
096         *
097         * @param excludes
098         *            A list of exclude patterns. May be <code>null</code>, indicating that no files should be excluded. If
099         *            a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
100         */
101        public void setExcludes(String[] excludes) {
102            if (excludes == null) {
103                this.excludes = null;
104            } else {
105                this.excludes = new String[excludes.length];
106                for (int i = 0; i < excludes.length; i++) {
107                    String pattern = excludes[i].trim();
108    
109                    if (pattern.endsWith("/")) {
110                        pattern += "**";
111                    }
112                    this.excludes[i] = pattern;
113                }
114            }
115        }
116    
117        /**
118         * Tests whether or not a name matches against at least one include pattern.
119         *
120         * @param name
121         *            The name to match. Must not be <code>null</code>.
122         * @return <code>true</code> when the name matches against at least one include pattern, or <code>false</code>
123         *         otherwise.
124         */
125        private boolean isIncluded(String name) {
126            for (int i = 0; i < includes.length; i++) {
127                if (matchPath(includes[i], name, isCaseSensitive)) {
128                    return true;
129                }
130            }
131            return false;
132        }
133    
134        /**
135         * Tests whether or not a name matches against at least one exclude pattern.
136         *
137         * @param name
138         *            The name to match. Must not be <code>null</code>.
139         * @return <code>true</code> when the name matches against at least one exclude pattern, or <code>false</code>
140         *         otherwise.
141         */
142        protected boolean isExcluded(String name) {
143            for (int i = 0; i < excludes.length; i++) {
144                if (matchPath(excludes[i], name, isCaseSensitive)) {
145                    return true;
146                }
147            }
148            return false;
149        }
150    
151        /**
152         * Tests whether or not a name matches the start of at least one include pattern.
153         *
154         * @param name
155         *            The name to match. Must not be <code>null</code>.
156         * @return <code>true</code> when the name matches against the start of at least one include pattern, or
157         *         <code>false</code> otherwise.
158         */
159        protected boolean couldHoldIncluded(String name) {
160            for (int i = 0; i < includes.length; i++) {
161                if (matchPatternStart(includes[i], name, isCaseSensitive)) {
162                    return true;
163                }
164            }
165            return false;
166        }
167    
168        /**
169         * Tests whether or not a given path matches the start of a given pattern up to the first "**".
170         * <p>
171         * This is not a general purpose test and should only be used if you can live with false positives. For example,
172         * <code>pattern=**\a</code> and <code>str=b</code> will yield <code>true</code>.
173         *
174         * @param pattern
175         *            The pattern to match against. Must not be <code>null</code>.
176         * @param str
177         *            The path to match, as a String. Must not be <code>null</code>.
178         * @param isCaseSensitive
179         *            Whether or not matching should be performed case sensitively.
180         *
181         * @return whether or not a given path matches the start of a given pattern up to the first "**".
182         */
183        protected static boolean matchPatternStart(String pattern, String str, boolean isCaseSensitive) {
184            return SelectorUtils.matchPatternStart(pattern, str, isCaseSensitive);
185        }
186    
187        /**
188         * Tests whether or not a given path matches a given pattern.
189         *
190         * @param pattern
191         *            The pattern to match against. Must not be <code>null</code>.
192         * @param str
193         *            The path to match, as a String. Must not be <code>null</code>.
194         * @param isCaseSensitive
195         *            Whether or not matching should be performed case sensitively.
196         *
197         * @return <code>true</code> if the pattern matches against the string, or <code>false</code> otherwise.
198         */
199        private static boolean matchPath(String pattern, String str, boolean isCaseSensitive) {
200            return SelectorUtils.matchPath(pattern, str, isCaseSensitive);
201        }
202    
203        public void scan() throws WagonException {
204            if (wagon == null) {
205                throw new IllegalStateException("No wagon set");
206            }
207    
208            if (StringUtils.isBlank(directory)) {
209                directory = "";
210            }
211    
212            if (includes == null) {
213                // No includes supplied, so set it to 'matches all'
214                includes = new String[1];
215                includes[0] = "**";
216            }
217    
218            if (excludes == null) {
219                excludes = new String[0];
220            }
221    
222            filesIncluded = new ArrayList<String>();
223    
224            scandir(directory, "");
225    
226            Collections.sort(filesIncluded);
227    
228        }
229    
230        /**
231         * Adds default exclusions to the current exclusions set.
232         */
233        public void addDefaultExcludes() {
234            int excludesLength = excludes == null ? 0 : excludes.length;
235            String[] newExcludes;
236            newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
237            if (excludesLength > 0) {
238                System.arraycopy(excludes, 0, newExcludes, 0, excludesLength);
239            }
240            for (int i = 0; i < DEFAULTEXCLUDES.length; i++) {
241                newExcludes[i + excludesLength] = DEFAULTEXCLUDES[i];
242            }
243            excludes = newExcludes;
244        }
245    
246        /**
247         * Jenkins, if nothing else, will return pathnames with * characters in them that lead to infinite recursion down
248         * here. Given the impoverished API to the wagons, some ad-hoc filtration is called for. The filters in here are
249         * just culled from strange stuff we see from Jenkins.
250         *
251         * @param fileName
252         *            supposed file name
253         * @return true if it seems like a bad idea.
254         */
255        private boolean isRidiculousFile(String fileName) {
256            return fileName.endsWith(".") || fileName.contains("*") || fileName.startsWith("?") || fileName.startsWith("#");
257        }
258    
259        // //////////////////////////////////////////////////////////////////////////////////
260        /**
261         * Scans the given directory for files and directories. Found files are placed in a collection, based on the
262         * matching of includes, excludes, and the selectors. When a directory is found, it is scanned recursively.
263         *
264         * @throws WagonException
265         *
266         * @see #filesIncluded
267         */
268        private void scandir(String dir, String vpath) throws WagonException {
269            logger.debug("scandir: dir: " + dir + " vpath: " + vpath);
270            List<?> files = wagon.getFileList(dir);
271    
272            for (Iterator<?> iterator = files.iterator(); iterator.hasNext();) {
273                String fileName = (String) iterator.next();
274    
275                if (isRidiculousFile(fileName)) {
276                    continue;
277                }
278    
279                String file = fileName;
280    
281                if (!StringUtils.isBlank(dir)) {
282                    if (dir.endsWith("/")) {
283                        file = dir + fileName;
284                    } else {
285                        file = dir + "/" + fileName;
286                    }
287                }
288    
289                String name = vpath + fileName;
290    
291                if (this.isDirectory(file)) {
292    
293                    if (!name.endsWith("/")) {
294                        name += "/";
295                    }
296    
297                    if (isIncluded(name)) {
298                        if (!isExcluded(name)) {
299                            scandir(file, name);
300                        } else {
301                            if (couldHoldIncluded(name)) {
302                                scandir(file, name);
303                            }
304                        }
305                    } else {
306                        if (couldHoldIncluded(name)) {
307                            scandir(file, name);
308                        }
309                    }
310    
311                } else {
312    
313                    if (isIncluded(name)) {
314                        if (!isExcluded(name)) {
315                            filesIncluded.add(name);
316                        }
317                    }
318                }
319            }
320        }
321    
322        private boolean isDirectory(String existedRemotePath) throws WagonException {
323            for (int x = 0; x < NOT_DIRECTORIES.length; x++) {
324                if (existedRemotePath.endsWith(NOT_DIRECTORIES[x])) {
325                    return false;
326                }
327            }
328            if (existedRemotePath.endsWith("/")) {
329                return true;
330            }
331    
332            return wagon.resourceExists(existedRemotePath + "/");
333        }
334    
335        // ///////////////////////////////////////////////////////////////////////////////
336        public List<String> getFilesIncluded() {
337            return filesIncluded;
338        }
339    
340        public void setWagon(Wagon wagon) {
341            this.wagon = wagon;
342        }
343    
344        public void setCaseSensitive(boolean isCaseSensitive) {
345            this.isCaseSensitive = isCaseSensitive;
346        }
347    
348        public void setDirectory(String basePath) {
349            this.directory = basePath;
350        }
351    
352        public Log getLogger() {
353            return logger;
354        }
355    
356        public void setLogger(Log logger) {
357            this.logger = logger;
358        }
359    
360    }