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