package er.extensions.foundation; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.apache.commons.lang3.CharEncoding; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; /** * ERXMutableURL provides a mutable model of a URL, including support for * storing relative "URLs" in addition to the traditional absolute URL provided * by the core Java URL object. * * @author mschrag */ public class ERXMutableURL { private String _protocol; private String _host; private String _path; private String _ref; private Integer _port; private Map<String, List<String>> _queryParameters; /** * Constructs a blank ERXMutableURL. */ public ERXMutableURL() { _queryParameters = new LinkedHashMap<String, List<String>>(); } /** * Constructs an ERXMutableURL with all of the properties of the given URL. * * @param url * the URL to copy data from * @throws MalformedURLException if the URL is invalid */ public ERXMutableURL(URL url) throws MalformedURLException { this(); setURL(url); } /** * Constructs an ERXMutableURL with all of the properties of the given * external form of a URL. * * @param str * a URL external form * @throws MalformedURLException if the URL is invalid */ public ERXMutableURL(String str) throws MalformedURLException { this(); setURL(str); } /** * Sets the contents of this ERXMutableURL to be the same as the given URL. * * @param url * the url to copy the contents from * @throws MalformedURLException * if the URL is malformed */ public synchronized void setURL(URL url) throws MalformedURLException { _protocol = url.getProtocol(); _host = url.getHost(); int port = url.getPort(); if (port == -1 || port == url.getDefaultPort()) { _port = null; } else { _port = Integer.valueOf(port); } _path = url.getPath(); _ref = url.getRef(); setQueryParameters(url.getQuery()); } /** * Sets the contents of this ERXMutableURL to be the same as the given URL * external form. * * @param str * the external form of a URL to copy the contents from * @throws MalformedURLException * if the external form of the URL is malformed */ public synchronized void setURL(String str) throws MalformedURLException { boolean relativeURL = false; boolean startsWithSlash = false; if (str != null) { str = str.replaceAll("&", "&"); if (str.indexOf("://") == -1) { relativeURL = true; String fakeHost; if (str.startsWith("/")) { startsWithSlash = true; fakeHost = "http://fakehost"; } else { fakeHost = "http://fakehost/"; } str = fakeHost + str; } } setURL(new URL(str)); if (relativeURL) { setHost(null); setProtocol(null); if (!startsWithSlash && _path != null && _path.length() > 0) { setPath(_path.substring(1)); } } } /** * Returns true if there is a host defined for this URL. * * @return true if there is a host defined for this URL */ public boolean isFullyQualified() { return _host != null; } /** * Returns true if this is an absolute URL. * * @return true if this is an absolute URL */ public boolean isAbsolute() { return _path != null && _path.startsWith("/"); } /** * Sets the protocol of this URL (http, https, etc). * * @param protocol * the new protocol */ public void setProtocol(String protocol) { _protocol = protocol; } /** * Returns the protocol of this URL. * * @return the protocol of this URL */ public String protocol() { return _protocol; } /** * Sets the host of this URL. * * @param host * the host of this URL */ public void setHost(String host) { _host = host; } /** * Returns the host of this URL. * * @return the host of this URL */ public String host() { return _host; } /** * Sets the path of this URL. * * @param path * the path of this URL */ public void setPath(String path) { _path = path; } /** * Returns the path of this URL. * * @return the path of this URL */ public String path() { return _path; } /** * Appends the given path to the end of the existing path. * * @param path the path to append * @return this */ public ERXMutableURL appendPath(String path) { if (_path == null) { _path = path; } else if (_path.endsWith("/")) { _path = _path + path; } else { _path = _path + "/" + path; } return this; } /** * Sets the port of this URL. * * @param port * the port of this URL */ public void setPort(Integer port) { _port = port; } /** * Returns the port of this URL (can be null). * * @return the port of this URL (can be null) */ public Integer port() { if (_port == null) { if (protocol() != null) { if ("https".equals(protocol())) { _port = 443; } else { _port = 80; } } } return _port; } /** * Sets the ref of this URL (the #whatever part). * * @param ref * the ref of this URL (the #whatever part) */ public void setRef(String ref) { _ref = ref; } /** * Returns the ref of this URL. * * @return the ref of this URL */ public String ref() { return _ref; } /** * Replaces the query parameters of this URL with the given k=v&k2=v2 format * string. * * @param queryParameters * the query parameters * @throws MalformedURLException * if the string is malformed */ public synchronized void setQueryParameters(String queryParameters) throws MalformedURLException { clearQueryParameters(); addQueryParameters(queryParameters); } /** * Appends the query parameters of this URL with the given k=v&k2=v2 format * string. * * @param queryParameters * the query parameters * @throws MalformedURLException * if the string is malformed */ public synchronized void addQueryParameters(String queryParameters) throws MalformedURLException { if (queryParameters != null) { StringTokenizer queryStringTokenizer = new StringTokenizer(queryParameters, "&"); while (queryStringTokenizer.hasMoreTokens()) { String queryStringToken = queryStringTokenizer.nextToken(); int equalsIndex = queryStringToken.indexOf('='); try { String key; String value; if (equalsIndex == -1) { key = queryStringToken.trim(); value = null; } else { key = queryStringToken.substring(0, equalsIndex).trim(); value = queryStringToken.substring(equalsIndex + 1); } if (key == null || key.length() == 0) { throw new MalformedURLException("The query string parameter '" + queryStringToken + " has an empty key in '" + queryParameters + "'."); } key = URLDecoder.decode(key, CharEncoding.UTF_8); if (value != null) { value = URLDecoder.decode(value, CharEncoding.UTF_8); } addQueryParameter(key, value); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Every VM is supposed to support UTF-8 encoding.", e); } } } } /** * Replaces the query parameters of this URL with those defined in the given * Map. * * @param queryParameters * the new query parameters */ public synchronized void setQueryParametersMap(Map<String, List<String>> queryParameters) { _queryParameters = queryParameters; } /** * Clears the query parameters of this URL. */ public synchronized void clearQueryParameters() { _queryParameters.clear(); } /** * Replaces the query parameters of this URL with those defined in the given * NSDictionary. * * @param queryParameters * the new query parameters */ public synchronized void setQueryParameters(NSDictionary<String, ? extends Object> queryParameters) { clearQueryParameters(); addQueryParameters(queryParameters); } /** * Adds additional query parameters to this URL from those defined in the * given NSDictionary. * * @param queryParameters * the new query parameters */ @SuppressWarnings("unchecked") public synchronized void addQueryParameters(NSDictionary<String, ? extends Object> queryParameters) { if (queryParameters != null) { Enumeration<String> keyEnum = queryParameters.keyEnumerator(); while (keyEnum.hasMoreElements()) { String key = keyEnum.nextElement(); Object valueObj = queryParameters.objectForKey(key); if (valueObj instanceof NSArray) { NSArray<String> valueArray = (NSArray<String>) valueObj; Enumeration<String> valueArrayEnum = valueArray.objectEnumerator(); while (valueArrayEnum.hasMoreElements()) { String value = valueArrayEnum.nextElement(); addQueryParameter(key, value); } } else { addQueryParameter(key, valueObj.toString()); } } } } /** * Adds additional query parameters to this URL from those defined in the * given Map. * * @param queryParameters * the new query parameters */ public synchronized void addQueryParametersMap(Map<String, String> queryParameters) { if (queryParameters != null) { Iterator<Map.Entry<String, String>> queryParameterIter = queryParameters.entrySet().iterator(); while (queryParameterIter.hasNext()) { Map.Entry<String, String> queryParameter = queryParameterIter.next(); addQueryParameter(queryParameter.getKey(), queryParameter.getValue()); } } } /** * Adds an additional query parameter to this URL. * * @param key * the key of the new parameter * @param value * the value of the new parameter */ public synchronized void addQueryParameter(String key, String value) { List<String> values = _queryParameters.get(key); if (values == null) { values = new LinkedList<>(); _queryParameters.put(key, values); } if (value != null) { values.add(value); } } /** * Returns true if the given key is a query parameter key in this URL. * * @param key * the key of the parameter to lookup * @return true if the given key is a query parameter key in this URL */ public synchronized boolean containsQueryParameter(String key) { return _queryParameters.containsKey(key); } /** * Removes the query parameters with the given key. * * @param key * the key of the query parameters to remove */ public synchronized void removeQueryParameter(String key) { _queryParameters.remove(key); } /** * Removes the query parameter value for the given key for multivalue * parameters. * * @param key * the key of the query parameters to lookup * @param value * the value to remove. */ public synchronized void removeQueryParameter(String key, String value) { List<String> queryParameters = queryParameters(key); if (queryParameters != null) { queryParameters.remove(value); } } /** * Returns the query parameters of this URL as a Map. * * @return the query parameters of this URL as a Map */ public synchronized Map<String, List<String>> queryParameters() { return _queryParameters; } /** * Returns the query parameters of this URL as a Map uniqued by key (which * avoids multivalue properties at the expense of predictability). * * @return the query parameters of this URL as a Map */ public synchronized Map<String, String> uniqueQueryParameters() { Map<String, String> uniqueQueryParameters = new LinkedHashMap<>(); Iterator<Map.Entry<String, List<String>>> queryParameterIter = _queryParameters.entrySet().iterator(); while (queryParameterIter.hasNext()) { Map.Entry<String, List<String>> queryParameter = queryParameterIter.next(); String key = queryParameter.getKey(); Iterator<String> valuesIter = queryParameter.getValue().iterator(); while (valuesIter.hasNext()) { String value = valuesIter.next(); uniqueQueryParameters.put(key, value); } } return uniqueQueryParameters; } /** * Returns the query parameters for the given key. * * @param key * the key to lookup * @return the query parameters for the given key */ public synchronized List<String> queryParameters(String key) { return _queryParameters.get(key); } /** * Returns the first query parameter for the given key. * * @param key * the key to lookup * @return the first query parameter for the given key */ public synchronized String queryParameter(String key) { String queryParameter = null; List<String> queryParameters = queryParameters(key); if (queryParameters != null && queryParameters.size() > 0) { queryParameter = queryParameters.get(0); } return queryParameter; } /** * Sets the given query parameter to the given value. * * @param key * the key to set * @param value * the value to set it to */ public synchronized void setQueryParameter(String key, String value) { LinkedList<String> queryParameters = new LinkedList<>(); if (value != null) { queryParameters.add(value); } _queryParameters.put(key, queryParameters); } /** * Returns a String form of this URL. * * @return a String form of this URL */ public synchronized String toExternalForm() { StringBuffer sb = new StringBuffer(); if (_protocol != null) { sb.append(_protocol); sb.append("://"); } if (_host != null) { sb.append(_host); } if (_port != null) { boolean includePort = true; if ("http".equalsIgnoreCase(_protocol) && Integer.valueOf(80).equals(_port)) { includePort = false; } else if ("https".equalsIgnoreCase(_protocol) && Integer.valueOf(443).equals(_port)) { includePort = false; } if (includePort) { sb.append(':'); sb.append(_port); } } if (_path != null) { if (!_path.startsWith("/")) { sb.append('/'); } sb.append(_path); } if (_queryParameters != null && !_queryParameters.isEmpty()) { if (_host != null || _path != null) { sb.append('?'); } queryParametersAsString(sb); } if (_ref != null) { sb.append('#'); sb.append(_ref); } return sb.toString(); } /** * Returns the query parameters of this URL as a String (in x=y&a=b syntax). * * @return the query parameters of this URL as a String */ public String queryParametersAsString() { StringBuffer sb = new StringBuffer(); queryParametersAsString(sb); return sb.toString(); } protected void queryParametersAsString(StringBuffer sb) { try { Iterator<Map.Entry<String, List<String>>> queryParameterIter = _queryParameters.entrySet().iterator(); while (queryParameterIter.hasNext()) { Map.Entry<String, List<String>> queryParameter = queryParameterIter.next(); String key = queryParameter.getKey(); Iterator<String> valuesIter = queryParameter.getValue().iterator(); if (!valuesIter.hasNext()) { sb.append(URLEncoder.encode(key, CharEncoding.UTF_8)); } while (valuesIter.hasNext()) { String value = valuesIter.next(); sb.append(URLEncoder.encode(key, CharEncoding.UTF_8)); if (value != null) { if (key.length() > 0) { sb.append('='); } sb.append(URLEncoder.encode(value, CharEncoding.UTF_8)); } if (valuesIter.hasNext()) { sb.append('&'); } } if (queryParameterIter.hasNext()) { sb.append('&'); } } } catch (UnsupportedEncodingException e) { throw new RuntimeException("Every VM is supposed to support UTF-8 encoding.", e); } } /** * Returns a java.net.URL object of this URL (which might fail if you have a * relative URL). * * @return a java.net.URL that represents this URL * @throws MalformedURLException * if this URL cannot be represented as a java.net.URL */ public URL toURL() throws MalformedURLException { return new URL(toExternalForm()); } @Override public String toString() { return toExternalForm(); } public static void main(String[] args) throws MalformedURLException { System.out.println("ERXMutableURL.main: " + new ERXMutableURL("http://java.sun.com:80/docs/books/tutorial/index.html?name=networking#DOWNLOADING")); System.out.println("ERXMutableURL.main: " + new ERXMutableURL("https://java.sun.com:443/docs/books/tutorial/index.html?name=networking#DOWNLOADING")); System.out.println("ERXMutableURL.main: " + new ERXMutableURL("http://java.sun.com:12/index.html?name=networking#DOWNLOADING")); System.out.println("ERXMutableURL.main: " + new ERXMutableURL("http://java.sun.com:80/docs/books/tutorial/index.html?name=networking&name2=networking2#DOWNLOADING")); ERXMutableURL mu = new ERXMutableURL("http://java.sun.com:80/docs/books/tutorial/index.html?name=networking&name2=networking2#DOWNLOADING"); mu.setRef(null); mu.removeQueryParameter("name2"); mu.removeQueryParameter("name"); System.out.println("ERXMutableURL.main: " + mu); System.out.println("ERXMutableURL.main: " + new ERXMutableURL("/docs/books/tutorial/index.html?name=networking&name2=networking2#DOWNLOADING")); } }