1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.storage.repository.git;
18
19 import static com.linecorp.centraldogma.internal.Util.validatePathPattern;
20
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.regex.Pattern;
25
26 import org.eclipse.jgit.treewalk.TreeWalk;
27 import org.eclipse.jgit.treewalk.filter.TreeFilter;
28
29 import com.linecorp.centraldogma.server.storage.repository.Repository;
30
31 final class PathPatternFilter extends TreeFilter {
32
33 private static final Pattern SPLIT = Pattern.compile("\\s*,\\s*");
34
35 private static final ThreadLocal<LruMap<String, PathPatternFilter>> filterCache =
36 LruMap.newThreadLocal(512);
37
38 private static final ThreadLocal<LruMap<String, Pattern>> regexCache = LruMap.newThreadLocal(1024);
39
40 static PathPatternFilter of(String pathPattern) {
41 final LruMap<String, PathPatternFilter> map = filterCache.get();
42 PathPatternFilter f = map.get(pathPattern);
43 if (f == null) {
44 f = new PathPatternFilter(pathPattern);
45 map.put(pathPattern, f);
46 }
47
48 return f;
49 }
50
51 private final Pattern[] pathPatterns;
52 private final String pathPattern;
53
54 private PathPatternFilter(String pathPattern) {
55 validatePathPattern(pathPattern, "pathPattern");
56
57 final String[] pathPatterns = SPLIT.split(pathPattern);
58 final StringBuilder pathPatternBuf = new StringBuilder(pathPattern.length());
59 final List<Pattern> compiledPathPatterns = new ArrayList<>(pathPatterns.length);
60 boolean matchAll = false;
61 for (String p: pathPatterns) {
62 if (Repository.ALL_PATH.equals(p)) {
63 matchAll = true;
64 break;
65 }
66
67 if (p.isEmpty()) {
68 continue;
69 }
70
71 final String normalized = normalize(p);
72 compiledPathPatterns.add(compile(normalized));
73 pathPatternBuf.append(normalized).append(',');
74 }
75
76 if (matchAll) {
77 this.pathPatterns = null;
78 this.pathPattern = "/**";
79 } else {
80 if (compiledPathPatterns.isEmpty()) {
81 throw new IllegalArgumentException("pathPattern is empty.");
82 }
83
84 this.pathPatterns = compiledPathPatterns.toArray(new Pattern[compiledPathPatterns.size()]);
85 this.pathPattern = pathPatternBuf.substring(0, pathPatternBuf.length() - 1);
86 }
87 }
88
89 private static String normalize(String p) {
90 final String normalized;
91 if (p.charAt(0) != '/') {
92 normalized = "/**/" + p;
93 } else {
94 normalized = p;
95 }
96 return normalized;
97 }
98
99 private static Pattern compile(final String pathPattern) {
100 if (pathPattern.isEmpty()) {
101 throw new IllegalArgumentException("contains an empty path pattern");
102 }
103
104 final Map<String, Pattern> map = regexCache.get();
105 Pattern compiled = map.get(pathPattern);
106 if (compiled == null) {
107 compiled = compileUncached(pathPattern);
108 map.put(pathPattern, compiled);
109 }
110
111 return compiled;
112 }
113
114 private static Pattern compileUncached(String pathPattern) {
115 if (pathPattern.charAt(0) != '/') {
116 pathPattern = "/**/" + pathPattern;
117 }
118
119 final int pathPatternLen = pathPattern.length();
120 final StringBuilder buf = new StringBuilder(pathPatternLen).append('^');
121 int asterisks = 0;
122 char beforeAsterisk = '/';
123
124 for (int i = 1; i < pathPatternLen; i++) {
125 final char c = pathPattern.charAt(i);
126 if (c == '*') {
127 asterisks++;
128 if (asterisks > 2) {
129 throw new IllegalArgumentException(
130 "contains a path pattern with invalid wildcard characters: " + pathPattern +
131 " (only * and ** are allowed)");
132 }
133 continue;
134 }
135
136 switch (asterisks) {
137 case 1:
138
139 if (beforeAsterisk == '/' && c == '/') {
140 buf.append("[^/]+");
141 } else {
142 buf.append("[^/]*");
143 }
144 break;
145 case 2:
146
147 if (beforeAsterisk == '/' && c == '/') {
148 buf.append("(?:.+/)?");
149 asterisks = 0;
150 beforeAsterisk = c;
151 continue;
152 }
153
154 buf.append(".*");
155 break;
156 }
157
158 asterisks = 0;
159 beforeAsterisk = c;
160
161 switch (c) {
162 case '\\':
163 case '.':
164 case '^':
165 case '$':
166 case '?':
167 case '+':
168 case '{':
169 case '}':
170 case '[':
171 case ']':
172 case '(':
173 case ')':
174 case '|':
175 buf.append('\\');
176 buf.append(c);
177 break;
178 default:
179 buf.append(c);
180 }
181 }
182
183
184 switch (asterisks) {
185 case 1:
186 if (beforeAsterisk == '/') {
187
188 buf.append("[^/]+");
189 } else {
190 buf.append("[^/]*");
191 }
192 break;
193 case 2:
194 buf.append(".*");
195 break;
196 }
197
198 return Pattern.compile(buf.append('$').toString());
199 }
200
201 @Override
202 public boolean include(TreeWalk walker) {
203 if (walker.isSubtree()) {
204 return true;
205 }
206
207 return matches(walker);
208 }
209
210 public boolean matches(TreeWalk walker) {
211 if (pathPatterns == null) {
212 return true;
213 }
214
215 for (Pattern p: pathPatterns) {
216 if (p.matcher(walker.getPathString()).matches()) {
217 return true;
218 }
219 }
220
221 return false;
222 }
223
224 public boolean matches(String path) {
225 if (pathPatterns == null) {
226 return true;
227 }
228
229 for (Pattern p: pathPatterns) {
230 if (p.matcher(path).matches()) {
231 return true;
232 }
233 }
234
235 return false;
236 }
237
238 public boolean matchesAll() {
239 return pathPatterns == null;
240 }
241
242 @Override
243 public boolean shouldBeRecursive() {
244 return true;
245 }
246
247 @Override
248 @SuppressWarnings("CloneInNonCloneableClass")
249 public PathPatternFilter clone() {
250 return this;
251 }
252
253 @Override
254 public int hashCode() {
255 return pathPattern.hashCode();
256 }
257
258 @Override
259 public boolean equals(Object obj) {
260 if (!(obj instanceof PathPatternFilter)) {
261 return false;
262 }
263
264 if (this == obj) {
265 return true;
266 }
267
268 return pathPattern.equals(((PathPatternFilter) obj).pathPattern);
269 }
270
271 @Override
272 public String toString() {
273 return pathPattern;
274 }
275 }