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 */ 020package org.sonar.api.utils; 021 022import java.util.HashMap; 023import java.util.Map; 024import java.util.regex.Pattern; 025 026import 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 */ 062public 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}