// ======================================================================== // Copyright (c) 1999 Mort Bay Consulting (Australia) Pty. Ltd. // $Id: PathMap.java,v 1.15.2.6 2003/06/04 04:47:42 starksm Exp $ // ======================================================================== package org.mortbay.http; import java.io.Externalizable; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.mortbay.util.Code; import org.mortbay.util.LazyList; import org.mortbay.util.SingletonList; import org.mortbay.util.StringMap; /* ------------------------------------------------------------ */ /** URI path map to Object. * This mapping implements the path specification recommended * in the 2.2 Servlet API. * * Path specifications can be of the following forms:
 * /foo/bar           - an exact path specification.
 * /foo/*             - a prefix path specification (must end '/*').
 * *.ext              - a suffix path specification.
 * /                  - the default path specification.       
 * 
* Matching is performed in the following order *
  • Exact match. *
  • Longest prefix match. *
  • Longest suffix match. *
  • default. * * Multiple path specifications can be mapped by providing a list of * specifications. The list is separated by the characters specified * in the "org.mortbay.http.PathMap.separators" System property, which * defaults to : *

    * Note that this is a very different mapping to that provided by PathMap * in Jetty2. *

    * This class is not synchronized for get's. If concurrent modifications are * possible then it should be synchronized at a higher level. * * @version $Id: PathMap.java,v 1.15.2.6 2003/06/04 04:47:42 starksm Exp $ * @author Greg Wilkins (gregw) */ public class PathMap extends HashMap implements Externalizable { /* ------------------------------------------------------------ */ private static String __pathSpecSeparators = System.getProperty("org.mortbay.http.PathMap.separators",":,"); /* ------------------------------------------------------------ */ public static boolean __oldDefaultPath = Boolean.getBoolean("org.mortbay.http.PathMap.oldDefaultPath"); /* ------------------------------------------------------------ */ /** Set the path spec separator. * Multiple path specification may be included in a single string * if they are separated by the characters set in this string. * The default value is ":," or whatever has been set by the * system property org.mortbay.http.PathMap.separators * @param s separators */ public static void setPathSpecSeparators(String s) { __pathSpecSeparators=s; } /* --------------------------------------------------------------- */ StringMap _prefixMap=new StringMap(); StringMap _suffixMap=new StringMap(); StringMap _exactMap=new StringMap(); List _defaultSingletonList=null; Map.Entry _prefixDefault=null; Map.Entry _default=null; Set _entrySet; /* --------------------------------------------------------------- */ /** Construct empty PathMap. */ public PathMap() { super(11); _entrySet=entrySet(); } /* --------------------------------------------------------------- */ /** Construct empty PathMap. */ public PathMap(int capacity) { super (capacity); _entrySet=entrySet(); } /* --------------------------------------------------------------- */ /** Construct from dictionary PathMap. */ public PathMap(Map m) { putAll(m); _entrySet=entrySet(); } /* ------------------------------------------------------------ */ public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException { HashMap map = new HashMap(this); out.writeObject(map); } /* ------------------------------------------------------------ */ public void readExternal(java.io.ObjectInput in) throws java.io.IOException, ClassNotFoundException { HashMap map = (HashMap)in.readObject(); this.putAll(map); } /* --------------------------------------------------------------- */ /** Add a single path match to the PathMap. * @param pathSpec The path specification, or comma separated list of * path specifications. * @param object The object the path maps to */ public synchronized Object put(Object pathSpec, Object object) { StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators); Object old =null; while (tok.hasMoreTokens()) { String spec=tok.nextToken(); if (!spec.startsWith("/") && !spec.startsWith("*.")) { Code.warning("PathSpec "+spec+". must start with '/' or '*.'"); spec="/"+spec; } old = super.put(spec,object); // Look for the entry that was just created. Iterator iter=_entrySet.iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); if (entry.getKey().equals(spec)) { if (spec.equals("/*")) _prefixDefault=entry; else if (spec.endsWith("/*")) { _prefixMap.put(spec.substring(0,spec.length()-2),entry); _exactMap.put(spec.substring(0,spec.length()-1),entry); _exactMap.put(spec.substring(0,spec.length()-2),entry); } else if (spec.startsWith("*.")) _suffixMap.put(spec.substring(2),entry); else if (spec.equals("/")) { _default=entry; _defaultSingletonList= SingletonList.newSingletonList(_default); } else _exactMap.put(spec,entry); } } } return old; } /* ------------------------------------------------------------ */ /** Get object matched by the path. * @param path the path. * @return Best matched object or null. */ public Object match(String path) { Map.Entry entry = getMatch(path); if (entry!=null) return entry.getValue(); return null; } /* --------------------------------------------------------------- */ /** Get the entry mapped by the best specification. * @param path the path. * @return Map.Entry of the best matched or null. */ public Map.Entry getMatch(String path) { Map.Entry entry; if (path==null) return null; int l=path.indexOf(';'); if (l<0) { l=path.indexOf('?'); if (l<0) l=path.length(); } // try exact match entry=_exactMap.getEntry(path,0,l); if (entry!=null) return (Map.Entry) entry.getValue(); // prefix search int i=l; while((i=path.lastIndexOf('/',i-1))>=0) { entry=_prefixMap.getEntry(path,0,i); if (entry!=null) return (Map.Entry) entry.getValue(); } // Prefix Default if (_prefixDefault!=null) return _prefixDefault; // Extension search i=0; while ((i=path.indexOf('.',i+1))>0) { entry=_suffixMap.getEntry(path,i+1,l-i-1); if (entry!=null) return (Map.Entry) entry.getValue(); } // Default return _default; } /* --------------------------------------------------------------- */ /** Get all entries matched by the path. * Best match first. * @param path Path to match * @return List of Map.Entry instances key=pathSpec */ public List getMatches(String path) { Map.Entry entry; Object entries=null; if (path==null) return LazyList.getList(entries); int l=path.indexOf(';'); if (l<0) { l=path.indexOf('?'); if (l<0) l=path.length(); } // try exact match entry=_exactMap.getEntry(path,0,l); if (entry!=null) entries=LazyList.add(entries,entry.getValue()); // prefix search int i=l-1; while((i=path.lastIndexOf('/',i-1))>=0) { entry=_prefixMap.getEntry(path,0,i); if (entry!=null) entries=LazyList.add(entries,entry.getValue()); } // Prefix Default if (_prefixDefault!=null) entries=LazyList.add(entries,_prefixDefault); // Extension search i=0; while ((i=path.indexOf('.',i+1))>0) { entry=_suffixMap.getEntry(path,i+1,l-i-1); if (entry!=null) entries=LazyList.add(entries,entry.getValue()); } // Default if (_default!=null) { // Optimization for just the default if (entries==null) return _defaultSingletonList; entries=LazyList.add(entries,_default); } return LazyList.getList(entries); } /* --------------------------------------------------------------- */ public synchronized Object remove(Object pathSpec) { if (pathSpec!=null) { String spec=(String) pathSpec; if (spec.equals("/*")) _prefixDefault=null; else if (spec.endsWith("/*")) { _prefixMap.remove(spec.substring(0,spec.length()-2)); _exactMap.remove(spec.substring(0,spec.length()-1)); _exactMap.remove(spec.substring(0,spec.length()-2)); } else if (spec.startsWith("*.")) _suffixMap.remove(spec.substring(2)); else if (spec.equals("/")) { _default=null; _defaultSingletonList=null; } else _exactMap.remove(spec); } return super.remove(pathSpec); } /* --------------------------------------------------------------- */ public void clear() { _exactMap=new StringMap(); _prefixMap=new StringMap(); _suffixMap=new StringMap(); _default=null; _defaultSingletonList=null; super.clear(); } /* --------------------------------------------------------------- */ /** * @return true if match. */ public static boolean match(String pathSpec, String path) throws IllegalArgumentException { char c = pathSpec.charAt(0); if (c=='/') { if (pathSpec.length()==1 || pathSpec.equals(path)) return true; if (pathSpec.endsWith("/*") && pathSpec.regionMatches(0,path,0,pathSpec.length()-2)) return true; if (path.startsWith(pathSpec) && path.charAt(pathSpec.length())==';') return true; } else if (c=='*') return path.regionMatches(path.length()-pathSpec.length()-1, pathSpec,1,pathSpec.length()-1); return false; } /* --------------------------------------------------------------- */ /** Return the portion of a path that matches a path spec. * @return null if no match at all. */ public static String pathMatch(String pathSpec, String path) { char c = pathSpec.charAt(0); if (c=='/') { if (pathSpec.length()==1) return __oldDefaultPath?"":path; if (pathSpec.equals(path)) return path; if (pathSpec.endsWith("/*") && pathSpec.regionMatches(0,path,0,pathSpec.length()-2)) return path.substring(0,pathSpec.length()-2); if (path.startsWith(pathSpec) && path.charAt(pathSpec.length())==';') return path; } else if (c=='*') { if (path.regionMatches(path.length()-(pathSpec.length()-1), pathSpec,1,pathSpec.length()-1)) return path; } return null; } /* --------------------------------------------------------------- */ /** Return the portion of a path that is after a path spec. * @return The path info string */ public static String pathInfo(String pathSpec, String path) { char c = pathSpec.charAt(0); if (c=='/') { if (pathSpec.length()==1) return __oldDefaultPath?path:null; if (pathSpec.equals(path)) return null; if (pathSpec.endsWith("/*") && pathSpec.regionMatches(0,path,0,pathSpec.length()-2)) { if (path.length()==pathSpec.length()-2) return null; return path.substring(pathSpec.length()-2); } } return null; } /* ------------------------------------------------------------ */ /** Relative path. * @param base The base the path is relative to. * @param pathSpec The spec of the path segment to ignore. * @param path the additional path * @return base plus path with pathspec removed */ public static String relativePath(String base, String pathSpec, String path ) { String info=pathInfo(pathSpec,path); if (info==null) info=path; if( info.startsWith( "./")) info = info.substring( 2); if( base.endsWith( "/")) if( info.startsWith( "/")) path = base + info.substring(1); else path = base + info; else if( info.startsWith( "/")) path = base + info; else path = base + "/" + info; return path; } }