001: // ========================================================================
002: // Copyright 1999-2005 Mort Bay Consulting Pty. Ltd.
003: // ------------------------------------------------------------------------
004: // Licensed under the Apache License, Version 2.0 (the "License");
005: // you may not use this file except in compliance with the License.
006: // You may obtain a copy of the License at
007: // http://www.apache.org/licenses/LICENSE-2.0
008: // Unless required by applicable law or agreed to in writing, software
009: // distributed under the License is distributed on an "AS IS" BASIS,
010: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011: // See the License for the specific language governing permissions and
012: // limitations under the License.
013: // ========================================================================
014:
015: package org.mortbay.jetty.servlet;
016:
017: import java.io.Externalizable;
018: import java.util.HashMap;
019: import java.util.List;
020: import java.util.Map;
021: import java.util.Set;
022: import java.util.StringTokenizer;
023:
024: import org.mortbay.util.LazyList;
025: import org.mortbay.util.SingletonList;
026: import org.mortbay.util.StringMap;
027:
028: /* ------------------------------------------------------------ */
029: /** URI path map to Object.
030: * This mapping implements the path specification recommended
031: * in the 2.2 Servlet API.
032: *
033: * Path specifications can be of the following forms:<PRE>
034: * /foo/bar - an exact path specification.
035: * /foo/* - a prefix path specification (must end '/*').
036: * *.ext - a suffix path specification.
037: * / - the default path specification.
038: * </PRE>
039: * Matching is performed in the following order <NL>
040: * <LI>Exact match.
041: * <LI>Longest prefix match.
042: * <LI>Longest suffix match.
043: * <LI>default.
044: * </NL>
045: * Multiple path specifications can be mapped by providing a list of
046: * specifications. The list is separated by the characters specified
047: * in the "org.mortbay.http.PathMap.separators" System property, which
048: * defaults to :
049: * <P>
050: * Special characters within paths such as '?� and ';' are not treated specially
051: * as it is assumed they would have been either encoded in the original URL or
052: * stripped from the path.
053: * <P>
054: * This class is not synchronized for get's. If concurrent modifications are
055: * possible then it should be synchronized at a higher level.
056: *
057: * @author Greg Wilkins (gregw)
058: */
059: public class PathMap extends HashMap implements Externalizable {
060: /* ------------------------------------------------------------ */
061: private static String __pathSpecSeparators = System.getProperty(
062: "org.mortbay.http.PathMap.separators", ":,");
063:
064: /* ------------------------------------------------------------ */
065: /** Set the path spec separator.
066: * Multiple path specification may be included in a single string
067: * if they are separated by the characters set in this string.
068: * The default value is ":," or whatever has been set by the
069: * system property org.mortbay.http.PathMap.separators
070: * @param s separators
071: */
072: public static void setPathSpecSeparators(String s) {
073: __pathSpecSeparators = s;
074: }
075:
076: /* --------------------------------------------------------------- */
077: StringMap _prefixMap = new StringMap();
078: StringMap _suffixMap = new StringMap();
079: StringMap _exactMap = new StringMap();
080:
081: List _defaultSingletonList = null;
082: Entry _prefixDefault = null;
083: Entry _default = null;
084: Set _entrySet;
085: boolean _nodefault = false;
086:
087: /* --------------------------------------------------------------- */
088: /** Construct empty PathMap.
089: */
090: public PathMap() {
091: super (11);
092: _entrySet = entrySet();
093: }
094:
095: /* --------------------------------------------------------------- */
096: /** Construct empty PathMap.
097: */
098: public PathMap(boolean nodefault) {
099: super (11);
100: _entrySet = entrySet();
101: _nodefault = nodefault;
102: }
103:
104: /* --------------------------------------------------------------- */
105: /** Construct empty PathMap.
106: */
107: public PathMap(int capacity) {
108: super (capacity);
109: _entrySet = entrySet();
110: }
111:
112: /* --------------------------------------------------------------- */
113: /** Construct from dictionary PathMap.
114: */
115: public PathMap(Map m) {
116: putAll(m);
117: _entrySet = entrySet();
118: }
119:
120: /* ------------------------------------------------------------ */
121: public void writeExternal(java.io.ObjectOutput out)
122: throws java.io.IOException {
123: HashMap map = new HashMap(this );
124: out.writeObject(map);
125: }
126:
127: /* ------------------------------------------------------------ */
128: public void readExternal(java.io.ObjectInput in)
129: throws java.io.IOException, ClassNotFoundException {
130: HashMap map = (HashMap) in.readObject();
131: this .putAll(map);
132: }
133:
134: /* --------------------------------------------------------------- */
135: /** Add a single path match to the PathMap.
136: * @param pathSpec The path specification, or comma separated list of
137: * path specifications.
138: * @param object The object the path maps to
139: */
140: public synchronized Object put(Object pathSpec, Object object) {
141: StringTokenizer tok = new StringTokenizer(pathSpec.toString(),
142: __pathSpecSeparators);
143: Object old = null;
144:
145: while (tok.hasMoreTokens()) {
146: String spec = tok.nextToken();
147:
148: if (!spec.startsWith("/") && !spec.startsWith("*."))
149: throw new IllegalArgumentException("PathSpec " + spec
150: + ". must start with '/' or '*.'");
151:
152: old = super .put(spec, object);
153:
154: // Make entry that was just created.
155: Entry entry = new Entry(spec, object);
156:
157: if (entry.getKey().equals(spec)) {
158: if (spec.equals("/*"))
159: _prefixDefault = entry;
160: else if (spec.endsWith("/*")) {
161: String mapped = spec
162: .substring(0, spec.length() - 2);
163: entry.setMapped(mapped);
164: _prefixMap.put(mapped, entry);
165: _exactMap.put(mapped, entry);
166: _exactMap.put(spec.substring(0, spec.length() - 1),
167: entry);
168: } else if (spec.startsWith("*."))
169: _suffixMap.put(spec.substring(2), entry);
170: else if (spec.equals("/")) {
171: if (_nodefault)
172: _exactMap.put(spec, entry);
173: else {
174: _default = entry;
175: _defaultSingletonList = SingletonList
176: .newSingletonList(_default);
177: }
178: } else {
179: entry.setMapped(spec);
180: _exactMap.put(spec, entry);
181: }
182: }
183: }
184:
185: return old;
186: }
187:
188: /* ------------------------------------------------------------ */
189: /** Get object matched by the path.
190: * @param path the path.
191: * @return Best matched object or null.
192: */
193: public Object match(String path) {
194: Map.Entry entry = getMatch(path);
195: if (entry != null)
196: return entry.getValue();
197: return null;
198: }
199:
200: /* --------------------------------------------------------------- */
201: /** Get the entry mapped by the best specification.
202: * @param path the path.
203: * @return Map.Entry of the best matched or null.
204: */
205: public Entry getMatch(String path) {
206: Map.Entry entry;
207:
208: if (path == null)
209: return null;
210:
211: int l = path.length();
212:
213: // try exact match
214: entry = _exactMap.getEntry(path, 0, l);
215: if (entry != null)
216: return (Entry) entry.getValue();
217:
218: // prefix search
219: int i = l;
220: while ((i = path.lastIndexOf('/', i - 1)) >= 0) {
221: entry = _prefixMap.getEntry(path, 0, i);
222: if (entry != null)
223: return (Entry) entry.getValue();
224: }
225:
226: // Prefix Default
227: if (_prefixDefault != null)
228: return _prefixDefault;
229:
230: // Extension search
231: i = 0;
232: while ((i = path.indexOf('.', i + 1)) > 0) {
233: entry = _suffixMap.getEntry(path, i + 1, l - i - 1);
234: if (entry != null)
235: return (Entry) entry.getValue();
236: }
237:
238: // Default
239: return _default;
240: }
241:
242: /* --------------------------------------------------------------- */
243: /** Get all entries matched by the path.
244: * Best match first.
245: * @param path Path to match
246: * @return LazyList of Map.Entry instances key=pathSpec
247: */
248: public Object getLazyMatches(String path) {
249: Map.Entry entry;
250: Object entries = null;
251:
252: if (path == null)
253: return LazyList.getList(entries);
254:
255: int l = path.length();
256:
257: // try exact match
258: entry = _exactMap.getEntry(path, 0, l);
259: if (entry != null)
260: entries = LazyList.add(entries, entry.getValue());
261:
262: // prefix search
263: int i = l - 1;
264: while ((i = path.lastIndexOf('/', i - 1)) >= 0) {
265: entry = _prefixMap.getEntry(path, 0, i);
266: if (entry != null)
267: entries = LazyList.add(entries, entry.getValue());
268: }
269:
270: // Prefix Default
271: if (_prefixDefault != null)
272: entries = LazyList.add(entries, _prefixDefault);
273:
274: // Extension search
275: i = 0;
276: while ((i = path.indexOf('.', i + 1)) > 0) {
277: entry = _suffixMap.getEntry(path, i + 1, l - i - 1);
278: if (entry != null)
279: entries = LazyList.add(entries, entry.getValue());
280: }
281:
282: // Default
283: if (_default != null) {
284: // Optimization for just the default
285: if (entries == null)
286: return _defaultSingletonList;
287:
288: entries = LazyList.add(entries, _default);
289: }
290:
291: return entries;
292: }
293:
294: /* --------------------------------------------------------------- */
295: /** Get all entries matched by the path.
296: * Best match first.
297: * @param path Path to match
298: * @return List of Map.Entry instances key=pathSpec
299: */
300: public List getMatches(String path) {
301: return LazyList.getList(getLazyMatches(path));
302: }
303:
304: /* --------------------------------------------------------------- */
305: public synchronized Object remove(Object pathSpec) {
306: if (pathSpec != null) {
307: String spec = (String) pathSpec;
308: if (spec.equals("/*"))
309: _prefixDefault = null;
310: else if (spec.endsWith("/*")) {
311: _prefixMap.remove(spec.substring(0, spec.length() - 2));
312: _exactMap.remove(spec.substring(0, spec.length() - 1));
313: _exactMap.remove(spec.substring(0, spec.length() - 2));
314: } else if (spec.startsWith("*."))
315: _suffixMap.remove(spec.substring(2));
316: else if (spec.equals("/")) {
317: _default = null;
318: _defaultSingletonList = null;
319: } else
320: _exactMap.remove(spec);
321: }
322: return super .remove(pathSpec);
323: }
324:
325: /* --------------------------------------------------------------- */
326: public void clear() {
327: _exactMap = new StringMap();
328: _prefixMap = new StringMap();
329: _suffixMap = new StringMap();
330: _default = null;
331: _defaultSingletonList = null;
332: super .clear();
333: }
334:
335: /* --------------------------------------------------------------- */
336: /**
337: * @return true if match.
338: */
339: public static boolean match(String pathSpec, String path)
340: throws IllegalArgumentException {
341: return match(pathSpec, path, false);
342: }
343:
344: /* --------------------------------------------------------------- */
345: /**
346: * @return true if match.
347: */
348: public static boolean match(String pathSpec, String path,
349: boolean noDefault) throws IllegalArgumentException {
350: char c = pathSpec.charAt(0);
351: if (c == '/') {
352: if (!noDefault && pathSpec.length() == 1
353: || pathSpec.equals(path))
354: return true;
355:
356: if (isPathWildcardMatch(pathSpec, path))
357: return true;
358: } else if (c == '*')
359: return path.regionMatches(path.length() - pathSpec.length()
360: + 1, pathSpec, 1, pathSpec.length() - 1);
361: return false;
362: }
363:
364: /* --------------------------------------------------------------- */
365: private static boolean isPathWildcardMatch(String pathSpec,
366: String path) {
367: // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
368: int cpl = pathSpec.length() - 2;
369: if (pathSpec.endsWith("/*")
370: && path.regionMatches(0, pathSpec, 0, cpl)) {
371: if (path.length() == cpl || '/' == path.charAt(cpl))
372: return true;
373: }
374: return false;
375: }
376:
377: /* --------------------------------------------------------------- */
378: /** Return the portion of a path that matches a path spec.
379: * @return null if no match at all.
380: */
381: public static String pathMatch(String pathSpec, String path) {
382: char c = pathSpec.charAt(0);
383:
384: if (c == '/') {
385: if (pathSpec.length() == 1)
386: return path;
387:
388: if (pathSpec.equals(path))
389: return path;
390:
391: if (isPathWildcardMatch(pathSpec, path))
392: return path.substring(0, pathSpec.length() - 2);
393: } else if (c == '*') {
394: if (path.regionMatches(path.length()
395: - (pathSpec.length() - 1), pathSpec, 1, pathSpec
396: .length() - 1))
397: return path;
398: }
399: return null;
400: }
401:
402: /* --------------------------------------------------------------- */
403: /** Return the portion of a path that is after a path spec.
404: * @return The path info string
405: */
406: public static String pathInfo(String pathSpec, String path) {
407: char c = pathSpec.charAt(0);
408:
409: if (c == '/') {
410: if (pathSpec.length() == 1)
411: return null;
412:
413: if (pathSpec.equals(path))
414: return null;
415:
416: if (isPathWildcardMatch(pathSpec, path)) {
417: if (path.length() == pathSpec.length() - 2)
418: return null;
419: return path.substring(pathSpec.length() - 2);
420: }
421: }
422: return null;
423: }
424:
425: /* ------------------------------------------------------------ */
426: /** Relative path.
427: * @param base The base the path is relative to.
428: * @param pathSpec The spec of the path segment to ignore.
429: * @param path the additional path
430: * @return base plus path with pathspec removed
431: */
432: public static String relativePath(String base, String pathSpec,
433: String path) {
434: String info = pathInfo(pathSpec, path);
435: if (info == null)
436: info = path;
437:
438: if (info.startsWith("./"))
439: info = info.substring(2);
440: if (base.endsWith("/"))
441: if (info.startsWith("/"))
442: path = base + info.substring(1);
443: else
444: path = base + info;
445: else if (info.startsWith("/"))
446: path = base + info;
447: else
448: path = base + "/" + info;
449: return path;
450: }
451:
452: /* ------------------------------------------------------------ */
453: /* ------------------------------------------------------------ */
454: /* ------------------------------------------------------------ */
455: public static class Entry implements Map.Entry {
456: private Object key;
457: private Object value;
458: private String mapped;
459: private transient String string;
460:
461: Entry(Object key, Object value) {
462: this .key = key;
463: this .value = value;
464: }
465:
466: public Object getKey() {
467: return key;
468: }
469:
470: public Object getValue() {
471: return value;
472: }
473:
474: public Object setValue(Object o) {
475: throw new UnsupportedOperationException();
476: }
477:
478: public String toString() {
479: if (string == null)
480: string = key + "=" + value;
481: return string;
482: }
483:
484: public String getMapped() {
485: return mapped;
486: }
487:
488: void setMapped(String mapped) {
489: this.mapped = mapped;
490: }
491: }
492: }
|