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