View Javadoc

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