2 // ========================================================================
3 // Copyright (c) 1995-2014 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.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.http;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
26 import java.util.StringTokenizer;
28 import org.eclipse.jetty.util.ArrayTernaryTrie;
29 import org.eclipse.jetty.util.Trie;
30 import org.eclipse.jetty.util.URIUtil;
32 /* ------------------------------------------------------------ */
33 /** URI path map to Object.
34 * This mapping implements the path specification recommended
35 * in the 2.2 Servlet API.
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 * "" - the / path specification
44 * Matching is performed in the following order <NL>
46 * <LI>Longest prefix match.
47 * <LI>Longest suffix match.
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)
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.
59 * This class is not synchronized. If concurrent modifications are
60 * possible then it should be synchronized at a higher level.
64 public class PathMap<O> extends HashMap<String,O>
66 /* ------------------------------------------------------------ */
67 private static String __pathSpecSeparators = ":,";
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.
76 public static void setPathSpecSeparators(String s)
78 __pathSpecSeparators=s;
81 /* --------------------------------------------------------------- */
82 Trie<MappedEntry<O>> _prefixMap=new ArrayTernaryTrie<>(false);
83 Trie<MappedEntry<O>> _suffixMap=new ArrayTernaryTrie<>(false);
84 final Map<String,MappedEntry<O>> _exactMap=new HashMap<>();
86 List<MappedEntry<O>> _defaultSingletonList=null;
87 MappedEntry<O> _prefixDefault=null;
88 MappedEntry<O> _default=null;
89 boolean _nodefault=false;
91 /* --------------------------------------------------------------- */
97 /* --------------------------------------------------------------- */
98 public PathMap(boolean noDefault)
103 /* --------------------------------------------------------------- */
104 public PathMap(int capacity)
106 this(capacity, false);
109 /* --------------------------------------------------------------- */
110 private PathMap(int capacity, boolean noDefault)
113 _nodefault=noDefault;
116 /* --------------------------------------------------------------- */
117 /** Construct from dictionary PathMap.
119 public PathMap(Map<String, ? extends O> m)
124 /* --------------------------------------------------------------- */
125 /** Add a single path match to the PathMap.
126 * @param pathSpec The path specification, or comma separated list of
127 * path specifications.
128 * @param object The object the path maps to
131 public O put(String pathSpec, O object)
133 if ("".equals(pathSpec.trim()))
135 MappedEntry<O> entry = new MappedEntry<>("",object);
137 _exactMap.put("", entry);
138 return super.put("", object);
141 StringTokenizer tok = new StringTokenizer(pathSpec,__pathSpecSeparators);
144 while (tok.hasMoreTokens())
146 String spec=tok.nextToken();
148 if (!spec.startsWith("/") && !spec.startsWith("*."))
149 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
151 old = super.put(spec,object);
153 // Make entry that was just created.
154 MappedEntry<O> entry = new MappedEntry<>(spec,object);
156 if (entry.getKey().equals(spec))
158 if (spec.equals("/*"))
159 _prefixDefault=entry;
160 else if (spec.endsWith("/*"))
162 String mapped=spec.substring(0,spec.length()-2);
163 entry.setMapped(mapped);
164 while (!_prefixMap.put(mapped,entry))
165 _prefixMap=new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedEntry<O>>)_prefixMap,1.5);
167 else if (spec.startsWith("*."))
169 String suffix=spec.substring(2);
170 while(!_suffixMap.put(suffix,entry))
171 _suffixMap=new ArrayTernaryTrie<>((ArrayTernaryTrie<MappedEntry<O>>)_suffixMap,1.5);
173 else if (spec.equals(URIUtil.SLASH))
176 _exactMap.put(spec,entry);
180 _defaultSingletonList=Collections.singletonList(_default);
185 entry.setMapped(spec);
186 _exactMap.put(spec,entry);
194 /* ------------------------------------------------------------ */
195 /** Get object matched by the path.
196 * @param path the path.
197 * @return Best matched object or null.
199 public O match(String path)
201 MappedEntry<O> entry = getMatch(path);
203 return entry.getValue();
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.
213 public MappedEntry<O> getMatch(String path)
220 MappedEntry<O> entry=null;
223 if (l == 1 && path.charAt(0)=='/')
225 entry = _exactMap.get("");
231 entry=_exactMap.get(path);
237 final Trie<PathMap.MappedEntry<O>> prefix_map=_prefixMap;
240 entry=prefix_map.getBest(path,0,i);
243 String key = entry.getKey();
244 if (key.length()-2>=path.length() || path.charAt(key.length()-2)=='/')
250 if (_prefixDefault!=null)
251 return _prefixDefault;
255 final Trie<PathMap.MappedEntry<O>> suffix_map=_suffixMap;
256 while ((i=path.indexOf('.',i+1))>0)
258 entry=suffix_map.get(path,i+1,l-i-1);
267 /* --------------------------------------------------------------- */
268 /** Get all entries matched by the path.
270 * @param path Path to match
271 * @return List of Map.Entry instances key=pathSpec
273 public List<? extends Map.Entry<String,O>> getMatches(String path)
275 MappedEntry<O> entry;
276 List<MappedEntry<O>> entries=new ArrayList<>();
280 if (path.length()==0)
281 return _defaultSingletonList;
284 entry=_exactMap.get(path);
291 final Trie<PathMap.MappedEntry<O>> prefix_map=_prefixMap;
294 entry=prefix_map.getBest(path,0,i);
297 String key = entry.getKey();
298 if (key.length()-2>=path.length() || path.charAt(key.length()-2)=='/')
305 if (_prefixDefault!=null)
306 entries.add(_prefixDefault);
310 final Trie<PathMap.MappedEntry<O>> suffix_map=_suffixMap;
311 while ((i=path.indexOf('.',i+1))>0)
313 entry=suffix_map.get(path,i+1,l-i-1);
319 if ("/".equals(path))
321 entry=_exactMap.get("");
328 entries.add(_default);
334 /* --------------------------------------------------------------- */
335 /** Return whether the path matches any entries in the PathMap,
336 * excluding the default entry
337 * @param path Path to match
338 * @return Whether the PathMap contains any entries that match this
340 public boolean containsMatch(String path)
342 MappedEntry<?> match = getMatch(path);
343 return match!=null && !match.equals(_default);
346 /* --------------------------------------------------------------- */
348 public O remove(Object pathSpec)
352 String spec=(String) pathSpec;
353 if (spec.equals("/*"))
355 else if (spec.endsWith("/*"))
356 _prefixMap.remove(spec.substring(0,spec.length()-2));
357 else if (spec.startsWith("*."))
358 _suffixMap.remove(spec.substring(2));
359 else if (spec.equals(URIUtil.SLASH))
362 _defaultSingletonList=null;
365 _exactMap.remove(spec);
367 return super.remove(pathSpec);
370 /* --------------------------------------------------------------- */
375 _prefixMap=new ArrayTernaryTrie<>(false);
376 _suffixMap=new ArrayTernaryTrie<>(false);
378 _defaultSingletonList=null;
383 /* --------------------------------------------------------------- */
385 * @return true if match.
387 public static boolean match(String pathSpec, String path)
388 throws IllegalArgumentException
390 return match(pathSpec, path, false);
393 /* --------------------------------------------------------------- */
395 * @return true if match.
397 public static boolean match(String pathSpec, String path, boolean noDefault)
398 throws IllegalArgumentException
400 if (pathSpec.length()==0)
401 return "/".equals(path);
403 char c = pathSpec.charAt(0);
406 if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
409 if(isPathWildcardMatch(pathSpec, path))
413 return path.regionMatches(path.length()-pathSpec.length()+1,
414 pathSpec,1,pathSpec.length()-1);
418 /* --------------------------------------------------------------- */
419 private static boolean isPathWildcardMatch(String pathSpec, String path)
421 // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
422 int cpl=pathSpec.length()-2;
423 if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
425 if (path.length()==cpl || '/'==path.charAt(cpl))
432 /* --------------------------------------------------------------- */
433 /** Return the portion of a path that matches a path spec.
434 * @return null if no match at all.
436 public static String pathMatch(String pathSpec, String path)
438 char c = pathSpec.charAt(0);
442 if (pathSpec.length()==1)
445 if (pathSpec.equals(path))
448 if (isPathWildcardMatch(pathSpec, path))
449 return path.substring(0,pathSpec.length()-2);
453 if (path.regionMatches(path.length()-(pathSpec.length()-1),
454 pathSpec,1,pathSpec.length()-1))
460 /* --------------------------------------------------------------- */
461 /** Return the portion of a path that is after a path spec.
462 * @return The path info string
464 public static String pathInfo(String pathSpec, String path)
466 if ("".equals(pathSpec))
467 return path; //servlet 3 spec sec 12.2 will be '/'
469 char c = pathSpec.charAt(0);
473 if (pathSpec.length()==1)
476 boolean wildcard = isPathWildcardMatch(pathSpec, path);
478 // handle the case where pathSpec uses a wildcard and path info is "/*"
479 if (pathSpec.equals(path) && !wildcard)
484 if (path.length()==pathSpec.length()-2)
486 return path.substring(pathSpec.length()-2);
493 /* ------------------------------------------------------------ */
495 * @param base The base the path is relative to.
496 * @param pathSpec The spec of the path segment to ignore.
497 * @param path the additional path
498 * @return base plus path with pathspec removed
500 public static String relativePath(String base,
504 String info=pathInfo(pathSpec,path);
508 if( info.startsWith( "./"))
509 info = info.substring( 2);
510 if( base.endsWith( URIUtil.SLASH))
511 if( info.startsWith( URIUtil.SLASH))
512 path = base + info.substring(1);
516 if( info.startsWith( URIUtil.SLASH))
519 path = base + URIUtil.SLASH + info;
523 /* ------------------------------------------------------------ */
524 /* ------------------------------------------------------------ */
525 /* ------------------------------------------------------------ */
526 public static class MappedEntry<O> implements Map.Entry<String,O>
528 private final String key;
529 private final O value;
530 private String mapped;
532 MappedEntry(String key, O value)
539 public String getKey()
551 public O setValue(O o)
553 throw new UnsupportedOperationException();
557 public String toString()
559 return key+"="+value;
562 public String getMapped()
567 void setMapped(String mapped)
569 this.mapped = mapped;