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.  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      final 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 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     /** Return whether the path matches any entries in the PathMap,
329      * excluding the default entry
330      * @param path Path to match
331      * @return Whether the PathMap contains any entries that match this
332      */
333     public boolean containsMatch(String path)
334     {      
335     	Entry match = getMatch(path);
336     	return match!=null && !match.equals(_default);
337     }
338 
339     /* --------------------------------------------------------------- */  
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     public void clear()
368     {
369         _exactMap.clear();
370         _prefixMap.clear();
371         _suffixMap.clear();
372         _default=null;
373         _defaultSingletonList=null;
374         super.clear();
375     }
376     
377     /* --------------------------------------------------------------- */
378     /**
379      * @return true if match.
380      */
381     public static boolean match(String pathSpec, String path)
382         throws IllegalArgumentException
383     {
384         return match(pathSpec, path, false);
385     }
386 
387     /* --------------------------------------------------------------- */
388     /**
389      * @return true if match.
390      */
391     public static boolean match(String pathSpec, String path, boolean noDefault)
392     throws IllegalArgumentException
393     {
394         char c = pathSpec.charAt(0);
395         if (c=='/')
396         {
397             if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
398                 return true;
399             
400             if(isPathWildcardMatch(pathSpec, path))
401                 return true;
402         }
403         else if (c=='*')
404             return path.regionMatches(path.length()-pathSpec.length()+1,
405                                       pathSpec,1,pathSpec.length()-1);
406         return false;
407     }
408 
409     /* --------------------------------------------------------------- */
410     private static boolean isPathWildcardMatch(String pathSpec, String path)
411     {
412         // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
413         int cpl=pathSpec.length()-2;
414         if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
415         {
416             if (path.length()==cpl || '/'==path.charAt(cpl))
417                 return true;
418         }
419         return false;
420     }
421     
422     
423     /* --------------------------------------------------------------- */
424     /** Return the portion of a path that matches a path spec.
425      * @return null if no match at all.
426      */
427     public static String pathMatch(String pathSpec, String path)
428     {  
429         char c = pathSpec.charAt(0);
430         
431         if (c=='/')
432         {
433             if (pathSpec.length()==1)
434                 return path;
435         
436             if (pathSpec.equals(path))
437                 return path;
438             
439             if (isPathWildcardMatch(pathSpec, path))
440                 return path.substring(0,pathSpec.length()-2);
441         }
442         else if (c=='*')
443         {
444             if (path.regionMatches(path.length()-(pathSpec.length()-1),
445                                    pathSpec,1,pathSpec.length()-1))
446                 return path;
447         }
448         return null;
449     }
450     
451     /* --------------------------------------------------------------- */
452     /** Return the portion of a path that is after a path spec.
453      * @return The path info string
454      */
455     public static String pathInfo(String pathSpec, String path)
456     {
457         char c = pathSpec.charAt(0);
458         
459         if (c=='/')
460         {
461             if (pathSpec.length()==1)
462                 return null;
463             
464             boolean wildcard = isPathWildcardMatch(pathSpec, path);
465 
466             // handle the case where pathSpec uses a wildcard and path info is "/*"
467             if (pathSpec.equals(path) && !wildcard)
468                 return null;
469 
470             if (wildcard)
471             {
472                 if (path.length()==pathSpec.length()-2)
473                     return null;
474                 return path.substring(pathSpec.length()-2);
475             }
476         } 
477         return null;
478     }
479 
480 
481     /* ------------------------------------------------------------ */
482     /** Relative path.
483      * @param base The base the path is relative to.
484      * @param pathSpec The spec of the path segment to ignore.
485      * @param path the additional path
486      * @return base plus path with pathspec removed 
487      */
488     public static String relativePath(String base,
489                                       String pathSpec,
490                                       String path )
491     {
492         String info=pathInfo(pathSpec,path);
493         if (info==null)
494             info=path;
495 
496         if( info.startsWith( "./"))
497             info = info.substring( 2);
498         if( base.endsWith( URIUtil.SLASH))
499             if( info.startsWith( URIUtil.SLASH))
500                 path = base + info.substring(1);
501             else
502                 path = base + info;
503         else
504             if( info.startsWith( URIUtil.SLASH))
505                 path = base + info;
506             else
507                 path = base + URIUtil.SLASH + info;
508         return path;
509     }
510  
511     /* ------------------------------------------------------------ */
512     /* ------------------------------------------------------------ */
513     /* ------------------------------------------------------------ */
514     public static class Entry implements Map.Entry
515     {
516         private final Object key;
517         private final Object value;
518         private String mapped; 
519         private transient String string;
520 
521         Entry(Object key, Object value)
522         {
523             this.key=key;
524             this.value=value;
525         }
526 
527         public Object getKey()
528         {
529             return key;
530         }
531         
532         public Object getValue()
533         {
534             return value;
535         }
536 
537         public Object setValue(Object o)
538         {
539             throw new UnsupportedOperationException();
540         }
541 
542         public String toString()
543         {
544             if (string==null)
545                 string=key+"="+value;
546             return string;
547         }
548 
549         public String getMapped()
550         {
551             return mapped;
552         }
553 
554         void setMapped(String mapped)
555         {
556             this.mapped = mapped;
557         }
558     }
559 }