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.  The list is separated by the characters specified
47   * in the "org.eclipse.http.PathMap.separators" System property, which
48   * defaults to :
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 for get's.  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          System.getProperty("org.eclipse.http.PathMap.separators",":,");
64      
65      /* ------------------------------------------------------------ */
66      /** Set the path spec separator.
67       * Multiple path specification may be included in a single string
68       * if they are separated by the characters set in this string.
69       * The default value is ":," or whatever has been set by the
70       * system property org.eclipse.http.PathMap.separators
71       * @param s separators
72       */
73      public static void setPathSpecSeparators(String s)
74      {
75          __pathSpecSeparators=s;
76      }
77      
78      /* --------------------------------------------------------------- */
79      final StringMap _prefixMap=new StringMap();
80      final StringMap _suffixMap=new StringMap();
81      final StringMap _exactMap=new StringMap();
82  
83      List _defaultSingletonList=null;
84      Entry _prefixDefault=null;
85      Entry _default=null;
86      Set _entrySet;
87      boolean _nodefault=false;
88      
89      /* --------------------------------------------------------------- */
90      /** Construct empty PathMap.
91       */
92      public PathMap()
93      {
94          super(11);
95          _entrySet=entrySet();
96      }
97  
98      /* --------------------------------------------------------------- */
99      /** Construct empty PathMap.
100      */
101     public PathMap(boolean nodefault)
102     {
103         super(11);
104         _entrySet=entrySet();
105         _nodefault=nodefault;
106     }
107     
108     /* --------------------------------------------------------------- */
109     /** Construct empty PathMap.
110      */
111     public PathMap(int capacity)
112     {
113         super (capacity);
114         _entrySet=entrySet();
115     }
116     
117     /* --------------------------------------------------------------- */
118     /** Construct from dictionary PathMap.
119      */
120     public PathMap(Map m)
121     {
122         putAll(m);
123         _entrySet=entrySet();
124     }
125     
126     /* ------------------------------------------------------------ */
127     public void writeExternal(java.io.ObjectOutput out)
128         throws java.io.IOException
129     {
130         HashMap map = new HashMap(this);
131         out.writeObject(map);
132     }
133     
134     /* ------------------------------------------------------------ */
135     public void readExternal(java.io.ObjectInput in)
136         throws java.io.IOException, ClassNotFoundException
137     {
138         HashMap map = (HashMap)in.readObject();
139         this.putAll(map);
140     }
141     
142     /* --------------------------------------------------------------- */
143     /** Add a single path match to the PathMap.
144      * @param pathSpec The path specification, or comma separated list of
145      * path specifications.
146      * @param object The object the path maps to
147      */
148     public synchronized Object put(Object pathSpec, Object object)
149     {
150         StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
151         Object old =null;
152         
153         while (tok.hasMoreTokens())
154         {
155             String spec=tok.nextToken();
156             
157             if (!spec.startsWith("/") && !spec.startsWith("*."))
158                 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
159             
160             old = super.put(spec,object);
161             
162             // Make entry that was just created.
163             Entry entry = new Entry(spec,object);
164 
165             if (entry.getKey().equals(spec))
166             {
167                 if (spec.equals("/*"))
168                     _prefixDefault=entry;
169                 else if (spec.endsWith("/*"))
170                 {
171                     String mapped=spec.substring(0,spec.length()-2);
172                     entry.setMapped(mapped);
173                     _prefixMap.put(mapped,entry);
174                     _exactMap.put(mapped,entry);
175                     _exactMap.put(spec.substring(0,spec.length()-1),entry);
176                 }
177                 else if (spec.startsWith("*."))
178                     _suffixMap.put(spec.substring(2),entry);
179                 else if (spec.equals(URIUtil.SLASH))
180                 {    
181                     if (_nodefault)
182                         _exactMap.put(spec,entry);
183                     else
184                     {
185                         _default=entry;
186                         _defaultSingletonList=
187                             SingletonList.newSingletonList(_default);
188                     }
189                 }
190                 else
191                 {
192                     entry.setMapped(spec);
193                     _exactMap.put(spec,entry);
194                 }
195             }
196         }
197             
198         return old;
199     }
200 
201     /* ------------------------------------------------------------ */
202     /** Get object matched by the path.
203      * @param path the path.
204      * @return Best matched object or null.
205      */
206     public Object match(String path)
207     {
208         Map.Entry entry = getMatch(path);
209         if (entry!=null)
210             return entry.getValue();
211         return null;
212     }
213     
214     
215     /* --------------------------------------------------------------- */
216     /** Get the entry mapped by the best specification.
217      * @param path the path.
218      * @return Map.Entry of the best matched  or null.
219      */
220     public Entry getMatch(String path)
221     {
222         Map.Entry entry;
223 
224         if (path==null)
225             return null;
226         
227         int l=path.length();        
228 
229         // try exact match
230         entry=_exactMap.getEntry(path,0,l);
231         if (entry!=null)
232             return (Entry) entry.getValue();
233         
234         // prefix search
235         int i=l;
236         while((i=path.lastIndexOf('/',i-1))>=0)
237         {
238             entry=_prefixMap.getEntry(path,0,i);
239             if (entry!=null)
240                 return (Entry) entry.getValue();
241         }
242         
243         // Prefix Default
244         if (_prefixDefault!=null)
245             return _prefixDefault;
246         
247         // Extension search
248         i=0;
249         while ((i=path.indexOf('.',i+1))>0)
250         {
251             entry=_suffixMap.getEntry(path,i+1,l-i-1);
252             if (entry!=null)
253                 return (Entry) entry.getValue();
254         }        
255         
256         // Default
257         return _default;
258     }
259     
260     /* --------------------------------------------------------------- */
261     /** Get all entries matched by the path.
262      * Best match first.
263      * @param path Path to match
264      * @return LazyList of Map.Entry instances key=pathSpec
265      */
266     public Object getLazyMatches(String path)
267     {        
268         Map.Entry entry;
269         Object entries=null;
270 
271         if (path==null)
272             return LazyList.getList(entries);
273         
274         int l=path.length();
275 
276         // try exact match
277         entry=_exactMap.getEntry(path,0,l);
278         if (entry!=null)
279             entries=LazyList.add(entries,entry.getValue());
280         
281         // prefix search
282         int i=l-1;
283         while((i=path.lastIndexOf('/',i-1))>=0)
284         {
285             entry=_prefixMap.getEntry(path,0,i);
286             if (entry!=null)
287                 entries=LazyList.add(entries,entry.getValue());
288         }
289         
290         // Prefix Default
291         if (_prefixDefault!=null)
292             entries=LazyList.add(entries,_prefixDefault);
293         
294         // Extension search
295         i=0;
296         while ((i=path.indexOf('.',i+1))>0)
297         {
298             entry=_suffixMap.getEntry(path,i+1,l-i-1);
299             if (entry!=null)
300                 entries=LazyList.add(entries,entry.getValue());
301         }
302 
303         // Default
304         if (_default!=null)
305         {
306             // Optimization for just the default
307             if (entries==null)
308                 return _defaultSingletonList;
309             
310             entries=LazyList.add(entries,_default);
311         }
312         
313         return entries;
314     }
315     
316     /* --------------------------------------------------------------- */
317     /** Get all entries matched by the path.
318      * Best match first.
319      * @param path Path to match
320      * @return List of Map.Entry instances key=pathSpec
321      */
322     public List getMatches(String path)
323     {       
324         return LazyList.getList(getLazyMatches(path));
325     }
326 
327     /* --------------------------------------------------------------- */  
328     public synchronized Object remove(Object pathSpec)
329     {
330         if (pathSpec!=null)
331         {
332             String spec=(String) pathSpec;
333             if (spec.equals("/*"))
334                 _prefixDefault=null;
335             else if (spec.endsWith("/*"))
336             {
337                 _prefixMap.remove(spec.substring(0,spec.length()-2));
338                 _exactMap.remove(spec.substring(0,spec.length()-1));
339                 _exactMap.remove(spec.substring(0,spec.length()-2));
340             }
341             else if (spec.startsWith("*."))
342                 _suffixMap.remove(spec.substring(2));
343             else if (spec.equals(URIUtil.SLASH))
344             {
345                 _default=null;
346                 _defaultSingletonList=null;
347             }
348             else
349                 _exactMap.remove(spec);
350         }
351         return super.remove(pathSpec);
352     }
353     
354     /* --------------------------------------------------------------- */
355     public void clear()
356     {
357         _exactMap.clear();
358         _prefixMap.clear();
359         _suffixMap.clear();
360         _default=null;
361         _defaultSingletonList=null;
362         super.clear();
363     }
364     
365     /* --------------------------------------------------------------- */
366     /**
367      * @return true if match.
368      */
369     public static boolean match(String pathSpec, String path)
370         throws IllegalArgumentException
371     {
372         return match(pathSpec, path, false);
373     }
374 
375     /* --------------------------------------------------------------- */
376     /**
377      * @return true if match.
378      */
379     public static boolean match(String pathSpec, String path, boolean noDefault)
380     throws IllegalArgumentException
381     {
382         char c = pathSpec.charAt(0);
383         if (c=='/')
384         {
385             if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
386                 return true;
387             
388             if(isPathWildcardMatch(pathSpec, path))
389                 return true;
390         }
391         else if (c=='*')
392             return path.regionMatches(path.length()-pathSpec.length()+1,
393                                       pathSpec,1,pathSpec.length()-1);
394         return false;
395     }
396 
397     /* --------------------------------------------------------------- */
398     private static boolean isPathWildcardMatch(String pathSpec, String path)
399     {
400         // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
401         int cpl=pathSpec.length()-2;
402         if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
403         {
404             if (path.length()==cpl || '/'==path.charAt(cpl))
405                 return true;
406         }
407         return false;
408     }
409     
410     
411     /* --------------------------------------------------------------- */
412     /** Return the portion of a path that matches a path spec.
413      * @return null if no match at all.
414      */
415     public static String pathMatch(String pathSpec, String path)
416     {  
417         char c = pathSpec.charAt(0);
418         
419         if (c=='/')
420         {
421             if (pathSpec.length()==1)
422                 return path;
423         
424             if (pathSpec.equals(path))
425                 return path;
426             
427             if (isPathWildcardMatch(pathSpec, path))
428                 return path.substring(0,pathSpec.length()-2);
429         }
430         else if (c=='*')
431         {
432             if (path.regionMatches(path.length()-(pathSpec.length()-1),
433                                    pathSpec,1,pathSpec.length()-1))
434                 return path;
435         }
436         return null;
437     }
438     
439     /* --------------------------------------------------------------- */
440     /** Return the portion of a path that is after a path spec.
441      * @return The path info string
442      */
443     public static String pathInfo(String pathSpec, String path)
444     {
445         char c = pathSpec.charAt(0);
446         
447         if (c=='/')
448         {
449             if (pathSpec.length()==1)
450                 return null;
451             
452             if (pathSpec.equals(path))
453                 return null;
454 
455             if (isPathWildcardMatch(pathSpec, path))
456             {
457                 if (path.length()==pathSpec.length()-2)
458                     return null;
459                 return path.substring(pathSpec.length()-2);
460             }
461         } 
462         return null;
463     }
464 
465 
466     /* ------------------------------------------------------------ */
467     /** Relative path.
468      * @param base The base the path is relative to.
469      * @param pathSpec The spec of the path segment to ignore.
470      * @param path the additional path
471      * @return base plus path with pathspec removed 
472      */
473     public static String relativePath(String base,
474                                       String pathSpec,
475                                       String path )
476     {
477         String info=pathInfo(pathSpec,path);
478         if (info==null)
479             info=path;
480 
481         if( info.startsWith( "./"))
482             info = info.substring( 2);
483         if( base.endsWith( URIUtil.SLASH))
484             if( info.startsWith( URIUtil.SLASH))
485                 path = base + info.substring(1);
486             else
487                 path = base + info;
488         else
489             if( info.startsWith( URIUtil.SLASH))
490                 path = base + info;
491             else
492                 path = base + URIUtil.SLASH + info;
493         return path;
494     }
495  
496     /* ------------------------------------------------------------ */
497     /* ------------------------------------------------------------ */
498     /* ------------------------------------------------------------ */
499     public static class Entry implements Map.Entry
500     {
501         private Object key;
502         private Object value;
503         private String mapped; 
504         private transient String string;
505 
506         Entry(Object key, Object value)
507         {
508             this.key=key;
509             this.value=value;
510         }
511 
512         public Object getKey()
513         {
514             return key;
515         }
516         
517         public Object getValue()
518         {
519             return value;
520         }
521 
522         public Object setValue(Object o)
523         {
524             throw new UnsupportedOperationException();
525         }
526 
527         public String toString()
528         {
529             if (string==null)
530                 string=key+"="+value;
531             return string;
532         }
533 
534         public String getMapped()
535         {
536             return mapped;
537         }
538 
539         void setMapped(String mapped)
540         {
541             this.mapped = mapped;
542         }
543     }
544 }