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