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