View Javadoc
1   package com.atlassian.plugin.servlet.util;
2   
3   import java.io.Serializable;
4   import java.util.ArrayList;
5   import java.util.Collection;
6   import java.util.Collections;
7   import java.util.HashMap;
8   import java.util.HashSet;
9   import java.util.Iterator;
10  import java.util.LinkedHashSet;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Set;
14  import java.util.concurrent.locks.ReadWriteLock;
15  import java.util.concurrent.locks.ReentrantReadWriteLock;
16  import java.util.regex.Pattern;
17  
18  /**
19   * Originally copied from Atlassian Seraph 1.0
20   * <p>
21   * Modified to store a list of keys for a mapping rather than a single value. This allows filters to be added that
22   * listen on the same path. The get() method will return the first value added if there are multiple keys matching the
23   * path, while the getAll() method returns the aggregate matches.
24   * <p>
25   * In practice, matching a servlet path should use the get() method and matching filters should use the getAll() method.
26   *
27   * @since 2.1.0
28   */
29  public class DefaultPathMapper implements Serializable, PathMapper {
30  
31      private static final Pattern REDUNDANT_SLASHES = Pattern.compile("//+");
32      private static final String[] DEFAULT_KEYS = {"/", "*", "/*"};
33  
34      private final Map<String, Collection<String>> mappings = new HashMap<>();
35      private final Set<String> complexPaths = new HashSet<>();
36  
37      private final KeyMatcher matcher = new KeyMatcher();
38  
39      // common use of this class is reentrant read-mostly (quite expensive calls too)
40      // we don't want these reads to block each other so use a RWLock
41      private final ReadWriteLock lock = new ReentrantReadWriteLock();
42  
43      public void put(final String key, final String pattern) {
44          lock.writeLock().lock();
45          try {
46              if (pattern == null) {
47                  removeMappingsForKey(key);
48                  return;
49              }
50              addMapping(pattern, key);
51              if ((pattern.indexOf('?') > -1) || (pattern.contains("*") && (pattern.length() > 1))) {
52                  complexPaths.add(pattern);
53              }
54          } finally {
55              lock.writeLock().unlock();
56          }
57      }
58  
59      private void addMapping(String pattern, String key) {
60          mappings.computeIfAbsent(pattern, k -> new LinkedHashSet<>()).add(key);
61      }
62  
63      // must be called under lock
64      private void removeMappingsForKey(final String key) {
65          for (final Iterator<Map.Entry<String, Collection<String>>> it = mappings.entrySet().iterator(); it.hasNext(); ) {
66              final Map.Entry<String, Collection<String>> entry = it.next();
67              if (entry.getValue().remove(key) && entry.getValue().isEmpty()) {
68                  complexPaths.remove(entry.getKey());
69                  it.remove();
70              }
71          }
72      }
73  
74      public String get(String path) {
75          path = removeRedundantSlashes(path);
76          lock.readLock().lock();
77          try {
78              if (path == null) {
79                  path = "/";
80              }
81              final String mapped = matcher.findKey(path, mappings, complexPaths);
82              if (mapped == null) {
83                  return null;
84              }
85              Collection<String> keys = mappings.get(mapped);
86              if (keys.isEmpty()) {
87                  return null;
88              }
89              return keys.iterator().next();
90          } finally {
91              lock.readLock().unlock();
92          }
93      }
94  
95      /*
96       * @see com.atlassian.seraph.util.PathMapper#getAll(java.lang.String)
97       */
98      public Collection<String> getAll(String path) {
99          path = removeRedundantSlashes(path);
100         lock.readLock().lock();
101         try {
102             if (path == null) {
103                 path = "/";
104             }
105             final Set<String> matches = new LinkedHashSet<>();
106             // find exact keys
107             final String exactKey = matcher.findExactKey(path, mappings);
108             if (exactKey != null) {
109                 matches.addAll(mappings.get(exactKey));
110             }
111             // find complex keys
112             for (final String mapped : matcher.findComplexKeys(path, complexPaths)) {
113                 if (mappings.containsKey(mapped)) {
114                     matches.addAll(mappings.get(mapped));
115                 }
116             }
117             // find default keys
118             for (final String mapped : matcher.findDefaultKeys(mappings)) {
119                 matches.addAll(mappings.get(mapped));
120             }
121             return Collections.unmodifiableCollection(matches);
122         } finally {
123             lock.readLock().unlock();
124         }
125     }
126 
127     /**
128      * Reduces sequences of more than one consecutive forward slash ("/") to a
129      * single slash (see: https://studio.atlassian.com/browse/PLUG-597).
130      *
131      * @param path any string, including {@code null} (e.g. {@code "foo//bar"})
132      * @return the input string, with all sequences of more than one
133      * consecutive slash removed (e.g. {@code "foo/bar"})
134      */
135     protected String removeRedundantSlashes(final String path) {
136         return path == null ? null : REDUNDANT_SLASHES.matcher(path).replaceAll("/");
137     }
138 
139     public String toString() {
140         final StringBuilder sb = new StringBuilder(30 * (mappings.size() + complexPaths.size()));
141         sb.append("Mappings:\n");
142         for (final String key : mappings.keySet()) {
143             sb.append(key).append("=").append(mappings.get(key)).append("\n");
144         }
145         sb.append("Complex Paths:\n");
146         for (final String path : complexPaths) {
147             sb.append(path).append("\n");
148         }
149 
150         return sb.toString();
151     }
152 
153     private final class KeyMatcher {
154         /**
155          * Find exact key in mappings.
156          */
157         String findKey(final String path, final Map<String, Collection<String>> mappings, final Set<String> keys) {
158             String result = findExactKey(path, mappings);
159             if (result == null) {
160                 result = findComplexKey(path, keys);
161             }
162             if (result == null) {
163                 result = findDefaultKey(mappings);
164             }
165             return result;
166         }
167 
168         /**
169          * Check if path matches exact pattern ( /blah/blah.jsp ).
170          */
171         String findExactKey(final String path, final Map<String, Collection<String>> mappings) {
172             if (mappings.containsKey(path)) {
173                 return path;
174             }
175             return null;
176         }
177 
178         /**
179          * Find single matching complex key
180          */
181         String findComplexKey(final String path, final Set<String> complexPaths) {
182             // More than one key can match the path, so we take the most specific one
183             // (which should be the longest one), instead of the first one
184             String matchedKey = null;
185             int keyLength = 0;
186             for (String key : complexPaths) {
187                 if (match(key, path, false)) {
188                     if (key.length() > keyLength) {
189                         keyLength = key.length();
190                         matchedKey = key;
191                     }
192                 }
193             }
194             return matchedKey;
195         }
196 
197         /**
198          * Find all matching complex keys
199          */
200         Collection<String> findComplexKeys(final String path, final Set<String> complexPaths) {
201             final List<String> matches = new ArrayList<>();
202             for (String key : complexPaths) {
203                 if (match(key, path, false)) {
204                     matches.add(key);
205                 }
206             }
207             return matches;
208         }
209 
210         /**
211          * Look for root pattern ( / ).
212          */
213         String findDefaultKey(final Map<String, Collection<String>> mappings) {
214             for (int i = 0; i < DefaultPathMapper.DEFAULT_KEYS.length; i++) {
215                 if (mappings.containsKey(DefaultPathMapper.DEFAULT_KEYS[i])) {
216                     return DefaultPathMapper.DEFAULT_KEYS[i];
217                 }
218             }
219             return null;
220         }
221 
222         /**
223          * Look for root patterns ( / ).
224          */
225         Collection<String> findDefaultKeys(final Map<String, Collection<String>> mappings) {
226             final List<String> matches = new ArrayList<>();
227 
228             for (int i = 0; i < DefaultPathMapper.DEFAULT_KEYS.length; i++) {
229                 if (mappings.containsKey(DefaultPathMapper.DEFAULT_KEYS[i])) {
230                     matches.add(DefaultPathMapper.DEFAULT_KEYS[i]);
231                 }
232             }
233 
234             return matches;
235         }
236 
237         boolean match(final String pattern, final String str, final boolean isCaseSensitive) {
238             final char[] patArr = pattern.toCharArray();
239             final char[] strArr = str.toCharArray();
240             int patIdxStart = 0;
241             int patIdxEnd = patArr.length - 1;
242             int strIdxStart = 0;
243             int strIdxEnd = strArr.length - 1;
244             char ch;
245 
246             boolean containsStar = false;
247             for (int i = 0; i < patArr.length; i++) {
248                 if (patArr[i] == '*') {
249                     containsStar = true;
250                     break;
251                 }
252             }
253 
254             if (!containsStar) {
255                 // No '*'s, so we make a shortcut
256                 if (patIdxEnd != strIdxEnd) {
257                     return false; // Pattern and string do not have the same size
258                 }
259                 for (int i = 0; i <= patIdxEnd; i++) {
260                     ch = patArr[i];
261                     if (ch != '?') {
262                         if (isCaseSensitive && (ch != strArr[i])) {
263                             return false;// Character mismatch
264                         }
265                         if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[i]))) {
266                             return false; // Character mismatch
267                         }
268                     }
269                 }
270                 return true; // String matches against pattern
271             }
272 
273             if (patIdxEnd == 0) {
274                 return true; // Pattern contains only '*', which matches anything
275             }
276 
277             // Process characters before first star
278             while (((ch = patArr[patIdxStart]) != '*') && (strIdxStart <= strIdxEnd)) {
279                 if (ch != '?') {
280                     if (isCaseSensitive && (ch != strArr[strIdxStart])) {
281                         return false;// Character mismatch
282                     }
283                     if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart]))) {
284                         return false;// Character mismatch
285                     }
286                 }
287                 patIdxStart++;
288                 strIdxStart++;
289             }
290             if (strIdxStart > strIdxEnd) {
291                 // All characters in the string are used. Check if only '*'s are
292                 // left in the pattern. If so, we succeeded. Otherwise failure.
293                 for (int i = patIdxStart; i <= patIdxEnd; i++) {
294                     if (patArr[i] != '*') {
295                         return false;
296                     }
297                 }
298                 return true;
299             }
300 
301             // Process characters after last star
302             while (((ch = patArr[patIdxEnd]) != '*') && (strIdxStart <= strIdxEnd)) {
303                 if (ch != '?') {
304                     if (isCaseSensitive && (ch != strArr[strIdxEnd])) {
305                         return false;// Character mismatch
306                     }
307                     if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxEnd]))) {
308                         return false;// Character mismatch
309                     }
310                 }
311                 patIdxEnd--;
312                 strIdxEnd--;
313             }
314             if (strIdxStart > strIdxEnd) {
315                 // All characters in the string are used. Check if only '*'s are
316                 // left in the pattern. If so, we succeeded. Otherwise failure.
317                 for (int i = patIdxStart; i <= patIdxEnd; i++) {
318                     if (patArr[i] != '*') {
319                         return false;
320                     }
321                 }
322                 return true;
323             }
324 
325             // process pattern between stars. padIdxStart and patIdxEnd point
326             // always to a '*'.
327             while ((patIdxStart != patIdxEnd) && (strIdxStart <= strIdxEnd)) {
328                 int patIdxTmp = -1;
329                 for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
330                     if (patArr[i] == '*') {
331                         patIdxTmp = i;
332                         break;
333                     }
334                 }
335                 if (patIdxTmp == patIdxStart + 1) {
336                     // Two stars next to each other, skip the first one.
337                     patIdxStart++;
338                     continue;
339                 }
340                 // Find the pattern between padIdxStart & padIdxTmp in str between
341                 // strIdxStart & strIdxEnd
342                 final int patLength = (patIdxTmp - patIdxStart - 1);
343                 final int strLength = (strIdxEnd - strIdxStart + 1);
344                 int foundIdx = -1;
345                 strLoop:
346                 for (int i = 0; i <= strLength - patLength; i++) {
347                     for (int j = 0; j < patLength; j++) {
348                         ch = patArr[patIdxStart + j + 1];
349                         if (ch != '?') {
350                             if (isCaseSensitive && (ch != strArr[strIdxStart + i + j])) {
351                                 continue strLoop;
352                             }
353                             if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart + i + j]))) {
354                                 continue strLoop;
355                             }
356                         }
357                     }
358 
359                     foundIdx = strIdxStart + i;
360                     break;
361                 }
362 
363                 if (foundIdx == -1) {
364                     return false;
365                 }
366 
367                 patIdxStart = patIdxTmp;
368                 strIdxStart = foundIdx + patLength;
369             }
370 
371             // All characters in the string are used. Check if only '*'s are left
372             // in the pattern. If so, we succeeded. Otherwise failure.
373             for (int i = patIdxStart; i <= patIdxEnd; i++) {
374                 if (patArr[i] != '*') {
375                     return false;
376                 }
377             }
378             return true;
379         }
380     }
381 }