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