View Javadoc

1   // ========================================================================
2   // Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.http;
15  
16  import java.io.Externalizable;
17  import java.util.HashMap;
18  import java.util.List;
19  import java.util.Map;
20  import java.util.Set;
21  import java.util.StringTokenizer;
22  
23  import org.eclipse.jetty.util.LazyList;
24  import org.eclipse.jetty.util.SingletonList;
25  import org.eclipse.jetty.util.StringMap;
26  import org.eclipse.jetty.util.URIUtil;
27  
28  /* ------------------------------------------------------------ */
29  /** URI path map to Object.
30   * This mapping implements the path specification recommended
31   * in the 2.2 Servlet API.
32   *
33   * Path specifications can be of the following forms:<PRE>
34   * /foo/bar           - an exact path specification.
35   * /foo/*             - a prefix path specification (must end '/*').
36   * *.ext              - a suffix path specification.
37   * /                  - the default path specification.       
38   * </PRE>
39   * Matching is performed in the following order <NL>
40   * <LI>Exact match.
41   * <LI>Longest prefix match.
42   * <LI>Longest suffix match.
43   * <LI>default.
44   * </NL>
45   * Multiple path specifications can be mapped by providing a list of
46   * specifications.  By default this class uses characters ":," as path
47   * separators, unless configured differently by calling the static 
48   * method @see PathMap#setPathSpecSeparators(String) 
49   * <P>
50   * Special characters within paths such as '?� and ';' are not treated specially
51   * as it is assumed they would have been either encoded in the original URL or 
52   * stripped from the path.
53   * <P>
54   * This class is not synchronized.  If concurrent modifications are
55   * possible then it should be synchronized at a higher level.
56   *
57   * 
58   */
59  public class PathMap extends HashMap implements Externalizable
60  {
61      /* ------------------------------------------------------------ */
62      private static String __pathSpecSeparators = ":,";
63      
64      /* ------------------------------------------------------------ */
65      /** Set the path spec separator.
66       * Multiple path specification may be included in a single string
67       * if they are separated by the characters set in this string.
68       * By default this class uses ":," characters as path separators.
69       * @param s separators
70       */
71      public static void setPathSpecSeparators(String s)
72      {
73          __pathSpecSeparators=s;
74      }
75      
76      /* --------------------------------------------------------------- */
77      final StringMap _prefixMap=new StringMap();
78      final StringMap _suffixMap=new StringMap();
79      final StringMap _exactMap=new StringMap();
80  
81      List _defaultSingletonList=null;
82      Entry _prefixDefault=null;
83      Entry _default=null;
84      final Set _entrySet;
85      boolean _nodefault=false;
86      
87      /* --------------------------------------------------------------- */
88      /** Construct empty PathMap.
89       */
90      public PathMap()
91      {
92          super(11);
93          _entrySet=entrySet();
94      }
95  
96      /* --------------------------------------------------------------- */
97      /** Construct empty PathMap.
98       */
99      public PathMap(boolean nodefault)
100     {
101         super(11);
102         _entrySet=entrySet();
103         _nodefault=nodefault;
104     }
105     
106     /* --------------------------------------------------------------- */
107     /** Construct empty PathMap.
108      */
109     public PathMap(int capacity)
110     {
111         super (capacity);
112         _entrySet=entrySet();
113     }
114     
115     /* --------------------------------------------------------------- */
116     /** Construct from dictionary PathMap.
117      */
118     public PathMap(Map m)
119     {
120         putAll(m);
121         _entrySet=entrySet();
122     }
123     
124     /* ------------------------------------------------------------ */
125     public void writeExternal(java.io.ObjectOutput out)
126         throws java.io.IOException
127     {
128         HashMap map = new HashMap(this);
129         out.writeObject(map);
130     }
131     
132     /* ------------------------------------------------------------ */
133     public void readExternal(java.io.ObjectInput in)
134         throws java.io.IOException, ClassNotFoundException
135     {
136         HashMap map = (HashMap)in.readObject();
137         this.putAll(map);
138     }
139     
140     /* --------------------------------------------------------------- */
141     /** Add a single path match to the PathMap.
142      * @param pathSpec The path specification, or comma separated list of
143      * path specifications.
144      * @param object The object the path maps to
145      */
146     public Object put(Object pathSpec, Object object)
147     {
148         StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
149         Object old =null;
150         
151         while (tok.hasMoreTokens())
152         {
153             String spec=tok.nextToken();
154             
155             if (!spec.startsWith("/") && !spec.startsWith("*."))
156                 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
157             
158             old = super.put(spec,object);
159             
160             // Make entry that was just created.
161             Entry entry = new Entry(spec,object);
162 
163             if (entry.getKey().equals(spec))
164             {
165                 if (spec.equals("/*"))
166                     _prefixDefault=entry;
167                 else if (spec.endsWith("/*"))
168                 {
169                     String mapped=spec.substring(0,spec.length()-2);
170                     entry.setMapped(mapped);
171                     _prefixMap.put(mapped,entry);
172                     _exactMap.put(mapped,entry);
173                     _exactMap.put(spec.substring(0,spec.length()-1),entry);
174                 }
175                 else if (spec.startsWith("*."))
176                     _suffixMap.put(spec.substring(2),entry);
177                 else if (spec.equals(URIUtil.SLASH))
178                 {    
179                     if (_nodefault)
180                         _exactMap.put(spec,entry);
181                     else
182                     {
183                         _default=entry;
184                         _defaultSingletonList=
185                             SingletonList.newSingletonList(_default);
186                     }
187                 }
188                 else
189                 {
190                     entry.setMapped(spec);
191                     _exactMap.put(spec,entry);
192                 }
193             }
194         }
195             
196         return old;
197     }
198 
199     /* ------------------------------------------------------------ */
200     /** Get object matched by the path.
201      * @param path the path.
202      * @return Best matched object or null.
203      */
204     public Object match(String path)
205     {
206         Map.Entry entry = getMatch(path);
207         if (entry!=null)
208             return entry.getValue();
209         return null;
210     }
211     
212     
213     /* --------------------------------------------------------------- */
214     /** Get the entry mapped by the best specification.
215      * @param path the path.
216      * @return Map.Entry of the best matched  or null.
217      */
218     public Entry getMatch(String path)
219     {
220         Map.Entry entry;
221 
222         if (path==null)
223             return null;
224         
225         int l=path.length();        
226 
227         // try exact match
228         entry=_exactMap.getEntry(path,0,l);
229         if (entry!=null)
230             return (Entry) entry.getValue();
231         
232         // prefix search
233         int i=l;
234         while((i=path.lastIndexOf('/',i-1))>=0)
235         {
236             entry=_prefixMap.getEntry(path,0,i);
237             if (entry!=null)
238                 return (Entry) entry.getValue();
239         }
240         
241         // Prefix Default
242         if (_prefixDefault!=null)
243             return _prefixDefault;
244         
245         // Extension search
246         i=0;
247         while ((i=path.indexOf('.',i+1))>0)
248         {
249             entry=_suffixMap.getEntry(path,i+1,l-i-1);
250             if (entry!=null)
251                 return (Entry) entry.getValue();
252         }        
253         
254         // Default
255         return _default;
256     }
257     
258     /* --------------------------------------------------------------- */
259     /** Get all entries matched by the path.
260      * Best match first.
261      * @param path Path to match
262      * @return LazyList of Map.Entry instances key=pathSpec
263      */
264     public Object getLazyMatches(String path)
265     {        
266         Map.Entry entry;
267         Object entries=null;
268 
269         if (path==null)
270             return LazyList.getList(entries);
271         
272         int l=path.length();
273 
274         // try exact match
275         entry=_exactMap.getEntry(path,0,l);
276         if (entry!=null)
277             entries=LazyList.add(entries,entry.getValue());
278         
279         // prefix search
280         int i=l-1;
281         while((i=path.lastIndexOf('/',i-1))>=0)
282         {
283             entry=_prefixMap.getEntry(path,0,i);
284             if (entry!=null)
285                 entries=LazyList.add(entries,entry.getValue());
286         }
287         
288         // Prefix Default
289         if (_prefixDefault!=null)
290             entries=LazyList.add(entries,_prefixDefault);
291         
292         // Extension search
293         i=0;
294         while ((i=path.indexOf('.',i+1))>0)
295         {
296             entry=_suffixMap.getEntry(path,i+1,l-i-1);
297             if (entry!=null)
298                 entries=LazyList.add(entries,entry.getValue());
299         }
300 
301         // Default
302         if (_default!=null)
303         {
304             // Optimization for just the default
305             if (entries==null)
306                 return _defaultSingletonList;
307             
308             entries=LazyList.add(entries,_default);
309         }
310         
311         return entries;
312     }
313     
314     /* --------------------------------------------------------------- */
315     /** Get all entries matched by the path.
316      * Best match first.
317      * @param path Path to match
318      * @return List of Map.Entry instances key=pathSpec
319      */
320     public List getMatches(String path)
321     {       
322         return LazyList.getList(getLazyMatches(path));
323     }
324 
325     /* --------------------------------------------------------------- */
326     /** Return whether the path matches any entries in the PathMap,
327      * excluding the default entry
328      * @param path Path to match
329      * @return Whether the PathMap contains any entries that match this
330      */
331     public boolean containsMatch(String path)
332     {      
333     	Entry match = getMatch(path);
334     	return match!=null && !match.equals(_default);
335     }
336 
337     /* --------------------------------------------------------------- */  
338     public Object remove(Object pathSpec)
339     {
340         if (pathSpec!=null)
341         {
342             String spec=(String) pathSpec;
343             if (spec.equals("/*"))
344                 _prefixDefault=null;
345             else if (spec.endsWith("/*"))
346             {
347                 _prefixMap.remove(spec.substring(0,spec.length()-2));
348                 _exactMap.remove(spec.substring(0,spec.length()-1));
349                 _exactMap.remove(spec.substring(0,spec.length()-2));
350             }
351             else if (spec.startsWith("*."))
352                 _suffixMap.remove(spec.substring(2));
353             else if (spec.equals(URIUtil.SLASH))
354             {
355                 _default=null;
356                 _defaultSingletonList=null;
357             }
358             else
359                 _exactMap.remove(spec);
360         }
361         return super.remove(pathSpec);
362     }
363     
364     /* --------------------------------------------------------------- */
365     public void clear()
366     {
367         _exactMap.clear();
368         _prefixMap.clear();
369         _suffixMap.clear();
370         _default=null;
371         _defaultSingletonList=null;
372         super.clear();
373     }
374     
375     /* --------------------------------------------------------------- */
376     /**
377      * @return true if match.
378      */
379     public static boolean match(String pathSpec, String path)
380         throws IllegalArgumentException
381     {
382         return match(pathSpec, path, false);
383     }
384 
385     /* --------------------------------------------------------------- */
386     /**
387      * @return true if match.
388      */
389     public static boolean match(String pathSpec, String path, boolean noDefault)
390     throws IllegalArgumentException
391     {
392         char c = pathSpec.charAt(0);
393         if (c=='/')
394         {
395             if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
396                 return true;
397             
398             if(isPathWildcardMatch(pathSpec, path))
399                 return true;
400         }
401         else if (c=='*')
402             return path.regionMatches(path.length()-pathSpec.length()+1,
403                                       pathSpec,1,pathSpec.length()-1);
404         return false;
405     }
406 
407     /* --------------------------------------------------------------- */
408     private static boolean isPathWildcardMatch(String pathSpec, String path)
409     {
410         // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
411         int cpl=pathSpec.length()-2;
412         if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
413         {
414             if (path.length()==cpl || '/'==path.charAt(cpl))
415                 return true;
416         }
417         return false;
418     }
419     
420     
421     /* --------------------------------------------------------------- */
422     /** Return the portion of a path that matches a path spec.
423      * @return null if no match at all.
424      */
425     public static String pathMatch(String pathSpec, String path)
426     {  
427         char c = pathSpec.charAt(0);
428         
429         if (c=='/')
430         {
431             if (pathSpec.length()==1)
432                 return path;
433         
434             if (pathSpec.equals(path))
435                 return path;
436             
437             if (isPathWildcardMatch(pathSpec, path))
438                 return path.substring(0,pathSpec.length()-2);
439         }
440         else if (c=='*')
441         {
442             if (path.regionMatches(path.length()-(pathSpec.length()-1),
443                                    pathSpec,1,pathSpec.length()-1))
444                 return path;
445         }
446         return null;
447     }
448     
449     /* --------------------------------------------------------------- */
450     /** Return the portion of a path that is after a path spec.
451      * @return The path info string
452      */
453     public static String pathInfo(String pathSpec, String path)
454     {
455         char c = pathSpec.charAt(0);
456         
457         if (c=='/')
458         {
459             if (pathSpec.length()==1)
460                 return null;
461             
462             boolean wildcard = isPathWildcardMatch(pathSpec, path);
463 
464             // handle the case where pathSpec uses a wildcard and path info is "/*"
465             if (pathSpec.equals(path) && !wildcard)
466                 return null;
467 
468             if (wildcard)
469             {
470                 if (path.length()==pathSpec.length()-2)
471                     return null;
472                 return path.substring(pathSpec.length()-2);
473             }
474         } 
475         return null;
476     }
477 
478 
479     /* ------------------------------------------------------------ */
480     /** Relative path.
481      * @param base The base the path is relative to.
482      * @param pathSpec The spec of the path segment to ignore.
483      * @param path the additional path
484      * @return base plus path with pathspec removed 
485      */
486     public static String relativePath(String base,
487                                       String pathSpec,
488                                       String path )
489     {
490         String info=pathInfo(pathSpec,path);
491         if (info==null)
492             info=path;
493 
494         if( info.startsWith( "./"))
495             info = info.substring( 2);
496         if( base.endsWith( URIUtil.SLASH))
497             if( info.startsWith( URIUtil.SLASH))
498                 path = base + info.substring(1);
499             else
500                 path = base + info;
501         else
502             if( info.startsWith( URIUtil.SLASH))
503                 path = base + info;
504             else
505                 path = base + URIUtil.SLASH + info;
506         return path;
507     }
508  
509     /* ------------------------------------------------------------ */
510     /* ------------------------------------------------------------ */
511     /* ------------------------------------------------------------ */
512     public static class Entry implements Map.Entry
513     {
514         private final Object key;
515         private final Object value;
516         private String mapped; 
517         private transient String string;
518 
519         Entry(Object key, Object value)
520         {
521             this.key=key;
522             this.value=value;
523         }
524 
525         public Object getKey()
526         {
527             return key;
528         }
529         
530         public Object getValue()
531         {
532             return value;
533         }
534 
535         public Object setValue(Object o)
536         {
537             throw new UnsupportedOperationException();
538         }
539 
540         public String toString()
541         {
542             if (string==null)
543                 string=key+"="+value;
544             return string;
545         }
546 
547         public String getMapped()
548         {
549             return mapped;
550         }
551 
552         void setMapped(String mapped)
553         {
554             this.mapped = mapped;
555         }
556     }
557 }