001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2014 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * SonarQube is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * SonarQube is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public License
017     * along with this program; if not, write to the Free Software Foundation,
018     * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019     */
020    package org.sonar.api.utils;
021    
022    import java.util.HashMap;
023    import java.util.Map;
024    import java.util.regex.Pattern;
025    
026    import org.apache.commons.lang.StringUtils;
027    
028    /**
029     * Implementation of Ant-style matching patterns.
030     * Contrary to other implementations (like AntPathMatcher from Spring Framework) it is based on {@link Pattern Java Regular Expressions}.
031     * To increase performance it holds an internal cache of all processed patterns.
032     * <p>
033     * Following rules are applied:
034     * <ul>
035     * <li>? matches single character</li>
036     * <li>* matches zero or more characters</li>
037     * <li>** matches zero or more 'directories'</li>
038     * </ul>
039     * </p>
040     * <p>
041     * Some examples of patterns:
042     * <ul>
043     * <li><code>org/T?st.java</code> - matches <code>org/Test.java</code> and also <code>org/Tost.java</code></li>
044     * <li><code>org/*.java</code> - matches all <code>.java</code> files in the <code>org</code> directory,
045     * e.g. <code>org/Foo.java</code> or <code>org/Bar.java</code></li>
046     * <li><code>org/**</code> - matches all files underneath the <code>org</code> directory,
047     * e.g. <code>org/Foo.java</code> or <code>org/foo/bar.jsp</code></li>
048     * <li><code>org/&#42;&#42;/Test.java</code> - matches all <code>Test.java</code> files underneath the <code>org</code> directory,
049     * e.g. <code>org/Test.java</code> or <code>org/foo/Test.java</code> or <code>org/foo/bar/Test.java</code></li>
050     * <li><code>org/&#42;&#42;/*.java</code> - matches all <code>.java</code> files underneath the <code>org</code> directory,
051     * e.g. <code>org/Foo.java</code> or <code>org/foo/Bar.java</code> or <code>org/foo/bar/Baz.java</code></li>
052     * </ul>
053     * </p>
054     * <p>
055     * Another implementation, which is also based on Java Regular Expressions, can be found in
056     * <a href="https://github.com/JetBrains/intellij-community/blob/idea/107.743/platform/util/src/com/intellij/openapi/util/io/FileUtil.java#L847">FileUtil</a>
057     * from IntelliJ OpenAPI.
058     * </p>
059     * 
060     * @since 1.10
061     */
062    public class WildcardPattern {
063    
064      private static final Map<String, WildcardPattern> CACHE = new HashMap<String, WildcardPattern>();
065      private static final String SPECIAL_CHARS = "()[]^$.{}+|";
066    
067      private Pattern pattern;
068      private String stringRepresentation;
069    
070      protected WildcardPattern(String pattern, String directorySeparator) {
071        this.stringRepresentation = pattern;
072        this.pattern = Pattern.compile(toRegexp(pattern, directorySeparator));
073      }
074    
075      private static String toRegexp(String antPattern, String directorySeparator) {
076        final String escapedDirectorySeparator = '\\' + directorySeparator;
077    
078        final StringBuilder sb = new StringBuilder(antPattern.length());
079    
080        sb.append('^');
081    
082        int i = antPattern.startsWith("/") || antPattern.startsWith("\\") ? 1 : 0;
083        while (i < antPattern.length()) {
084          final char ch = antPattern.charAt(i);
085    
086          if (SPECIAL_CHARS.indexOf(ch) != -1) {
087            // Escape regexp-specific characters
088            sb.append('\\').append(ch);
089          } else if (ch == '*') {
090            if (i + 1 < antPattern.length() && antPattern.charAt(i + 1) == '*') {
091              // Double asterisk
092              // Zero or more directories
093              if (i + 2 < antPattern.length() && isSlash(antPattern.charAt(i + 2))) {
094                sb.append("(?:.*").append(escapedDirectorySeparator).append("|)");
095                i += 2;
096              } else {
097                sb.append(".*");
098                i += 1;
099              }
100            } else {
101              // Single asterisk
102              // Zero or more characters excluding directory separator
103              sb.append("[^").append(escapedDirectorySeparator).append("]*?");
104            }
105          } else if (ch == '?') {
106            // Any single character excluding directory separator
107            sb.append("[^").append(escapedDirectorySeparator).append("]");
108          } else if (isSlash(ch)) {
109            // Directory separator
110            sb.append(escapedDirectorySeparator);
111          } else {
112            // Single character
113            sb.append(ch);
114          }
115    
116          i++;
117        }
118    
119        sb.append('$');
120    
121        return sb.toString();
122      }
123    
124      private static boolean isSlash(char ch) {
125        return ch == '/' || ch == '\\';
126      }
127    
128      /**
129       * Returns string representation of this pattern.
130       * 
131       * @since 2.5
132       */
133      @Override
134      public String toString() {
135        return stringRepresentation;
136      }
137    
138      /**
139       * Returns true if specified value matches this pattern.
140       */
141      public boolean match(String value) {
142        value = StringUtils.removeStart(value, "/");
143        value = StringUtils.removeEnd(value, "/");
144        return pattern.matcher(value).matches();
145      }
146    
147      /**
148       * Returns true if specified value matches one of specified patterns.
149       * 
150       * @since 2.4
151       */
152      public static boolean match(WildcardPattern[] patterns, String value) {
153        for (WildcardPattern pattern : patterns) {
154          if (pattern.match(value)) {
155            return true;
156          }
157        }
158        return false;
159      }
160    
161      /**
162       * Creates pattern with "/" as a directory separator.
163       * 
164       * @see #create(String, String)
165       */
166      public static WildcardPattern create(String pattern) {
167        return create(pattern, "/");
168      }
169    
170      /**
171       * Creates array of patterns with "/" as a directory separator.
172       * 
173       * @see #create(String, String)
174       */
175      public static WildcardPattern[] create(String[] patterns) {
176        if (patterns == null) {
177          return new WildcardPattern[0];
178        }
179        WildcardPattern[] exclusionPAtterns = new WildcardPattern[patterns.length];
180        for (int i = 0; i < patterns.length; i++) {
181          exclusionPAtterns[i] = create(patterns[i]);
182        }
183        return exclusionPAtterns;
184      }
185    
186      /**
187       * Creates pattern with specified separator for directories.
188       * <p>
189       * This is used to match Java-classes, i.e. <code>org.foo.Bar</code> against <code>org/**</code>.
190       * <b>However usage of character other than "/" as a directory separator is misleading and should be avoided,
191       * so method {@link #create(String)} is preferred over this one.</b>
192       * </p>
193       * <p>
194       * Also note that no matter whether forward or backward slashes were used in the <code>antPattern</code>
195       * the returned pattern will use <code>directorySeparator</code>.
196       * Thus to match Windows-style path "dir\file.ext" against pattern "dir/file.ext" normalization should be performed.
197       * </p>
198       */
199      public static WildcardPattern create(String pattern, String directorySeparator) {
200        String key = pattern + directorySeparator;
201        WildcardPattern wildcardPattern = CACHE.get(key);
202        if (wildcardPattern == null) {
203          wildcardPattern = new WildcardPattern(pattern, directorySeparator);
204          CACHE.put(key, wildcardPattern);
205        }
206        return wildcardPattern;
207      }
208    }