View Javadoc

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