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