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     public synchronized Object remove(Object pathSpec)
    330     {
    331         if (pathSpec!=null)
    332         {
    333             String spec=(String) pathSpec;
    334             if (spec.equals("/*"))
    335                 _prefixDefault=null;
    336             else if (spec.endsWith("/*"))
    337             {
    338                 _prefixMap.remove(spec.substring(0,spec.length()-2));
    339                 _exactMap.remove(spec.substring(0,spec.length()-1));
    340                 _exactMap.remove(spec.substring(0,spec.length()-2));
    341             }
    342             else if (spec.startsWith("*."))
    343                 _suffixMap.remove(spec.substring(2));
    344             else if (spec.equals(URIUtil.SLASH))
    345             {
    346                 _default=null;
    347                 _defaultSingletonList=null;
    348             }
    349             else
    350                 _exactMap.remove(spec);
    351         }
    352         return super.remove(pathSpec);
    353     }
    354
    355     /* --------------------------------------------------------------- */
    356     public void clear()
    357     {
    358         _exactMap=new StringMap();
    359         _prefixMap=new StringMap();
    360         _suffixMap=new StringMap();
    361         _default=null;
    362         _defaultSingletonList=null;
    363         super.clear();
    364     }
    365
    366     /* --------------------------------------------------------------- */
    367     /**
    368      * @return true if match.
    369      */
    370     public static boolean match(String pathSpec, String path)
    371         throws IllegalArgumentException
    372     {
    373         return match(pathSpec, path, false);
    374     }
    375
    376     /* --------------------------------------------------------------- */
    377     /**
    378      * @return true if match.
    379      */
    380     public static boolean match(String pathSpec, String path, boolean noDefault)
    381     throws IllegalArgumentException
    382     {
    383         char c = pathSpec.charAt(0);
    384         if (c=='/')
    385         {
    386             if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
    387                 return true;
    388
    389             if(isPathWildcardMatch(pathSpec, path))
    390                 return true;
    391         }
    392         else if (c=='*')
    393             return path.regionMatches(path.length()-pathSpec.length()+1,
    394                                       pathSpec,1,pathSpec.length()-1);
    395         return false;
    396     }
    397
    398     /* --------------------------------------------------------------- */
    399     private static boolean isPathWildcardMatch(String pathSpec, String path)
    400     {
    401         // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
    402         int cpl=pathSpec.length()-2;
    403         if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
    404         {
    405             if (path.length()==cpl || '/'==path.charAt(cpl))
    406                 return true;
    407         }
    408         return false;
    409     }
    410
    411
    412     /* --------------------------------------------------------------- */
    413     /** Return the portion of a path that matches a path spec.
    414      * @return null if no match at all.
    415      */
    416     public static String pathMatch(String pathSpec, String path)
    417     {
    418         char c = pathSpec.charAt(0);
    419
    420         if (c=='/')
    421         {
    422             if (pathSpec.length()==1)
    423                 return path;
    424
    425             if (pathSpec.equals(path))
    426                 return path;
    427
    428             if (isPathWildcardMatch(pathSpec, path))
    429                 return path.substring(0,pathSpec.length()-2);
    430         }
    431         else if (c=='*')
    432         {
    433             if (path.regionMatches(path.length()-(pathSpec.length()-1),
    434                                    pathSpec,1,pathSpec.length()-1))
    435                 return path;
    436         }
    437         return null;
    438     }
    439
    440     /* --------------------------------------------------------------- */
    441     /** Return the portion of a path that is after a path spec.
    442      * @return The path info string
    443      */
    444     public static String pathInfo(String pathSpec, String path)
    445     {
    446         char c = pathSpec.charAt(0);
    447
    448         if (c=='/')
    449         {
    450             if (pathSpec.length()==1)
    451                 return null;
    452
    453             if (pathSpec.equals(path))
    454                 return null;
    455
    456             if (isPathWildcardMatch(pathSpec, path))
    457             {
    458                 if (path.length()==pathSpec.length()-2)
    459                     return null;
    460                 return path.substring(pathSpec.length()-2);
    461             }
    462         }
    463         return null;
    464     }
    465
    466
    467     /* ------------------------------------------------------------ */
    468     /** Relative path.
    469      * @param base The base the path is relative to.
    470      * @param pathSpec The spec of the path segment to ignore.
    471      * @param path the additional path
    472      * @return base plus path with pathspec removed
    473      */
    474     public static String relativePath(String base,
    475                                       String pathSpec,
    476                                       String path )
    477     {
    478         String info=pathInfo(pathSpec,path);
    479         if (info==null)
    480             info=path;
    481
    482         if( info.startsWith( "./"))
    483             info = info.substring( 2);
    484         if( base.endsWith( URIUtil.SLASH))
    485             if( info.startsWith( URIUtil.SLASH))
    486                 path = base + info.substring(1);
    487             else
    488                 path = base + info;
    489         else
    490             if( info.startsWith( URIUtil.SLASH))
    491                 path = base + info;
    492             else
    493                 path = base + URIUtil.SLASH + info;
    494         return path;
    495     }
    496
    497     /* ------------------------------------------------------------ */
    498     /* ------------------------------------------------------------ */
    499     /* ------------------------------------------------------------ */
    500     public static class Entry implements Map.Entry
    501     {
    502         private Object key;
    503         private Object value;
    504         private String mapped;
    505         private transient String string;
    506
    507         Entry(Object key, Object value)
    508         {
    509             this.key=key;
    510             this.value=value;
    511         }
    512
    513         public Object getKey()
    514         {
    515             return key;
    516         }
    517
    518         public Object getValue()
    519         {
    520             return value;
    521         }
    522
    523         public Object setValue(Object o)
    524         {
    525             throw new UnsupportedOperationException();
    526         }
    527
    528         public String toString()
    529         {
    530             if (string==null)
    531                 string=key+"="+value;
    532             return string;
    533         }
    534
    535         public String getMapped()
    536         {
    537             return mapped;
    538         }
    539
    540         void setMapped(String mapped)
    541         {
    542             this.mapped = mapped;
    543         }
    544     }
    545 }