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             // More than one key can match the path, so we take the most specific one
227             // (which should be the longest one), instead of the first one
228             String matchedKey = null;
229             int keyLength = 0;
230             for (String key : complexPaths)
231             {
232                 if (match(key, path, false))
233                 {
234                     if (key.length() > keyLength)
235                     {
236                         keyLength = key.length();
237                         matchedKey = key;
238                     }
239                 }
240             }
241             return matchedKey;
242         }
243 
244         /** Find all matching complex keys */
245         Collection<String> findComplexKeys(final String path, final Set<String> complexPaths)
246         {
247             final List<String> matches = new ArrayList<String>();
248             for (String key : complexPaths)
249             {
250                 if (match(key, path, false))
251                 {
252                     matches.add(key);
253                 }
254             }
255             return matches;
256         }
257 
258         /** Look for root pattern ( / ). */
259         String findDefaultKey(final Map<String, Collection<String>> mappings)
260         {
261             for (int i = 0; i < DefaultPathMapper.DEFAULT_KEYS.length; i++)
262             {
263                 if (mappings.containsKey(DefaultPathMapper.DEFAULT_KEYS[i]))
264                 {
265                     return DefaultPathMapper.DEFAULT_KEYS[i];
266                 }
267             }
268             return null;
269         }
270 
271         /** Look for root patterns ( / ). */
272         Collection<String> findDefaultKeys(final Map<String, Collection<String>> mappings)
273         {
274             final List<String> matches = new ArrayList<String>();
275 
276             for (int i = 0; i < DefaultPathMapper.DEFAULT_KEYS.length; i++)
277             {
278                 if (mappings.containsKey(DefaultPathMapper.DEFAULT_KEYS[i]))
279                 {
280                     matches.add(DefaultPathMapper.DEFAULT_KEYS[i]);
281                 }
282             }
283 
284             return matches;
285         }
286 
287         boolean match(final String pattern, final String str, final boolean isCaseSensitive)
288         {
289             final char[] patArr = pattern.toCharArray();
290             final char[] strArr = str.toCharArray();
291             int patIdxStart = 0;
292             int patIdxEnd = patArr.length - 1;
293             int strIdxStart = 0;
294             int strIdxEnd = strArr.length - 1;
295             char ch;
296 
297             boolean containsStar = false;
298             for (int i = 0; i < patArr.length; i++)
299             {
300                 if (patArr[i] == '*')
301                 {
302                     containsStar = true;
303                     break;
304                 }
305             }
306 
307             if (!containsStar)
308             {
309                 // No '*'s, so we make a shortcut
310                 if (patIdxEnd != strIdxEnd)
311                 {
312                     return false; // Pattern and string do not have the same size
313                 }
314                 for (int i = 0; i <= patIdxEnd; i++)
315                 {
316                     ch = patArr[i];
317                     if (ch != '?')
318                     {
319                         if (isCaseSensitive && (ch != strArr[i]))
320                         {
321                             return false;// Character mismatch
322                         }
323                         if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[i])))
324                         {
325                             return false; // Character mismatch
326                         }
327                     }
328                 }
329                 return true; // String matches against pattern
330             }
331 
332             if (patIdxEnd == 0)
333             {
334                 return true; // Pattern contains only '*', which matches anything
335             }
336 
337             // Process characters before first star
338             while (((ch = patArr[patIdxStart]) != '*') && (strIdxStart <= strIdxEnd))
339             {
340                 if (ch != '?')
341                 {
342                     if (isCaseSensitive && (ch != strArr[strIdxStart]))
343                     {
344                         return false;// Character mismatch
345                     }
346                     if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart])))
347                     {
348                         return false;// Character mismatch
349                     }
350                 }
351                 patIdxStart++;
352                 strIdxStart++;
353             }
354             if (strIdxStart > strIdxEnd)
355             {
356                 // All characters in the string are used. Check if only '*'s are
357                 // left in the pattern. If so, we succeeded. Otherwise failure.
358                 for (int i = patIdxStart; i <= patIdxEnd; i++)
359                 {
360                     if (patArr[i] != '*')
361                     {
362                         return false;
363                     }
364                 }
365                 return true;
366             }
367 
368             // Process characters after last star
369             while (((ch = patArr[patIdxEnd]) != '*') && (strIdxStart <= strIdxEnd))
370             {
371                 if (ch != '?')
372                 {
373                     if (isCaseSensitive && (ch != strArr[strIdxEnd]))
374                     {
375                         return false;// Character mismatch
376                     }
377                     if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxEnd])))
378                     {
379                         return false;// Character mismatch
380                     }
381                 }
382                 patIdxEnd--;
383                 strIdxEnd--;
384             }
385             if (strIdxStart > strIdxEnd)
386             {
387                 // All characters in the string are used. Check if only '*'s are
388                 // left in the pattern. If so, we succeeded. Otherwise failure.
389                 for (int i = patIdxStart; i <= patIdxEnd; i++)
390                 {
391                     if (patArr[i] != '*')
392                     {
393                         return false;
394                     }
395                 }
396                 return true;
397             }
398 
399             // process pattern between stars. padIdxStart and patIdxEnd point
400             // always to a '*'.
401             while ((patIdxStart != patIdxEnd) && (strIdxStart <= strIdxEnd))
402             {
403                 int patIdxTmp = -1;
404                 for (int i = patIdxStart + 1; i <= patIdxEnd; i++)
405                 {
406                     if (patArr[i] == '*')
407                     {
408                         patIdxTmp = i;
409                         break;
410                     }
411                 }
412                 if (patIdxTmp == patIdxStart + 1)
413                 {
414                     // Two stars next to each other, skip the first one.
415                     patIdxStart++;
416                     continue;
417                 }
418                 // Find the pattern between padIdxStart & padIdxTmp in str between
419                 // strIdxStart & strIdxEnd
420                 final int patLength = (patIdxTmp - patIdxStart - 1);
421                 final int strLength = (strIdxEnd - strIdxStart + 1);
422                 int foundIdx = -1;
423                 strLoop : for (int i = 0; i <= strLength - patLength; i++)
424                 {
425                     for (int j = 0; j < patLength; j++)
426                     {
427                         ch = patArr[patIdxStart + j + 1];
428                         if (ch != '?')
429                         {
430                             if (isCaseSensitive && (ch != strArr[strIdxStart + i + j]))
431                             {
432                                 continue strLoop;
433                             }
434                             if (!isCaseSensitive && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart + i + j])))
435                             {
436                                 continue strLoop;
437                             }
438                         }
439                     }
440 
441                     foundIdx = strIdxStart + i;
442                     break;
443                 }
444 
445                 if (foundIdx == -1)
446                 {
447                     return false;
448                 }
449 
450                 patIdxStart = patIdxTmp;
451                 strIdxStart = foundIdx + patLength;
452             }
453 
454             // All characters in the string are used. Check if only '*'s are left
455             // in the pattern. If so, we succeeded. Otherwise failure.
456             for (int i = patIdxStart; i <= patIdxEnd; i++)
457             {
458                 if (patArr[i] != '*')
459                 {
460                     return false;
461                 }
462             }
463             return true;
464         }
465     }
466 }