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. The list is separated by the characters specified
47 * in the "org.eclipse.http.PathMap.separators" System property, which
48 * defaults to :
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 for get's. 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 System.getProperty("org.eclipse.http.PathMap.separators",":,");
64
65 /* ------------------------------------------------------------ */
66 /** Set the path spec separator.
67 * Multiple path specification may be included in a single string
68 * if they are separated by the characters set in this string.
69 * The default value is ":," or whatever has been set by the
70 * system property org.eclipse.http.PathMap.separators
71 * @param s separators
72 */
73 public static void setPathSpecSeparators(String s)
74 {
75 __pathSpecSeparators=s;
76 }
77
78 /* --------------------------------------------------------------- */
79 final StringMap _prefixMap=new StringMap();
80 final StringMap _suffixMap=new StringMap();
81 final StringMap _exactMap=new StringMap();
82
83 List _defaultSingletonList=null;
84 Entry _prefixDefault=null;
85 Entry _default=null;
86 Set _entrySet;
87 boolean _nodefault=false;
88
89 /* --------------------------------------------------------------- */
90 /** Construct empty PathMap.
91 */
92 public PathMap()
93 {
94 super(11);
95 _entrySet=entrySet();
96 }
97
98 /* --------------------------------------------------------------- */
99 /** Construct empty PathMap.
100 */
101 public PathMap(boolean nodefault)
102 {
103 super(11);
104 _entrySet=entrySet();
105 _nodefault=nodefault;
106 }
107
108 /* --------------------------------------------------------------- */
109 /** Construct empty PathMap.
110 */
111 public PathMap(int capacity)
112 {
113 super (capacity);
114 _entrySet=entrySet();
115 }
116
117 /* --------------------------------------------------------------- */
118 /** Construct from dictionary PathMap.
119 */
120 public PathMap(Map m)
121 {
122 putAll(m);
123 _entrySet=entrySet();
124 }
125
126 /* ------------------------------------------------------------ */
127 public void writeExternal(java.io.ObjectOutput out)
128 throws java.io.IOException
129 {
130 HashMap map = new HashMap(this);
131 out.writeObject(map);
132 }
133
134 /* ------------------------------------------------------------ */
135 public void readExternal(java.io.ObjectInput in)
136 throws java.io.IOException, ClassNotFoundException
137 {
138 HashMap map = (HashMap)in.readObject();
139 this.putAll(map);
140 }
141
142 /* --------------------------------------------------------------- */
143 /** Add a single path match to the PathMap.
144 * @param pathSpec The path specification, or comma separated list of
145 * path specifications.
146 * @param object The object the path maps to
147 */
148 public synchronized Object put(Object pathSpec, Object object)
149 {
150 StringTokenizer tok = new StringTokenizer(pathSpec.toString(),__pathSpecSeparators);
151 Object old =null;
152
153 while (tok.hasMoreTokens())
154 {
155 String spec=tok.nextToken();
156
157 if (!spec.startsWith("/") && !spec.startsWith("*."))
158 throw new IllegalArgumentException("PathSpec "+spec+". must start with '/' or '*.'");
159
160 old = super.put(spec,object);
161
162 // Make entry that was just created.
163 Entry entry = new Entry(spec,object);
164
165 if (entry.getKey().equals(spec))
166 {
167 if (spec.equals("/*"))
168 _prefixDefault=entry;
169 else if (spec.endsWith("/*"))
170 {
171 String mapped=spec.substring(0,spec.length()-2);
172 entry.setMapped(mapped);
173 _prefixMap.put(mapped,entry);
174 _exactMap.put(mapped,entry);
175 _exactMap.put(spec.substring(0,spec.length()-1),entry);
176 }
177 else if (spec.startsWith("*."))
178 _suffixMap.put(spec.substring(2),entry);
179 else if (spec.equals(URIUtil.SLASH))
180 {
181 if (_nodefault)
182 _exactMap.put(spec,entry);
183 else
184 {
185 _default=entry;
186 _defaultSingletonList=
187 SingletonList.newSingletonList(_default);
188 }
189 }
190 else
191 {
192 entry.setMapped(spec);
193 _exactMap.put(spec,entry);
194 }
195 }
196 }
197
198 return old;
199 }
200
201 /* ------------------------------------------------------------ */
202 /** Get object matched by the path.
203 * @param path the path.
204 * @return Best matched object or null.
205 */
206 public Object match(String path)
207 {
208 Map.Entry entry = getMatch(path);
209 if (entry!=null)
210 return entry.getValue();
211 return null;
212 }
213
214
215 /* --------------------------------------------------------------- */
216 /** Get the entry mapped by the best specification.
217 * @param path the path.
218 * @return Map.Entry of the best matched or null.
219 */
220 public Entry getMatch(String path)
221 {
222 Map.Entry entry;
223
224 if (path==null)
225 return null;
226
227 int l=path.length();
228
229 // try exact match
230 entry=_exactMap.getEntry(path,0,l);
231 if (entry!=null)
232 return (Entry) entry.getValue();
233
234 // prefix search
235 int i=l;
236 while((i=path.lastIndexOf('/',i-1))>=0)
237 {
238 entry=_prefixMap.getEntry(path,0,i);
239 if (entry!=null)
240 return (Entry) entry.getValue();
241 }
242
243 // Prefix Default
244 if (_prefixDefault!=null)
245 return _prefixDefault;
246
247 // Extension search
248 i=0;
249 while ((i=path.indexOf('.',i+1))>0)
250 {
251 entry=_suffixMap.getEntry(path,i+1,l-i-1);
252 if (entry!=null)
253 return (Entry) entry.getValue();
254 }
255
256 // Default
257 return _default;
258 }
259
260 /* --------------------------------------------------------------- */
261 /** Get all entries matched by the path.
262 * Best match first.
263 * @param path Path to match
264 * @return LazyList of Map.Entry instances key=pathSpec
265 */
266 public Object getLazyMatches(String path)
267 {
268 Map.Entry entry;
269 Object entries=null;
270
271 if (path==null)
272 return LazyList.getList(entries);
273
274 int l=path.length();
275
276 // try exact match
277 entry=_exactMap.getEntry(path,0,l);
278 if (entry!=null)
279 entries=LazyList.add(entries,entry.getValue());
280
281 // prefix search
282 int i=l-1;
283 while((i=path.lastIndexOf('/',i-1))>=0)
284 {
285 entry=_prefixMap.getEntry(path,0,i);
286 if (entry!=null)
287 entries=LazyList.add(entries,entry.getValue());
288 }
289
290 // Prefix Default
291 if (_prefixDefault!=null)
292 entries=LazyList.add(entries,_prefixDefault);
293
294 // Extension search
295 i=0;
296 while ((i=path.indexOf('.',i+1))>0)
297 {
298 entry=_suffixMap.getEntry(path,i+1,l-i-1);
299 if (entry!=null)
300 entries=LazyList.add(entries,entry.getValue());
301 }
302
303 // Default
304 if (_default!=null)
305 {
306 // Optimization for just the default
307 if (entries==null)
308 return _defaultSingletonList;
309
310 entries=LazyList.add(entries,_default);
311 }
312
313 return entries;
314 }
315
316 /* --------------------------------------------------------------- */
317 /** Get all entries matched by the path.
318 * Best match first.
319 * @param path Path to match
320 * @return List of Map.Entry instances key=pathSpec
321 */
322 public List getMatches(String path)
323 {
324 return LazyList.getList(getLazyMatches(path));
325 }
326
327 /* --------------------------------------------------------------- */
328 public synchronized Object remove(Object pathSpec)
329 {
330 if (pathSpec!=null)
331 {
332 String spec=(String) pathSpec;
333 if (spec.equals("/*"))
334 _prefixDefault=null;
335 else if (spec.endsWith("/*"))
336 {
337 _prefixMap.remove(spec.substring(0,spec.length()-2));
338 _exactMap.remove(spec.substring(0,spec.length()-1));
339 _exactMap.remove(spec.substring(0,spec.length()-2));
340 }
341 else if (spec.startsWith("*."))
342 _suffixMap.remove(spec.substring(2));
343 else if (spec.equals(URIUtil.SLASH))
344 {
345 _default=null;
346 _defaultSingletonList=null;
347 }
348 else
349 _exactMap.remove(spec);
350 }
351 return super.remove(pathSpec);
352 }
353
354 /* --------------------------------------------------------------- */
355 public void clear()
356 {
357 _exactMap.clear();
358 _prefixMap.clear();
359 _suffixMap.clear();
360 _default=null;
361 _defaultSingletonList=null;
362 super.clear();
363 }
364
365 /* --------------------------------------------------------------- */
366 /**
367 * @return true if match.
368 */
369 public static boolean match(String pathSpec, String path)
370 throws IllegalArgumentException
371 {
372 return match(pathSpec, path, false);
373 }
374
375 /* --------------------------------------------------------------- */
376 /**
377 * @return true if match.
378 */
379 public static boolean match(String pathSpec, String path, boolean noDefault)
380 throws IllegalArgumentException
381 {
382 char c = pathSpec.charAt(0);
383 if (c=='/')
384 {
385 if (!noDefault && pathSpec.length()==1 || pathSpec.equals(path))
386 return true;
387
388 if(isPathWildcardMatch(pathSpec, path))
389 return true;
390 }
391 else if (c=='*')
392 return path.regionMatches(path.length()-pathSpec.length()+1,
393 pathSpec,1,pathSpec.length()-1);
394 return false;
395 }
396
397 /* --------------------------------------------------------------- */
398 private static boolean isPathWildcardMatch(String pathSpec, String path)
399 {
400 // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
401 int cpl=pathSpec.length()-2;
402 if (pathSpec.endsWith("/*") && path.regionMatches(0,pathSpec,0,cpl))
403 {
404 if (path.length()==cpl || '/'==path.charAt(cpl))
405 return true;
406 }
407 return false;
408 }
409
410
411 /* --------------------------------------------------------------- */
412 /** Return the portion of a path that matches a path spec.
413 * @return null if no match at all.
414 */
415 public static String pathMatch(String pathSpec, String path)
416 {
417 char c = pathSpec.charAt(0);
418
419 if (c=='/')
420 {
421 if (pathSpec.length()==1)
422 return path;
423
424 if (pathSpec.equals(path))
425 return path;
426
427 if (isPathWildcardMatch(pathSpec, path))
428 return path.substring(0,pathSpec.length()-2);
429 }
430 else if (c=='*')
431 {
432 if (path.regionMatches(path.length()-(pathSpec.length()-1),
433 pathSpec,1,pathSpec.length()-1))
434 return path;
435 }
436 return null;
437 }
438
439 /* --------------------------------------------------------------- */
440 /** Return the portion of a path that is after a path spec.
441 * @return The path info string
442 */
443 public static String pathInfo(String pathSpec, String path)
444 {
445 char c = pathSpec.charAt(0);
446
447 if (c=='/')
448 {
449 if (pathSpec.length()==1)
450 return null;
451
452 if (pathSpec.equals(path))
453 return null;
454
455 if (isPathWildcardMatch(pathSpec, path))
456 {
457 if (path.length()==pathSpec.length()-2)
458 return null;
459 return path.substring(pathSpec.length()-2);
460 }
461 }
462 return null;
463 }
464
465
466 /* ------------------------------------------------------------ */
467 /** Relative path.
468 * @param base The base the path is relative to.
469 * @param pathSpec The spec of the path segment to ignore.
470 * @param path the additional path
471 * @return base plus path with pathspec removed
472 */
473 public static String relativePath(String base,
474 String pathSpec,
475 String path )
476 {
477 String info=pathInfo(pathSpec,path);
478 if (info==null)
479 info=path;
480
481 if( info.startsWith( "./"))
482 info = info.substring( 2);
483 if( base.endsWith( URIUtil.SLASH))
484 if( info.startsWith( URIUtil.SLASH))
485 path = base + info.substring(1);
486 else
487 path = base + info;
488 else
489 if( info.startsWith( URIUtil.SLASH))
490 path = base + info;
491 else
492 path = base + URIUtil.SLASH + info;
493 return path;
494 }
495
496 /* ------------------------------------------------------------ */
497 /* ------------------------------------------------------------ */
498 /* ------------------------------------------------------------ */
499 public static class Entry implements Map.Entry
500 {
501 private Object key;
502 private Object value;
503 private String mapped;
504 private transient String string;
505
506 Entry(Object key, Object value)
507 {
508 this.key=key;
509 this.value=value;
510 }
511
512 public Object getKey()
513 {
514 return key;
515 }
516
517 public Object getValue()
518 {
519 return value;
520 }
521
522 public Object setValue(Object o)
523 {
524 throw new UnsupportedOperationException();
525 }
526
527 public String toString()
528 {
529 if (string==null)
530 string=key+"="+value;
531 return string;
532 }
533
534 public String getMapped()
535 {
536 return mapped;
537 }
538
539 void setMapped(String mapped)
540 {
541 this.mapped = mapped;
542 }
543 }
544 }