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/**/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/**/*.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 }