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     @Override
147     public Object put(Object pathSpec, Object object)
148     {
149         StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
150         Object old =null;
151         
152         while (tok.hasMoreTokens())
153         {
154             String spec=tok.nextToken();
155             
156             if (!spec.startsWith("/") && !spec.startsWith("*."))
157                 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
158             
159             old = super.put(spec,object);
160             
161             // Make entry that was just created.
162             Entry entry = new Entry(spec,object);
163 
164             if (entry.getKey().equals(spec))
165             {
166                 if (spec.equals("/*"))
167                     _prefixDefault=entry;
168                 else if (spec.endsWith("/*"))
169                 {
170                     String mapped=spec.substring(0,spec.length()-2);
171                     entry.setMapped(mapped);
172                     _prefixMap.put(mapped,entry);
173                     _exactMap.put(mapped,entry);
174                     _exactMap.put(spec.substring(0,spec.length()-1),entry);
175                 }
176                 else if (spec.startsWith("*."))
177                     _suffixMap.put(spec.substring(2),entry);
178                 else if (spec.equals(URIUtil.SLASH))
179                 {    
180                     if (_nodefault)
181                         _exactMap.put(spec,entry);
182                     else
183                     {
184                         _default=entry;
185                         _defaultSingletonList=
186                             SingletonList.newSingletonList(_default);
187                     }
188                 }
189                 else
190                 {
191                     entry.setMapped(spec);
192                     _exactMap.put(spec,entry);
193                 }
194             }
195         }
196             
197         return old;
198     }
199 
200     /* ------------------------------------------------------------ */
201     /** Get object matched by the path.
202      * @param path the path.
203      * @return Best matched object or null.
204      */
205     public Object match(String path)
206     {
207         Map.Entry entry = getMatch(path);
208         if (entry!=null)
209             return entry.getValue();
210         return null;
211     }
212     
213     
214     /* --------------------------------------------------------------- */
215     /** Get the entry mapped by the best specification.
216      * @param path the path.
217      * @return Map.Entry of the best matched  or null.
218      */
219     public Entry getMatch(String path)
220     {
221         Map.Entry entry;
222 
223         if (path==null)
224             return null;
225         
226         int l=path.length();        
227 
228         // try exact match
229         entry=_exactMap.getEntry(path,0,l);
230         if (entry!=null)
231             return (Entry) entry.getValue();
232         
233         // prefix search
234         int i=l;
235         while((i=path.lastIndexOf('/',i-1))>=0)
236         {
237             entry=_prefixMap.getEntry(path,0,i);
238             if (entry!=null)
239                 return (Entry) entry.getValue();
240         }
241         
242         // Prefix Default
243         if (_prefixDefault!=null)
244             return _prefixDefault;
245         
246         // Extension search
247         i=0;
248         while ((i=path.indexOf('.',i+1))>0)
249         {
250             entry=_suffixMap.getEntry(path,i+1,l-i-1);
251             if (entry!=null)
252                 return (Entry) entry.getValue();
253         }        
254         
255         // Default
256         return _default;
257     }
258     
259     /* --------------------------------------------------------------- */
260     /** Get all entries matched by the path.
261      * Best match first.
262      * @param path Path to match
263      * @return LazyList of Map.Entry instances key=pathSpec
264      */
265     public Object getLazyMatches(String path)
266     {        
267         Map.Entry entry;
268         Object entries=null;
269 
270         if (path==null)
271             return LazyList.getList(entries);
272         
273         int l=path.length();
274 
275         // try exact match
276         entry=_exactMap.getEntry(path,0,l);
277         if (entry!=null)
278             entries=LazyList.add(entries,entry.getValue());
279         
280         // prefix search
281         int i=l-1;
282         while((i=path.lastIndexOf('/',i-1))>=0)
283         {
284             entry=_prefixMap.getEntry(path,0,i);
285             if (entry!=null)
286                 entries=LazyList.add(entries,entry.getValue());
287         }
288         
289         // Prefix Default
290         if (_prefixDefault!=null)
291             entries=LazyList.add(entries,_prefixDefault);
292         
293         // Extension search
294         i=0;
295         while ((i=path.indexOf('.',i+1))>0)
296         {
297             entry=_suffixMap.getEntry(path,i+1,l-i-1);
298             if (entry!=null)
299                 entries=LazyList.add(entries,entry.getValue());
300         }
301 
302         // Default
303         if (_default!=null)
304         {
305             // Optimization for just the default
306             if (entries==null)
307                 return _defaultSingletonList;
308             
309             entries=LazyList.add(entries,_default);
310         }
311         
312         return entries;
313     }
314     
315     /* --------------------------------------------------------------- */
316     /** Get all entries matched by the path.
317      * Best match first.
318      * @param path Path to match
319      * @return List of Map.Entry instances key=pathSpec
320      */
321     public List getMatches(String path)
322     {       
323         return LazyList.getList(getLazyMatches(path));
324     }
325 
326     /* --------------------------------------------------------------- */
327     /** Return whether the path matches any entries in the PathMap,
328      * excluding the default entry
329      * @param path Path to match
330      * @return Whether the PathMap contains any entries that match this
331      */
332     public boolean containsMatch(String path)
333     {      
334     	Entry match = getMatch(path);
335     	return match!=null && !match.equals(_default);
336     }
337 
338     /* --------------------------------------------------------------- */  
339     @Override
340     public Object remove(Object pathSpec)
341     {
342         if (pathSpec!=null)
343         {
344             String spec=(String) pathSpec;
345             if (spec.equals("/*"))
346                 _prefixDefault=null;
347             else if (spec.endsWith("/*"))
348             {
349                 _prefixMap.remove(spec.substring(0,spec.length()-2));
350                 _exactMap.remove(spec.substring(0,spec.length()-1));
351                 _exactMap.remove(spec.substring(0,spec.length()-2));
352             }
353             else if (spec.startsWith("*."))
354                 _suffixMap.remove(spec.substring(2));
355             else if (spec.equals(URIUtil.SLASH))
356             {
357                 _default=null;
358                 _defaultSingletonList=null;
359             }
360             else
361                 _exactMap.remove(spec);
362         }
363         return super.remove(pathSpec);
364     }
365     
366     /* --------------------------------------------------------------- */
367     @Override
368     public void clear()
369     {
370         _exactMap.clear();
371         _prefixMap.clear();
372         _suffixMap.clear();
373         _default=null;
374         _defaultSingletonList=null;
375         super.clear();
376     }
377     
378     /* --------------------------------------------------------------- */
379     /**
380      * @return true if match.
381      */
382     public static boolean match(String pathSpec, String path)
383         throws IllegalArgumentException
384     {
385         return match(pathSpec, path, false);
386     }
387 
388     /* --------------------------------------------------------------- */
389     /**
390      * @return true if match.
391      */
392     public static boolean match(String pathSpec, String path, boolean noDefault)
393     throws IllegalArgumentException
394     {
395         char c = pathSpec.charAt(0);
396         if (c=='/')
397         {
398             if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
399                 return true;
400             
401             if(isPathWildcardMatch(pathSpec, path))
402                 return true;
403         }
404         else if (c=='*')
405             return path.regionMatches(path.length()-pathSpec.length()+1,
406                                       pathSpec,1,pathSpec.length()-1);
407         return false;
408     }
409 
410     /* --------------------------------------------------------------- */
411     private static boolean isPathWildcardMatch(String pathSpec, String path)
412     {
413         // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
414         int cpl=pathSpec.length()-2;
415         if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
416         {
417             if (path.length()==cpl || '/'==path.charAt(cpl))
418                 return true;
419         }
420         return false;
421     }
422     
423     
424     /* --------------------------------------------------------------- */
425     /** Return the portion of a path that matches a path spec.
426      * @return null if no match at all.
427      */
428     public static String pathMatch(String pathSpec, String path)
429     {  
430         char c = pathSpec.charAt(0);
431         
432         if (c=='/')
433         {
434             if (pathSpec.length()==1)
435                 return path;
436         
437             if (pathSpec.equals(path))
438                 return path;
439             
440             if (isPathWildcardMatch(pathSpec, path))
441                 return path.substring(0,pathSpec.length()-2);
442         }
443         else if (c=='*')
444         {
445             if (path.regionMatches(path.length()-(pathSpec.length()-1),
446                                    pathSpec,1,pathSpec.length()-1))
447                 return path;
448         }
449         return null;
450     }
451     
452     /* --------------------------------------------------------------- */
453     /** Return the portion of a path that is after a path spec.
454      * @return The path info string
455      */
456     public static String pathInfo(String pathSpec, String path)
457     {
458         char c = pathSpec.charAt(0);
459         
460         if (c=='/')
461         {
462             if (pathSpec.length()==1)
463                 return null;
464             
465             boolean wildcard = isPathWildcardMatch(pathSpec, path);
466 
467             // handle the case where pathSpec uses a wildcard and path info is "/*"
468             if (pathSpec.equals(path) && !wildcard)
469                 return null;
470 
471             if (wildcard)
472             {
473                 if (path.length()==pathSpec.length()-2)
474                     return null;
475                 return path.substring(pathSpec.length()-2);
476             }
477         } 
478         return null;
479     }
480 
481 
482     /* ------------------------------------------------------------ */
483     /** Relative path.
484      * @param base The base the path is relative to.
485      * @param pathSpec The spec of the path segment to ignore.
486      * @param path the additional path
487      * @return base plus path with pathspec removed 
488      */
489     public static String relativePath(String base,
490                                       String pathSpec,
491                                       String path )
492     {
493         String info=pathInfo(pathSpec,path);
494         if (info==null)
495             info=path;
496 
497         if( info.startsWith( "./"))
498             info = info.substring( 2);
499         if( base.endsWith( URIUtil.SLASH))
500             if( info.startsWith( URIUtil.SLASH))
501                 path = base + info.substring(1);
502             else
503                 path = base + info;
504         else
505             if( info.startsWith( URIUtil.SLASH))
506                 path = base + info;
507             else
508                 path = base + URIUtil.SLASH + info;
509         return path;
510     }
511  
512     /* ------------------------------------------------------------ */
513     /* ------------------------------------------------------------ */
514     /* ------------------------------------------------------------ */
515     public static class Entry implements Map.Entry
516     {
517         private final Object key;
518         private final Object value;
519         private String mapped; 
520         private transient String string;
521 
522         Entry(Object key, Object value)
523         {
524             this.key=key;
525             this.value=value;
526         }
527 
528         public Object getKey()
529         {
530             return key;
531         }
532         
533         public Object getValue()
534         {
535             return value;
536         }
537 
538         public Object setValue(Object o)
539         {
540             throw new UnsupportedOperationException();
541         }
542 
543         @Override
544         public String toString()
545         {
546             if (string==null)
547                 string=key+"="+value;
548             return string;
549         }
550 
551         public String getMapped()
552         {
553             return mapped;
554         }
555 
556         void setMapped(String mapped)
557         {
558             this.mapped = mapped;
559         }
560     }
561 }