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