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