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.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.StringTokenizer;
22
23 import org.eclipse.jetty.util.LazyList;
24 import org.eclipse.jetty.util.SingletonList;
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 StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
150 Object old =null;
151
152 while (tok.hasMoreTokens())
153 {
154 String spec=tok.nextToken();
155
156 if (!spec.startsWith("/") && !spec.startsWith("*."))
157 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
158
159 old = super.put(spec,object);
160
161 // Make entry that was just created.
162 Entry entry = new Entry(spec,object);
163
164 if (entry.getKey().equals(spec))
165 {
166 if (spec.equals("/*"))
167 _prefixDefault=entry;
168 else if (spec.endsWith("/*"))
169 {
170 String mapped=spec.substring(0,spec.length()-2);
171 entry.setMapped(mapped);
172 _prefixMap.put(mapped,entry);
173 _exactMap.put(mapped,entry);
174 _exactMap.put(spec.substring(0,spec.length()-1),entry);
175 }
176 else if (spec.startsWith("*."))
177 _suffixMap.put(spec.substring(2),entry);
178 else if (spec.equals(URIUtil.SLASH))
179 {
180 if (_nodefault)
181 _exactMap.put(spec,entry);
182 else
183 {
184 _default=entry;
185 _defaultSingletonList=
186 SingletonList.newSingletonList(_default);
187 }
188 }
189 else
190 {
191 entry.setMapped(spec);
192 _exactMap.put(spec,entry);
193 }
194 }
195 }
196
197 return old;
198 }
199
200 /* ------------------------------------------------------------ */
201 /** Get object matched by the path.
202 * @param path the path.
203 * @return Best matched object or null.
204 */
205 public Object match(String path)
206 {
207 Map.Entry entry = getMatch(path);
208 if (entry!=null)
209 return entry.getValue();
210 return null;
211 }
212
213
214 /* --------------------------------------------------------------- */
215 /** Get the entry mapped by the best specification.
216 * @param path the path.
217 * @return Map.Entry of the best matched or null.
218 */
219 public Entry getMatch(String path)
220 {
221 Map.Entry entry;
222
223 if (path==null)
224 return null;
225
226 int l=path.length();
227
228 // try exact match
229 entry=_exactMap.getEntry(path,0,l);
230 if (entry!=null)
231 return (Entry) entry.getValue();
232
233 // prefix search
234 int i=l;
235 while((i=path.lastIndexOf('/',i-1))>=0)
236 {
237 entry=_prefixMap.getEntry(path,0,i);
238 if (entry!=null)
239 return (Entry) entry.getValue();
240 }
241
242 // Prefix Default
243 if (_prefixDefault!=null)
244 return _prefixDefault;
245
246 // Extension search
247 i=0;
248 while ((i=path.indexOf('.',i+1))>0)
249 {
250 entry=_suffixMap.getEntry(path,i+1,l-i-1);
251 if (entry!=null)
252 return (Entry) entry.getValue();
253 }
254
255 // Default
256 return _default;
257 }
258
259 /* --------------------------------------------------------------- */
260 /** Get all entries matched by the path.
261 * Best match first.
262 * @param path Path to match
263 * @return LazyList of Map.Entry instances key=pathSpec
264 */
265 public Object getLazyMatches(String path)
266 {
267 Map.Entry entry;
268 Object entries=null;
269
270 if (path==null)
271 return LazyList.getList(entries);
272
273 int l=path.length();
274
275 // try exact match
276 entry=_exactMap.getEntry(path,0,l);
277 if (entry!=null)
278 entries=LazyList.add(entries,entry.getValue());
279
280 // prefix search
281 int i=l-1;
282 while((i=path.lastIndexOf('/',i-1))>=0)
283 {
284 entry=_prefixMap.getEntry(path,0,i);
285 if (entry!=null)
286 entries=LazyList.add(entries,entry.getValue());
287 }
288
289 // Prefix Default
290 if (_prefixDefault!=null)
291 entries=LazyList.add(entries,_prefixDefault);
292
293 // Extension search
294 i=0;
295 while ((i=path.indexOf('.',i+1))>0)
296 {
297 entry=_suffixMap.getEntry(path,i+1,l-i-1);
298 if (entry!=null)
299 entries=LazyList.add(entries,entry.getValue());
300 }
301
302 // Default
303 if (_default!=null)
304 {
305 // Optimization for just the default
306 if (entries==null)
307 return _defaultSingletonList;
308
309 entries=LazyList.add(entries,_default);
310 }
311
312 return entries;
313 }
314
315 /* --------------------------------------------------------------- */
316 /** Get all entries matched by the path.
317 * Best match first.
318 * @param path Path to match
319 * @return List of Map.Entry instances key=pathSpec
320 */
321 public List getMatches(String path)
322 {
323 return LazyList.getList(getLazyMatches(path));
324 }
325
326 /* --------------------------------------------------------------- */
327 /** Return whether the path matches any entries in the PathMap,
328 * excluding the default entry
329 * @param path Path to match
330 * @return Whether the PathMap contains any entries that match this
331 */
332 public boolean containsMatch(String path)
333 {
334 Entry match = getMatch(path);
335 return match!=null && !match.equals(_default);
336 }
337
338 /* --------------------------------------------------------------- */
339 @Override
340 public Object remove(Object pathSpec)
341 {
342 if (pathSpec!=null)
343 {
344 String spec=(String) pathSpec;
345 if (spec.equals("/*"))
346 _prefixDefault=null;
347 else if (spec.endsWith("/*"))
348 {
349 _prefixMap.remove(spec.substring(0,spec.length()-2));
350 _exactMap.remove(spec.substring(0,spec.length()-1));
351 _exactMap.remove(spec.substring(0,spec.length()-2));
352 }
353 else if (spec.startsWith("*."))
354 _suffixMap.remove(spec.substring(2));
355 else if (spec.equals(URIUtil.SLASH))
356 {
357 _default=null;
358 _defaultSingletonList=null;
359 }
360 else
361 _exactMap.remove(spec);
362 }
363 return super.remove(pathSpec);
364 }
365
366 /* --------------------------------------------------------------- */
367 @Override
368 public void clear()
369 {
370 _exactMap.clear();
371 _prefixMap.clear();
372 _suffixMap.clear();
373 _default=null;
374 _defaultSingletonList=null;
375 super.clear();
376 }
377
378 /* --------------------------------------------------------------- */
379 /**
380 * @return true if match.
381 */
382 public static boolean match(String pathSpec, String path)
383 throws IllegalArgumentException
384 {
385 return match(pathSpec, path, false);
386 }
387
388 /* --------------------------------------------------------------- */
389 /**
390 * @return true if match.
391 */
392 public static boolean match(String pathSpec, String path, boolean noDefault)
393 throws IllegalArgumentException
394 {
395 char c = pathSpec.charAt(0);
396 if (c=='/')
397 {
398 if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
399 return true;
400
401 if(isPathWildcardMatch(pathSpec, path))
402 return true;
403 }
404 else if (c=='*')
405 return path.regionMatches(path.length()-pathSpec.length()+1,
406 pathSpec,1,pathSpec.length()-1);
407 return false;
408 }
409
410 /* --------------------------------------------------------------- */
411 private static boolean isPathWildcardMatch(String pathSpec, String path)
412 {
413 // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
414 int cpl=pathSpec.length()-2;
415 if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
416 {
417 if (path.length()==cpl || '/'==path.charAt(cpl))
418 return true;
419 }
420 return false;
421 }
422
423
424 /* --------------------------------------------------------------- */
425 /** Return the portion of a path that matches a path spec.
426 * @return null if no match at all.
427 */
428 public static String pathMatch(String pathSpec, String path)
429 {
430 char c = pathSpec.charAt(0);
431
432 if (c=='/')
433 {
434 if (pathSpec.length()==1)
435 return path;
436
437 if (pathSpec.equals(path))
438 return path;
439
440 if (isPathWildcardMatch(pathSpec, path))
441 return path.substring(0,pathSpec.length()-2);
442 }
443 else if (c=='*')
444 {
445 if (path.regionMatches(path.length()-(pathSpec.length()-1),
446 pathSpec,1,pathSpec.length()-1))
447 return path;
448 }
449 return null;
450 }
451
452 /* --------------------------------------------------------------- */
453 /** Return the portion of a path that is after a path spec.
454 * @return The path info string
455 */
456 public static String pathInfo(String pathSpec, String path)
457 {
458 char c = pathSpec.charAt(0);
459
460 if (c=='/')
461 {
462 if (pathSpec.length()==1)
463 return null;
464
465 boolean wildcard = isPathWildcardMatch(pathSpec, path);
466
467 // handle the case where pathSpec uses a wildcard and path info is "/*"
468 if (pathSpec.equals(path) && !wildcard)
469 return null;
470
471 if (wildcard)
472 {
473 if (path.length()==pathSpec.length()-2)
474 return null;
475 return path.substring(pathSpec.length()-2);
476 }
477 }
478 return null;
479 }
480
481
482 /* ------------------------------------------------------------ */
483 /** Relative path.
484 * @param base The base the path is relative to.
485 * @param pathSpec The spec of the path segment to ignore.
486 * @param path the additional path
487 * @return base plus path with pathspec removed
488 */
489 public static String relativePath(String base,
490 String pathSpec,
491 String path )
492 {
493 String info=pathInfo(pathSpec,path);
494 if (info==null)
495 info=path;
496
497 if( info.startsWith( "./"))
498 info = info.substring( 2);
499 if( base.endsWith( URIUtil.SLASH))
500 if( info.startsWith( URIUtil.SLASH))
501 path = base + info.substring(1);
502 else
503 path = base + info;
504 else
505 if( info.startsWith( URIUtil.SLASH))
506 path = base + info;
507 else
508 path = base + URIUtil.SLASH + info;
509 return path;
510 }
511
512 /* ------------------------------------------------------------ */
513 /* ------------------------------------------------------------ */
514 /* ------------------------------------------------------------ */
515 public static class Entry implements Map.Entry
516 {
517 private final Object key;
518 private final Object value;
519 private String mapped;
520 private transient String string;
521
522 Entry(Object key, Object value)
523 {
524 this.key=key;
525 this.value=value;
526 }
527
528 public Object getKey()
529 {
530 return key;
531 }
532
533 public Object getValue()
534 {
535 return value;
536 }
537
538 public Object setValue(Object o)
539 {
540 throw new UnsupportedOperationException();
541 }
542
543 @Override
544 public String toString()
545 {
546 if (string==null)
547 string=key+"="+value;
548 return string;
549 }
550
551 public String getMapped()
552 {
553 return mapped;
554 }
555
556 void setMapped(String mapped)
557 {
558 this.mapped = mapped;
559 }
560 }
561 }