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 }