Java tutorial
// ======================================================================== // $Id: URI.java,v 1.39 2006/01/04 13:55:31 gregwilkins Exp $ // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package net.lightbody.bmp.proxy.jetty.util; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import org.apache.commons.logging.Log; import java.io.UnsupportedEncodingException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /* ------------------------------------------------------------ */ /** URI Holder. * This class assists with the decoding and encoding or HTTP URI's. * It differs from the java.net.URL class as it does not provide * communications ability, but it does assist with query string * formatting. * <P>ISO_8859_1 encoding is used by default for % encoded characters. This * may be overridden with the org.mortbay.util.URI.charset system property. * @see UrlEncoded * @version $Id: URI.java,v 1.39 2006/01/04 13:55:31 gregwilkins Exp $ * @author Greg Wilkins (gregw) */ public class URI implements Cloneable { private static Log log = LogFactory.getLog(URI.class); public static final String __CHARSET = System.getProperty("net.lightbody.bmp.proxy.jetty.util.URI.charset", StringUtil.__UTF_8); public static final boolean __CHARSET_IS_DEFAULT = __CHARSET.equals(StringUtil.__UTF_8); /* ------------------------------------------------------------ */ private String _uri; private String _scheme; private String _host; private int _port; private String _path; private String _encodedPath; private String _query; private UrlEncoded _parameters; private boolean _dirty; /* ------------------------------------------------------------ */ /** Copy Constructor . * @param uri */ public URI(URI uri) throws IllegalArgumentException { _uri = uri.toString(); _scheme = uri._scheme; _host = uri._host; _port = uri._port; _path = uri._path; _encodedPath = uri._encodedPath; _query = uri._query; if (uri._parameters != null) _parameters = (UrlEncoded) uri._parameters.clone(); _dirty = false; } /* ------------------------------------------------------------ */ /** Construct from a String. * The string must contain a URI path, but optionaly may contain a * scheme, host, port and query string. * * @param uri [scheme://host[:port]]/path[?query] */ public URI(String uri) throws IllegalArgumentException { setURI(uri); } /* ------------------------------------------------------------ */ public void setURI(String uri) throws IllegalArgumentException { try { _uri = uri; _scheme = null; _host = null; _port = 0; _path = null; _encodedPath = null; _query = null; if (_parameters != null) _parameters.clear(); // Scan _uri for host, port, path & query int maxi = uri.length() - 1; int mark = 0; int state = 0; int i = 0; if (maxi == 0 || uri.charAt(0) == '/' && uri.charAt(1) != '/') { state = 3; _scheme = null; _host = null; _port = 0; } else { for (i = 0; state < 3 && i <= maxi; i++) { char c = uri.charAt(i); switch (state) { case 0: // looking for scheme or path if (c == ':' && uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') { // found end of scheme & start of host _scheme = uri.substring(mark, i); i += 2; mark = i + 1; state = 1; } else if (i == 0 && c == '/') { // Found path state = 3; } else if (i == 0 && c == '*') { state = 5; _path = "*"; _encodedPath = "*"; } continue; case 1: // Get host & look for port or path if (c == ':') { // found port _host = uri.substring(mark, i); mark = i + 1; state = 2; } else if (c == '/') { // found path _host = uri.substring(mark, i); mark = i; state = 3; } continue; case 2: // Get port & look for path if (c == '/') { _port = TypeUtil.parseInt(uri, mark, i - mark, 10); mark = i; state = 3; } continue; } } } // State 3 - Get path & look for query _query = null; for (i++; i <= maxi; i++) { char c = uri.charAt(i); if (c == '?') { // Found query _encodedPath = uri.substring(mark, i); _path = decodePath(_encodedPath); mark = i + 1; state = 4; break; } } // complete last state switch (state) { case 0: _dirty = false; _encodedPath = _uri; _path = decodePath(_encodedPath); break; case 1: _dirty = true; _encodedPath = "/"; _path = _encodedPath; _host = uri.substring(mark); break; case 2: _dirty = true; _encodedPath = "/"; _path = _encodedPath; _port = TypeUtil.parseInt(uri, mark, -1, 10); break; case 3: _dirty = (mark == maxi); _encodedPath = uri.substring(mark); _path = decodePath(_encodedPath); break; case 4: _dirty = false; if (mark <= maxi) _query = uri.substring(mark); break; case 5: _dirty = false; } if (_query != null && _query.length() > 0) { if (_parameters == null) _parameters = new UrlEncoded(); else _parameters.clear(); _parameters.decode(_query, __CHARSET); } else _query = null; } catch (Exception e) { LogSupport.ignore(log, e); throw new IllegalArgumentException("Malformed URI '" + uri + "' : " + e.toString()); } } /* ------------------------------------------------------------ */ /** Is the URI an absolute URL? * @return True if the URI has a scheme or host */ public boolean isAbsolute() { return _scheme != null || _host != null; } /* ------------------------------------------------------------ */ /** Get the uri scheme. * @return the URI scheme */ public String getScheme() { return _scheme; } /* ------------------------------------------------------------ */ /** Set the uri scheme. * @param scheme the uri scheme */ public void setScheme(String scheme) { _scheme = scheme; _dirty = true; } /* ------------------------------------------------------------ */ /** Get the uri host. * @return the URI host */ public String getHost() { return _host; } /* ------------------------------------------------------------ */ /** Set the uri host. * @param host the uri host */ public void setHost(String host) { _host = host; _dirty = true; } /* ------------------------------------------------------------ */ /** Get the uri port. * @return the URI port */ public int getPort() { return _port; } /* ------------------------------------------------------------ */ /** Set the uri port. * A port of 0 implies use the default port. * @param port the uri port */ public void setPort(int port) { _port = port; _dirty = true; } /* ------------------------------------------------------------ */ /** Get the uri path. * @return the URI path */ public String getPath() { return _path; } /* ------------------------------------------------------------ */ /** Get the encoded uri path. * @return the URI path */ public String getEncodedPath() { return _encodedPath; } /* ------------------------------------------------------------ */ /** Set the uri path. * @param path the URI path */ public void setPath(String path) { _path = path; _encodedPath = encodePath(_path); _dirty = true; } /* ------------------------------------------------------------ */ /** Get the uri query String. * @return the URI query string */ public String getQuery() { if (_dirty && _parameters != null) { _query = _parameters.encode(__CHARSET); if (_query != null && _query.length() == 0) _query = null; } return _query; } /* ------------------------------------------------------------ */ /** Set the uri query String. * @param query the URI query string */ public void setQuery(String query) { _query = query; if (_parameters != null) _parameters.clear(); else if (query != null) _parameters = new UrlEncoded(); if (query != null) _parameters.decode(query, __CHARSET); cleanURI(); } /* ------------------------------------------------------------ */ /** Get the uri query _parameters names. * @return Unmodifiable set of URI query _parameters names */ public Set getParameterNames() { if (_parameters == null) return Collections.EMPTY_SET; return _parameters.keySet(); } /* ------------------------------------------------------------ */ /** Get the uri query _parameters. * @return the URI query _parameters */ public MultiMap getParameters() { if (_parameters == null) _parameters = new UrlEncoded(); _dirty = true; return _parameters; } /* ------------------------------------------------------------ */ /** Get the uri query _parameters. * @return the URI query _parameters in an unmodifiable map. */ public Map getUnmodifiableParameters() { if (_parameters == null) return Collections.EMPTY_MAP; return Collections.unmodifiableMap(_parameters); } /* ------------------------------------------------------------ */ /** Add the uri query _parameters to a MultiMap */ public void putParametersTo(MultiMap map) { if (_parameters != null && _parameters.size() > 0) map.putAll(_parameters); } /* ------------------------------------------------------------ */ /** Clear the URI _parameters. */ public void clearParameters() { if (_parameters != null) { _dirty = true; _parameters.clear(); } } /* ------------------------------------------------------------ */ /** Add encoded _parameters. * @param encoded A HTTP encoded string of _parameters: e.g.. "a=1&b=2" */ public void put(String encoded) { UrlEncoded params = new UrlEncoded(encoded); put(params); } /* ------------------------------------------------------------ */ /** Add name value pair to the uri query _parameters. * @param name name of value * @param value The value, which may be a multi valued list or * String array. */ public Object put(Object name, Object value) { return getParameters().put(name, value); } /* ------------------------------------------------------------ */ /** Add dictionary to the uri query _parameters. */ public void put(Map values) { getParameters().putAll(values); } /* ------------------------------------------------------------ */ /** Get named value */ public String get(String name) { if (_parameters == null) return null; return (String) _parameters.get(name); } /* ------------------------------------------------------------ */ /** Get named multiple values. * @param name The parameter name * @return Umodifiable list of values or null */ public List getValues(String name) { if (_parameters == null) return null; return _parameters.getValues(name); } /* ------------------------------------------------------------ */ /** Remove named value */ public void remove(String name) { if (_parameters != null) { _dirty = _parameters.remove(name) != null; } } /* ------------------------------------------------------------ */ /** @return the URI string encoded. */ public String toString() { if (_dirty) { getQuery(); cleanURI(); } return _uri; } /* ------------------------------------------------------------ */ private void cleanURI() { StringBuffer buf = new StringBuffer(_uri.length() * 2); synchronized (buf) { if (_scheme != null) { buf.append(_scheme); buf.append("://"); buf.append(_host); if (_port > 0) { buf.append(':'); buf.append(_port); } } buf.append(_encodedPath); if (_query != null && _query.length() > 0) { buf.append('?'); buf.append(_query); } _uri = buf.toString(); _dirty = false; } } /* ------------------------------------------------------------ */ /** Encode a URI path. * This is the same encoding offered by URLEncoder, except that * the '/' character is not encoded. * @param path The path the encode * @return The encoded path */ public static String encodePath(String path) { if (path == null || path.length() == 0) return path; StringBuffer buf = encodePath(null, path); return buf == null ? path : buf.toString(); } /* ------------------------------------------------------------ */ /** Encode a URI path. * @param path The path the encode * @param buf StringBuffer to encode path into (or null) * @return The StringBuffer or null if no substitutions required. */ public static StringBuffer encodePath(StringBuffer buf, String path) { if (buf == null) { loop: for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); switch (c) { case '%': case '?': case ';': case '#': case ' ': buf = new StringBuffer(path.length() << 1); break loop; } } if (buf == null) return null; } synchronized (buf) { for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); switch (c) { case '%': buf.append("%25"); continue; case '?': buf.append("%3F"); continue; case ';': buf.append("%3B"); continue; case '#': buf.append("%23"); continue; case ' ': buf.append("%20"); continue; default: buf.append(c); continue; } } } return buf; } /* ------------------------------------------------------------ */ /** Encode a URI path. * @param path The path the encode * @param buf StringBuffer to encode path into (or null) * @param encode String of characters to encode. % is always encoded. * @return The StringBuffer or null if no substitutions required. */ public static StringBuffer encodeString(StringBuffer buf, String path, String encode) { if (buf == null) { loop: for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); if (c == '%' || encode.indexOf(c) >= 0) { buf = new StringBuffer(path.length() << 1); break loop; } } if (buf == null) return null; } synchronized (buf) { for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); if (c == '%' || encode.indexOf(c) >= 0) { buf.append('%'); StringUtil.append(buf, (byte) (0xff & c), 16); } else buf.append(c); } } return buf; } /* ------------------------------------------------------------ */ /* Decode a URI path. * @param path The path the encode * @param buf StringBuffer to encode path into */ public static String decodePath(String path) { int len = path.length(); byte[] bytes = null; int n = 0; boolean noDecode = true; for (int i = 0; i < len; i++) { char c = path.charAt(i); byte b = (byte) (0xff & c); if (c == '%' && (i + 2) < len) { noDecode = false; b = (byte) (0xff & TypeUtil.parseInt(path, i + 1, 2, 16)); i += 2; } else if (bytes == null) { n++; continue; } if (bytes == null) { noDecode = false; bytes = new byte[len]; for (int j = 0; j < n; j++) bytes[j] = (byte) (0xff & path.charAt(j)); } bytes[n++] = b; } if (noDecode) return path; try { return new String(bytes, 0, n, __CHARSET); } catch (UnsupportedEncodingException e) { log.warn(LogSupport.EXCEPTION, e); return new String(bytes, 0, n); } } /* ------------------------------------------------------------ */ /** Clone URI. * @return cloned URI */ public Object clone() throws CloneNotSupportedException { URI u = (URI) super.clone(); if (_parameters != null) u._parameters = (UrlEncoded) _parameters.clone(); _dirty = false; return u; } /* ------------------------------------------------------------ */ /** Add two URI path segments. * Handles null and empty paths, path and query params (eg ?a=b or * ;JSESSIONID=xxx) and avoids duplicate '/' * @param p1 URI path segment * @param p2 URI path segment * @return Legally combined path segments. */ public static String addPaths(String p1, String p2) { if (p1 == null || p1.length() == 0) { if (p2 == null || p2.length() == 0) return p1; return p2; } if (p2 == null || p2.length() == 0) return p1; int split = p1.indexOf(';'); if (split < 0) split = p1.indexOf('?'); if (split == 0) return p2 + p1; if (split < 0) split = p1.length(); StringBuffer buf = new StringBuffer(p1.length() + p2.length() + 2); buf.append(p1); if (buf.charAt(split - 1) == '/') { if (p2.startsWith("/")) { buf.deleteCharAt(split - 1); buf.insert(split - 1, p2); } else buf.insert(split, p2); } else { if (p2.startsWith("/")) buf.insert(split, p2); else { buf.insert(split, '/'); buf.insert(split + 1, p2); } } return buf.toString(); } /* ------------------------------------------------------------ */ /** Return the parent Path. * Treat a URI like a directory path and return the parent directory. */ public static String parentPath(String p) { if (p == null || "/".equals(p)) return null; int slash = p.lastIndexOf('/', p.length() - 2); if (slash >= 0) return p.substring(0, slash + 1); return null; } /* ------------------------------------------------------------ */ /** Strip parameters from a path. * Return path upto any semicolon parameters. */ public static String stripPath(String path) { if (path == null) return null; int semi = path.indexOf(';'); if (semi < 0) return path; return path.substring(0, semi); } /* ------------------------------------------------------------ */ /** Convert a path to a cananonical form. * All instances of "." and ".." are factored out. Null is returned * if the path tries to .. above it's root. * @param path * @return path or null. */ public static String canonicalPath(String path) { if (path == null || path.length() == 0) return path; int end = path.length(); int queryIdx = path.indexOf('?'); int start = path.lastIndexOf('/', (queryIdx > 0 ? queryIdx : end)); search: while (end > 0) { switch (end - start) { case 2: // possible single dot if (path.charAt(start + 1) != '.') break; break search; case 3: // possible double dot if (path.charAt(start + 1) != '.' || path.charAt(start + 2) != '.') break; break search; } end = start; start = path.lastIndexOf('/', end - 1); } // If we have checked the entire string if (start >= end) return path; StringBuffer buf = new StringBuffer(path); int delStart = -1; int delEnd = -1; int skip = 0; while (end > 0) { switch (end - start) { case 2: // possible single dot if (buf.charAt(start + 1) != '.') { if (skip > 0 && --skip == 0) { delStart = start >= 0 ? start : 0; if (delStart > 0 && delEnd == buf.length() && buf.charAt(delEnd - 1) == '.') delStart++; } break; } if (start < 0 && buf.length() > 2 && buf.charAt(1) == '/' && buf.charAt(2) == '/') break; if (delEnd < 0) delEnd = end; delStart = start; if (delStart < 0 || delStart == 0 && buf.charAt(delStart) == '/') { delStart++; if (delEnd < buf.length() && buf.charAt(delEnd) == '/') delEnd++; break; } if (end == buf.length()) delStart++; end = start--; while (start >= 0 && buf.charAt(start) != '/') start--; continue; case 3: // possible double dot if (buf.charAt(start + 1) != '.' || buf.charAt(start + 2) != '.') { if (skip > 0 && --skip == 0) { delStart = start >= 0 ? start : 0; if (delStart > 0 && delEnd == buf.length() && buf.charAt(delEnd - 1) == '.') delStart++; } break; } delStart = start; if (delEnd < 0) delEnd = end; skip++; end = start--; while (start >= 0 && buf.charAt(start) != '/') start--; continue; default: if (skip > 0 && --skip == 0) { delStart = start >= 0 ? start : 0; if (delEnd == buf.length() && buf.charAt(delEnd - 1) == '.') delStart++; } } // Do the delete if (skip <= 0 && delStart >= 0 && delStart >= 0) { buf.delete(delStart, delEnd); delStart = delEnd = -1; if (skip > 0) delEnd = end; } end = start--; while (start >= 0 && buf.charAt(start) != '/') start--; } // Too many .. if (skip > 0) return null; // Do the delete if (delEnd >= 0) buf.delete(delStart, delEnd); return buf.toString(); } /* ------------------------------------------------------------ */ /** * @param uri URI * @return True if the uri has a scheme */ public static boolean hasScheme(String uri) { for (int i = 0; i < uri.length(); i++) { char c = uri.charAt(i); if (c == ':') return true; if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || (i > 0 && (c >= '0' && c <= '9' || c == '.' || c == '+' || c == '-')))) break; } return false; } }