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 public synchronized Object remove(Object pathSpec)
330 {
331 if (pathSpec!=null)
332 {
333 String spec=(String) pathSpec;
334 if (spec.equals("/*"))
335 _prefixDefault=null;
336 else if (spec.endsWith("/*"))
337 {
338 _prefixMap.remove(spec.substring(0,spec.length()-2));
339 _exactMap.remove(spec.substring(0,spec.length()-1));
340 _exactMap.remove(spec.substring(0,spec.length()-2));
341 }
342 else if (spec.startsWith("*."))
343 _suffixMap.remove(spec.substring(2));
344 else if (spec.equals(URIUtil.SLASH))
345 {
346 _default=null;
347 _defaultSingletonList=null;
348 }
349 else
350 _exactMap.remove(spec);
351 }
352 return super.remove(pathSpec);
353 }
354
355 /* --------------------------------------------------------------- */
356 public void clear()
357 {
358 _exactMap=new StringMap();
359 _prefixMap=new StringMap();
360 _suffixMap=new StringMap();
361 _default=null;
362 _defaultSingletonList=null;
363 super.clear();
364 }
365
366 /* --------------------------------------------------------------- */
367 /**
368 * @return true if match.
369 */
370 public static boolean match(String pathSpec, String path)
371 throws IllegalArgumentException
372 {
373 return match(pathSpec, path, false);
374 }
375
376 /* --------------------------------------------------------------- */
377 /**
378 * @return true if match.
379 */
380 public static boolean match(String pathSpec, String path, boolean noDefault)
381 throws IllegalArgumentException
382 {
383 char c = pathSpec.charAt(0);
384 if (c=='/')
385 {
386 if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
387 return true;
388
389 if(isPathWildcardMatch(pathSpec, path))
390 return true;
391 }
392 else if (c=='*')
393 return path.regionMatches(path.length()-pathSpec.length()+1,
394 pathSpec,1,pathSpec.length()-1);
395 return false;
396 }
397
398 /* --------------------------------------------------------------- */
399 private static boolean isPathWildcardMatch(String pathSpec, String path)
400 {
401 // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
402 int cpl=pathSpec.length()-2;
403 if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
404 {
405 if (path.length()==cpl || '/'==path.charAt(cpl))
406 return true;
407 }
408 return false;
409 }
410
411
412 /* --------------------------------------------------------------- */
413 /** Return the portion of a path that matches a path spec.
414 * @return null if no match at all.
415 */
416 public static String pathMatch(String pathSpec, String path)
417 {
418 char c = pathSpec.charAt(0);
419
420 if (c=='/')
421 {
422 if (pathSpec.length()==1)
423 return path;
424
425 if (pathSpec.equals(path))
426 return path;
427
428 if (isPathWildcardMatch(pathSpec, path))
429 return path.substring(0,pathSpec.length()-2);
430 }
431 else if (c=='*')
432 {
433 if (path.regionMatches(path.length()-(pathSpec.length()-1),
434 pathSpec,1,pathSpec.length()-1))
435 return path;
436 }
437 return null;
438 }
439
440 /* --------------------------------------------------------------- */
441 /** Return the portion of a path that is after a path spec.
442 * @return The path info string
443 */
444 public static String pathInfo(String pathSpec, String path)
445 {
446 char c = pathSpec.charAt(0);
447
448 if (c=='/')
449 {
450 if (pathSpec.length()==1)
451 return null;
452
453 if (pathSpec.equals(path))
454 return null;
455
456 if (isPathWildcardMatch(pathSpec, path))
457 {
458 if (path.length()==pathSpec.length()-2)
459 return null;
460 return path.substring(pathSpec.length()-2);
461 }
462 }
463 return null;
464 }
465
466
467 /* ------------------------------------------------------------ */
468 /** Relative path.
469 * @param base The base the path is relative to.
470 * @param pathSpec The spec of the path segment to ignore.
471 * @param path the additional path
472 * @return base plus path with pathspec removed
473 */
474 public static String relativePath(String base,
475 String pathSpec,
476 String path )
477 {
478 String info=pathInfo(pathSpec,path);
479 if (info==null)
480 info=path;
481
482 if( info.startsWith( "./"))
483 info = info.substring( 2);
484 if( base.endsWith( URIUtil.SLASH))
485 if( info.startsWith( URIUtil.SLASH))
486 path = base + info.substring(1);
487 else
488 path = base + info;
489 else
490 if( info.startsWith( URIUtil.SLASH))
491 path = base + info;
492 else
493 path = base + URIUtil.SLASH + info;
494 return path;
495 }
496
497 /* ------------------------------------------------------------ */
498 /* ------------------------------------------------------------ */
499 /* ------------------------------------------------------------ */
500 public static class Entry implements Map.Entry
501 {
502 private Object key;
503 private Object value;
504 private String mapped;
505 private transient String string;
506
507 Entry(Object key, Object value)
508 {
509 this.key=key;
510 this.value=value;
511 }
512
513 public Object getKey()
514 {
515 return key;
516 }
517
518 public Object getValue()
519 {
520 return value;
521 }
522
523 public Object setValue(Object o)
524 {
525 throw new UnsupportedOperationException();
526 }
527
528 public String toString()
529 {
530 if (string==null)
531 string=key+"="+value;
532 return string;
533 }
534
535 public String getMapped()
536 {
537 return mapped;
538 }
539
540 void setMapped(String mapped)
541 {
542 this.mapped = mapped;
543 }
544 }
545 }