View Javadoc

    1   // ========================================================================
    2   // Copyright 1999-2005 Mort Bay Consulting Pty. Ltd.
    3   // ------------------------------------------------------------------------
    4   // Licensed under the Apache License, Version 2.0 (the "License");
    5   // you may not use this file except in compliance with the License.
    6   // You may obtain a copy of the License at
    7   // http://www.apache.org/licenses/LICENSE-2.0
    8   // Unless required by applicable law or agreed to in writing, software
    9   // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  // ========================================================================
    14
    15  package org.mortbay.jetty.servlet;
    16
    17  import java.io.Externalizable;
    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.mortbay.util.LazyList;
    25  import org.mortbay.util.SingletonList;
    26  import org.mortbay.util.StringMap;
    27  import org.mortbay.util.URIUtil;
    28
    29  /* ------------------------------------------------------------ */
    30  /** URI path map to Object.
    31   * This mapping implements the path specification recommended
    32   * in the 2.2 Servlet API.
    33   *
    34   * Path specifications can be of the following forms:<PRE>
    35   * /foo/bar           - an exact path specification.
    36   * /foo/*             - a prefix path specification (must end '/*').
    37   * *.ext              - a suffix path specification.
    38   * /                  - the default path specification.
    39   * </PRE>
    40   * Matching is performed in the following order <NL>
    41   * <LI>Exact match.
    42   * <LI>Longest prefix match.
    43   * <LI>Longest suffix match.
    44   * <LI>default.
    45   * </NL>
    46   * Multiple path specifications can be mapped by providing a list of
    47   * specifications.  The list is separated by the characters specified
    48   * in the "org.mortbay.http.PathMap.separators" System property, which
    49   * defaults to :
    50   * <P>
    51   * Special characters within paths such as '?� and ';' are not treated specially
    52   * as it is assumed they would have been either encoded in the original URL or
    53   * stripped from the path.
    54   * <P>
    55   * This class is not synchronized for get's.  If concurrent modifications are
    56   * possible then it should be synchronized at a higher level.
    57   *
    58   * @author Greg Wilkins (gregw)
    59   */
    60  public class PathMap extends HashMap implements Externalizable
    61  {
    62      /* ------------------------------------------------------------ */
    63      private static String __pathSpecSeparators =
    64          System.getProperty("org.mortbay.http.PathMap.separators",":,");
    65
    66      /* ------------------------------------------------------------ */
    67      /** Set the path spec separator.
    68       * Multiple path specification may be included in a single string
    69       * if they are separated by the characters set in this string.
    70       * The default value is ":," or whatever has been set by the
    71       * system property org.mortbay.http.PathMap.separators
    72       * @param s separators
    73       */
    74      public static void setPathSpecSeparators(String s)
    75      {
    76          __pathSpecSeparators=s;
    77      }
    78
    79      /* --------------------------------------------------------------- */
    80      StringMap _prefixMap=new StringMap();
    81      StringMap _suffixMap=new StringMap();
    82      StringMap _exactMap=new StringMap();
    83
    84      List _defaultSingletonList=null;
    85      Entry _prefixDefault=null;
    86      Entry _default=null;
    87      Set _entrySet;
    88      boolean _nodefault=false;
    89
    90      /* --------------------------------------------------------------- */
    91      /** Construct empty PathMap.
    92       */
    93      public PathMap()
    94      {
    95          super(11);
    96          _entrySet=entrySet();
    97      }
    98
    99      /* --------------------------------------------------------------- */
    100     /** Construct empty PathMap.
    101      */
    102     public PathMap(boolean nodefault)
    103     {
    104         super(11);
    105         _entrySet=entrySet();
    106         _nodefault=nodefault;
    107     }
    108
    109     /* --------------------------------------------------------------- */
    110     /** Construct empty PathMap.
    111      */
    112     public PathMap(int capacity)
    113     {
    114         super (capacity);
    115         _entrySet=entrySet();
    116     }
    117
    118     /* --------------------------------------------------------------- */
    119     /** Construct from dictionary PathMap.
    120      */
    121     public PathMap(Map m)
    122     {
    123         putAll(m);
    124         _entrySet=entrySet();
    125     }
    126
    127     /* ------------------------------------------------------------ */
    128     public void writeExternal(java.io.ObjectOutput out)
    129         throws java.io.IOException
    130     {
    131         HashMap map = new HashMap(this);
    132         out.writeObject(map);
    133     }
    134
    135     /* ------------------------------------------------------------ */
    136     public void readExternal(java.io.ObjectInput in)
    137         throws java.io.IOException, ClassNotFoundException
    138     {
    139         HashMap map = (HashMap)in.readObject();
    140         this.putAll(map);
    141     }
    142
    143     /* --------------------------------------------------------------- */
    144     /** Add a single path match to the PathMap.
    145      * @param pathSpec The path specification, or comma separated list of
    146      * path specifications.
    147      * @param object The object the path maps to
    148      */
    149     public synchronized Object put(Object pathSpec, Object object)
    150     {
    151         StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
    152         Object old =null;
    153
    154         while (tok.hasMoreTokens())
    155         {
    156             String spec=tok.nextToken();
    157
    158             if (!spec.startsWith("/") && !spec.startsWith("*."))
    159                 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
    160
    161             old = super.put(spec,object);
    162
    163             // Make entry that was just created.
    164             Entry entry = new Entry(spec,object);
    165
    166             if (entry.getKey().equals(spec))
    167             {
    168                 if (spec.equals("/*"))
    169                     _prefixDefault=entry;
    170                 else if (spec.endsWith("/*"))
    171                 {
    172                     String mapped=spec.substring(0,spec.length()-2);
    173                     entry.setMapped(mapped);
    174                     _prefixMap.put(mapped,entry);
    175                     _exactMap.put(mapped,entry);
    176                     _exactMap.put(spec.substring(0,spec.length()-1),entry);
    177                 }
    178                 else if (spec.startsWith("*."))
    179                     _suffixMap.put(spec.substring(2),entry);
    180                 else if (spec.equals(URIUtil.SLASH))
    181                 {
    182                     if (_nodefault)
    183                         _exactMap.put(spec,entry);
    184                     else
    185                     {
    186                         _default=entry;
    187                         _defaultSingletonList=
    188                             SingletonList.newSingletonList(_default);
    189                     }
    190                 }
    191                 else
    192                 {
    193                     entry.setMapped(spec);
    194                     _exactMap.put(spec,entry);
    195                 }
    196             }
    197         }
    198
    199         return old;
    200     }
    201
    202     /* ------------------------------------------------------------ */
    203     /** Get object matched by the path.
    204      * @param path the path.
    205      * @return Best matched object or null.
    206      */
    207     public Object match(String path)
    208     {
    209         Map.Entry entry = getMatch(path);
    210         if (entry!=null)
    211             return entry.getValue();
    212         return null;
    213     }
    214
    215
    216     /* --------------------------------------------------------------- */
    217     /** Get the entry mapped by the best specification.
    218      * @param path the path.
    219      * @return Map.Entry of the best matched  or null.
    220      */
    221     public Entry getMatch(String path)
    222     {
    223         Map.Entry entry;
    224
    225         if (path==null)
    226             return null;
    227
    228         int l=path.length();
    229
    230         // try exact match
    231         entry=_exactMap.getEntry(path,0,l);
    232         if (entry!=null)
    233             return (Entry) entry.getValue();
    234
    235         // prefix search
    236         int i=l;
    237         while((i=path.lastIndexOf('/',i-1))>=0)
    238         {
    239             entry=_prefixMap.getEntry(path,0,i);
    240             if (entry!=null)
    241                 return (Entry) entry.getValue();
    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.getEntry(path,i+1,l-i-1);
    253             if (entry!=null)
    254                 return (Entry) entry.getValue();
    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         Map.Entry 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.getEntry(path,0,l);
    279         if (entry!=null)
    280             entries=LazyList.add(entries,entry.getValue());
    281
    282         // prefix search
    283         int i=l-1;
    284         while((i=path.lastIndexOf('/',i-1))>=0)
    285         {
    286             entry=_prefixMap.getEntry(path,0,i);
    287             if (entry!=null)
    288                 entries=LazyList.add(entries,entry.getValue());
    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.getEntry(path,i+1,l-i-1);
    300             if (entry!=null)
    301                 entries=LazyList.add(entries,entry.getValue());
    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         Entry match = getMatch(path);
    337         return match!=null && !match.equals(_default);
    338     }
    339
    340
    341
    342     /* --------------------------------------------------------------- */
    343     public synchronized Object remove(Object pathSpec)
    344     {
    345         if (pathSpec!=null)
    346         {
    347             String spec=(String) pathSpec;
    348             if (spec.equals("/*"))
    349                 _prefixDefault=null;
    350             else if (spec.endsWith("/*"))
    351             {
    352                 _prefixMap.remove(spec.substring(0,spec.length()-2));
    353                 _exactMap.remove(spec.substring(0,spec.length()-1));
    354                 _exactMap.remove(spec.substring(0,spec.length()-2));
    355             }
    356             else if (spec.startsWith("*."))
    357                 _suffixMap.remove(spec.substring(2));
    358             else if (spec.equals(URIUtil.SLASH))
    359             {
    360                 _default=null;
    361                 _defaultSingletonList=null;
    362             }
    363             else
    364                 _exactMap.remove(spec);
    365         }
    366         return super.remove(pathSpec);
    367     }
    368
    369     /* --------------------------------------------------------------- */
    370     public void clear()
    371     {
    372         _exactMap=new StringMap();
    373         _prefixMap=new StringMap();
    374         _suffixMap=new StringMap();
    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         char c = pathSpec.charAt(0);
    461
    462         if (c=='/')
    463         {
    464             if (pathSpec.length()==1)
    465                 return null;
    466
    467             boolean wildcard = isPathWildcardMatch(pathSpec, path);
    468
    469             // handle the case where pathSpec uses a wildcard and path info is "/*"
    470             if (pathSpec.equals(path) && !wildcard)
    471                 return null;
    472
    473             if (wildcard)
    474             {
    475                 if (path.length()==pathSpec.length()-2)
    476                     return null;
    477                 return path.substring(pathSpec.length()-2);
    478             }
    479         }
    480         return null;
    481     }
    482
    483
    484     /* ------------------------------------------------------------ */
    485     /** Relative path.
    486      * @param base The base the path is relative to.
    487      * @param pathSpec The spec of the path segment to ignore.
    488      * @param path the additional path
    489      * @return base plus path with pathspec removed
    490      */
    491     public static String relativePath(String base,
    492                                       String pathSpec,
    493                                       String path )
    494     {
    495         String info=pathInfo(pathSpec,path);
    496         if (info==null)
    497             info=path;
    498
    499         if( info.startsWith( "./"))
    500             info = info.substring( 2);
    501         if( base.endsWith( URIUtil.SLASH))
    502             if( info.startsWith( URIUtil.SLASH))
    503                 path = base + info.substring(1);
    504             else
    505                 path = base + info;
    506         else
    507             if( info.startsWith( URIUtil.SLASH))
    508                 path = base + info;
    509             else
    510                 path = base + URIUtil.SLASH + info;
    511         return path;
    512     }
    513
    514     /* ------------------------------------------------------------ */
    515     /* ------------------------------------------------------------ */
    516     /* ------------------------------------------------------------ */
    517     public static class Entry implements Map.Entry
    518     {
    519         private Object key;
    520         private Object value;
    521         private String mapped;
    522         private transient String string;
    523
    524         Entry(Object key, Object value)
    525         {
    526             this.key=key;
    527             this.value=value;
    528         }
    529
    530         public Object getKey()
    531         {
    532             return key;
    533         }
    534
    535         public Object getValue()
    536         {
    537             return value;
    538         }
    539
    540         public Object setValue(Object o)
    541         {
    542             throw new UnsupportedOperationException();
    543         }
    544
    545         public String toString()
    546         {
    547             if (string==null)
    548                 string=key+"="+value;
    549             return string;
    550         }
    551
    552         public String getMapped()
    553         {
    554             return mapped;
    555         }
    556
    557         void setMapped(String mapped)
    558         {
    559             this.mapped = mapped;
    560         }
    561     }
    562 }