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