1 // ========================================================================
2 // Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd.
3 // ------------------------------------------------------------------------
4 // All rights reserved. This program and the accompanying materials
5 // are made available under the terms of the Eclipse Public License v1.0
6 // and Apache License v2.0 which accompanies this distribution.
7 // The Eclipse Public License is available at
8 // http://www.eclipse.org/legal/epl-v10.html
9 // The Apache License v2.0 is available at
10 // http://www.opensource.org/licenses/apache2.0.php
11 // You may elect to redistribute this code under either of these licenses.
12 // ========================================================================
13
14 package org.eclipse.jetty.http;
15
16 import java.io.Externalizable;
17 import java.util.Collections;
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.eclipse.jetty.util.LazyList;
25 import org.eclipse.jetty.util.StringMap;
26 import org.eclipse.jetty.util.URIUtil;
27
28 /* ------------------------------------------------------------ */
29 /** URI path map to Object.
30 * This mapping implements the path specification recommended
31 * in the 2.2 Servlet API.
32 *
33 * Path specifications can be of the following forms:<PRE>
34 * /foo/bar - an exact path specification.
35 * /foo/* - a prefix path specification (must end '/*').
36 * *.ext - a suffix path specification.
37 * / - the default path specification.
38 * </PRE>
39 * Matching is performed in the following order <NL>
40 * <LI>Exact match.
41 * <LI>Longest prefix match.
42 * <LI>Longest suffix match.
43 * <LI>default.
44 * </NL>
45 * Multiple path specifications can be mapped by providing a list of
46 * specifications. By default this class uses characters ":," as path
47 * separators, unless configured differently by calling the static
48 * method @see PathMap#setPathSpecSeparators(String)
49 * <P>
50 * Special characters within paths such as '?� and ';' are not treated specially
51 * as it is assumed they would have been either encoded in the original URL or
52 * stripped from the path.
53 * <P>
54 * This class is not synchronized. If concurrent modifications are
55 * possible then it should be synchronized at a higher level.
56 *
57 *
58 */
59 public class PathMap extends HashMap implements Externalizable
60 {
61 /* ------------------------------------------------------------ */
62 private static String __pathSpecSeparators = ":,";
63
64 /* ------------------------------------------------------------ */
65 /** Set the path spec separator.
66 * Multiple path specification may be included in a single string
67 * if they are separated by the characters set in this string.
68 * By default this class uses ":," characters as path separators.
69 * @param s separators
70 */
71 public static void setPathSpecSeparators(String s)
72 {
73 __pathSpecSeparators=s;
74 }
75
76 /* --------------------------------------------------------------- */
77 final StringMap _prefixMap=new StringMap();
78 final StringMap _suffixMap=new StringMap();
79 final StringMap _exactMap=new StringMap();
80
81 List _defaultSingletonList=null;
82 Entry _prefixDefault=null;
83 Entry _default=null;
84 final Set _entrySet;
85 boolean _nodefault=false;
86
87 /* --------------------------------------------------------------- */
88 /** Construct empty PathMap.
89 */
90 public PathMap()
91 {
92 super(11);
93 _entrySet=entrySet();
94 }
95
96 /* --------------------------------------------------------------- */
97 /** Construct empty PathMap.
98 */
99 public PathMap(boolean nodefault)
100 {
101 super(11);
102 _entrySet=entrySet();
103 _nodefault=nodefault;
104 }
105
106 /* --------------------------------------------------------------- */
107 /** Construct empty PathMap.
108 */
109 public PathMap(int capacity)
110 {
111 super (capacity);
112 _entrySet=entrySet();
113 }
114
115 /* --------------------------------------------------------------- */
116 /** Construct from dictionary PathMap.
117 */
118 public PathMap(Map m)
119 {
120 putAll(m);
121 _entrySet=entrySet();
122 }
123
124 /* ------------------------------------------------------------ */
125 public void writeExternal(java.io.ObjectOutput out)
126 throws java.io.IOException
127 {
128 HashMap map = new HashMap(this);
129 out.writeObject(map);
130 }
131
132 /* ------------------------------------------------------------ */
133 public void readExternal(java.io.ObjectInput in)
134 throws java.io.IOException, ClassNotFoundException
135 {
136 HashMap map = (HashMap)in.readObject();
137 this.putAll(map);
138 }
139
140 /* --------------------------------------------------------------- */
141 /** Add a single path match to the PathMap.
142 * @param pathSpec The path specification, or comma separated list of
143 * path specifications.
144 * @param object The object the path maps to
145 */
146 @Override
147 public Object put(Object pathSpec, Object object)
148 {
149 String str = pathSpec.toString();
150 if ("".equals(str.trim()))
151 {
152 Entry entry = new Entry("",object);
153 entry.setMapped("");
154 _exactMap.put("", entry);
155 return super.put("", object);
156 }
157
158 StringTokenizer tok = new StringTokenizer(str,__pathSpecSeparators);
159 Object old =null;
160
161 while (tok.hasMoreTokens())
162 {
163 String spec=tok.nextToken();
164
165 if (!spec.startsWith("/") && !spec.startsWith("*."))
166 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
167
168 old = super.put(spec,object);
169
170 // Make entry that was just created.
171 Entry entry = new Entry(spec,object);
172
173 if (entry.getKey().equals(spec))
174 {
175 if (spec.equals("/*"))
176 _prefixDefault=entry;
177 else if (spec.endsWith("/*"))
178 {
179 String mapped=spec.substring(0,spec.length()-2);
180 entry.setMapped(mapped);
181 _prefixMap.put(mapped,entry);
182 _exactMap.put(mapped,entry);
183 _exactMap.put(spec.substring(0,spec.length()-1),entry);
184 }
185 else if (spec.startsWith("*."))
186 _suffixMap.put(spec.substring(2),entry);
187 else if (spec.equals(URIUtil.SLASH))
188 {
189 if (_nodefault)
190 _exactMap.put(spec,entry);
191 else
192 {
193 _default=entry;
194 _defaultSingletonList=
195 Collections.singletonList(_default);
196 }
197 }
198 else
199 {
200 entry.setMapped(spec);
201 _exactMap.put(spec,entry);
202 }
203 }
204 }
205
206 return old;
207 }
208
209 /* ------------------------------------------------------------ */
210 /** Get object matched by the path.
211 * @param path the path.
212 * @return Best matched object or null.
213 */
214 public Object match(String path)
215 {
216 Map.Entry entry = getMatch(path);
217 if (entry!=null)
218 return entry.getValue();
219 return null;
220 }
221
222
223 /* --------------------------------------------------------------- */
224 /** Get the entry mapped by the best specification.
225 * @param path the path.
226 * @return Map.Entry of the best matched or null.
227 */
228 public Entry getMatch(String path)
229 {
230 Map.Entry entry=null;
231
232 if (path==null)
233 return null;
234
235 int l=path.length();
236
237 //special case
238 if (l == 1 && path.charAt(0)=='/')
239 {
240 entry = (Map.Entry)_exactMap.get("");
241 if (entry != null)
242 return (Entry)entry;
243 }
244
245 // try exact match
246 entry=_exactMap.getEntry(path,0,l);
247 if (entry!=null)
248 return (Entry) entry.getValue();
249
250 // prefix search
251 int i=l;
252 while((i=path.lastIndexOf('/',i-1))>=0)
253 {
254 entry=_prefixMap.getEntry(path,0,i);
255 if (entry!=null)
256 return (Entry) entry.getValue();
257 }
258
259 // Prefix Default
260 if (_prefixDefault!=null)
261 return _prefixDefault;
262
263 // Extension search
264 i=0;
265 while ((i=path.indexOf('.',i+1))>0)
266 {
267 entry=_suffixMap.getEntry(path,i+1,l-i-1);
268 if (entry!=null)
269 return (Entry) entry.getValue();
270 }
271
272 // Default
273 return _default;
274 }
275
276 /* --------------------------------------------------------------- */
277 /** Get all entries matched by the path.
278 * Best match first.
279 * @param path Path to match
280 * @return LazyList of Map.Entry instances key=pathSpec
281 */
282 public Object getLazyMatches(String path)
283 {
284 Map.Entry entry;
285 Object entries=null;
286
287 if (path==null)
288 return LazyList.getList(entries);
289
290 int l=path.length();
291
292 // try exact match
293 entry=_exactMap.getEntry(path,0,l);
294 if (entry!=null)
295 entries=LazyList.add(entries,entry.getValue());
296
297 // prefix search
298 int i=l-1;
299 while((i=path.lastIndexOf('/',i-1))>=0)
300 {
301 entry=_prefixMap.getEntry(path,0,i);
302 if (entry!=null)
303 entries=LazyList.add(entries,entry.getValue());
304 }
305
306 // Prefix Default
307 if (_prefixDefault!=null)
308 entries=LazyList.add(entries,_prefixDefault);
309
310 // Extension search
311 i=0;
312 while ((i=path.indexOf('.',i+1))>0)
313 {
314 entry=_suffixMap.getEntry(path,i+1,l-i-1);
315 if (entry!=null)
316 entries=LazyList.add(entries,entry.getValue());
317 }
318
319 // Default
320 if (_default!=null)
321 {
322 // Optimization for just the default
323 if (entries==null)
324 return _defaultSingletonList;
325
326 entries=LazyList.add(entries,_default);
327 }
328
329 return entries;
330 }
331
332 /* --------------------------------------------------------------- */
333 /** Get all entries matched by the path.
334 * Best match first.
335 * @param path Path to match
336 * @return List of Map.Entry instances key=pathSpec
337 */
338 public List getMatches(String path)
339 {
340 return LazyList.getList(getLazyMatches(path));
341 }
342
343 /* --------------------------------------------------------------- */
344 /** Return whether the path matches any entries in the PathMap,
345 * excluding the default entry
346 * @param path Path to match
347 * @return Whether the PathMap contains any entries that match this
348 */
349 public boolean containsMatch(String path)
350 {
351 Entry match = getMatch(path);
352 return match!=null && !match.equals(_default);
353 }
354
355 /* --------------------------------------------------------------- */
356 @Override
357 public Object remove(Object pathSpec)
358 {
359 if (pathSpec!=null)
360 {
361 String spec=(String) pathSpec;
362 if (spec.equals("/*"))
363 _prefixDefault=null;
364 else if (spec.endsWith("/*"))
365 {
366 _prefixMap.remove(spec.substring(0,spec.length()-2));
367 _exactMap.remove(spec.substring(0,spec.length()-1));
368 _exactMap.remove(spec.substring(0,spec.length()-2));
369 }
370 else if (spec.startsWith("*."))
371 _suffixMap.remove(spec.substring(2));
372 else if (spec.equals(URIUtil.SLASH))
373 {
374 _default=null;
375 _defaultSingletonList=null;
376 }
377 else
378 _exactMap.remove(spec);
379 }
380 return super.remove(pathSpec);
381 }
382
383 /* --------------------------------------------------------------- */
384 @Override
385 public void clear()
386 {
387 _exactMap.clear();
388 _prefixMap.clear();
389 _suffixMap.clear();
390 _default=null;
391 _defaultSingletonList=null;
392 super.clear();
393 }
394
395 /* --------------------------------------------------------------- */
396 /**
397 * @return true if match.
398 */
399 public static boolean match(String pathSpec, String path)
400 throws IllegalArgumentException
401 {
402 return match(pathSpec, path, false);
403 }
404
405 /* --------------------------------------------------------------- */
406 /**
407 * @return true if match.
408 */
409 public static boolean match(String pathSpec, String path, boolean noDefault)
410 throws IllegalArgumentException
411 {
412 char c = pathSpec.charAt(0);
413 if (c=='/')
414 {
415 if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
416 return true;
417
418 if(isPathWildcardMatch(pathSpec, path))
419 return true;
420 }
421 else if (c=='*')
422 return path.regionMatches(path.length()-pathSpec.length()+1,
423 pathSpec,1,pathSpec.length()-1);
424 return false;
425 }
426
427 /* --------------------------------------------------------------- */
428 private static boolean isPathWildcardMatch(String pathSpec, String path)
429 {
430 // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
431 int cpl=pathSpec.length()-2;
432 if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
433 {
434 if (path.length()==cpl || '/'==path.charAt(cpl))
435 return true;
436 }
437 return false;
438 }
439
440
441 /* --------------------------------------------------------------- */
442 /** Return the portion of a path that matches a path spec.
443 * @return null if no match at all.
444 */
445 public static String pathMatch(String pathSpec, String path)
446 {
447 char c = pathSpec.charAt(0);
448
449 if (c=='/')
450 {
451 if (pathSpec.length()==1)
452 return path;
453
454 if (pathSpec.equals(path))
455 return path;
456
457 if (isPathWildcardMatch(pathSpec, path))
458 return path.substring(0,pathSpec.length()-2);
459 }
460 else if (c=='*')
461 {
462 if (path.regionMatches(path.length()-(pathSpec.length()-1),
463 pathSpec,1,pathSpec.length()-1))
464 return path;
465 }
466 return null;
467 }
468
469 /* --------------------------------------------------------------- */
470 /** Return the portion of a path that is after a path spec.
471 * @return The path info string
472 */
473 public static String pathInfo(String pathSpec, String path)
474 {
475 if ("".equals(pathSpec))
476 return path; //servlet 3 spec sec 12.2 will be '/'
477
478 char c = pathSpec.charAt(0);
479
480 if (c=='/')
481 {
482 if (pathSpec.length()==1)
483 return null;
484
485 boolean wildcard = isPathWildcardMatch(pathSpec, path);
486
487 // handle the case where pathSpec uses a wildcard and path info is "/*"
488 if (pathSpec.equals(path) && !wildcard)
489 return null;
490
491 if (wildcard)
492 {
493 if (path.length()==pathSpec.length()-2)
494 return null;
495 return path.substring(pathSpec.length()-2);
496 }
497 }
498 return null;
499 }
500
501
502 /* ------------------------------------------------------------ */
503 /** Relative path.
504 * @param base The base the path is relative to.
505 * @param pathSpec The spec of the path segment to ignore.
506 * @param path the additional path
507 * @return base plus path with pathspec removed
508 */
509 public static String relativePath(String base,
510 String pathSpec,
511 String path )
512 {
513 String info=pathInfo(pathSpec,path);
514 if (info==null)
515 info=path;
516
517 if( info.startsWith( "./"))
518 info = info.substring( 2);
519 if( base.endsWith( URIUtil.SLASH))
520 if( info.startsWith( URIUtil.SLASH))
521 path = base + info.substring(1);
522 else
523 path = base + info;
524 else
525 if( info.startsWith( URIUtil.SLASH))
526 path = base + info;
527 else
528 path = base + URIUtil.SLASH + info;
529 return path;
530 }
531
532 /* ------------------------------------------------------------ */
533 /* ------------------------------------------------------------ */
534 /* ------------------------------------------------------------ */
535 public static class Entry implements Map.Entry
536 {
537 private final Object key;
538 private final Object value;
539 private String mapped;
540 private transient String string;
541
542 Entry(Object key, Object value)
543 {
544 this.key=key;
545 this.value=value;
546 }
547
548 public Object getKey()
549 {
550 return key;
551 }
552
553 public Object getValue()
554 {
555 return value;
556 }
557
558 public Object setValue(Object o)
559 {
560 throw new UnsupportedOperationException();
561 }
562
563 @Override
564 public String toString()
565 {
566 if (string==null)
567 string=key+"="+value;
568 return string;
569 }
570
571 public String getMapped()
572 {
573 return mapped;
574 }
575
576 void setMapped(String mapped)
577 {
578 this.mapped = mapped;
579 }
580 }
581 }