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