com.rt.storage.api.client.http.GenericUrl Maven / Gradle / Ivy
package com.rt.storage.api.client.http;
import com.rt.storage.api.client.util.GenericData;
import com.rt.storage.api.client.util.Key;
import com.rt.storage.api.client.util.Preconditions;
import com.rt.storage.api.client.util.escape.CharEscapers;
import com.rt.storage.api.client.util.escape.Escaper;
import com.rt.storage.api.client.util.escape.PercentEscaper;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* URL builder in which the query parameters are specified as generic data key/value pairs, based on
* the specification RFC 3986: Uniform Resource
* Identifier (URI).
*
* The query parameters are specified with the data key name as the parameter name, and the data
* value as the parameter value. Subclasses can declare fields for known query parameters using the
* {@link Key} annotation. {@code null} parameter names are not allowed, but {@code null} query
* values are allowed.
*
*
Query parameter values are parsed using {@link UrlEncodedParser#parse(String, Object)}.
*
*
Implementation is not thread-safe.
*
* @since 1.0
* @author Yaniv Inbar
*/
public class GenericUrl extends GenericData {
private static final Escaper URI_FRAGMENT_ESCAPER = new PercentEscaper("=&-_.!~*'()@:$,;/?:");
/** Scheme (lowercase), for example {@code "https"}. */
private String scheme;
/** Host. */
private String host;
/** User info or {@code null} for none, for example {@code "username:password"}. */
private String userInfo;
/** Port number or {@code -1} if undefined, for example {@code 443}. */
private int port = -1;
/**
* Decoded path component by parts with each part separated by a {@code '/'} or {@code null} for
* none, for example {@code "/m8/feeds/contacts/default/full"} is represented by {@code "", "m8",
* "feeds", "contacts", "default", "full"}.
*
*
Use {@link #appendRawPath(String)} to append to the path, which ensures that no extra slash
* is added.
*/
private List pathParts;
/** Fragment component or {@code null} for none. */
private String fragment;
/**
* If true, the URL string originally given is used as is (without encoding, decoding and
* escaping) whenever referenced; otherwise, part of the URL string may be encoded or decoded as
* deemed appropriate or necessary.
*/
private boolean verbatim;
public GenericUrl() {}
/**
* Constructs a GenericUrl from a URL encoded string.
*
* Any known query parameters with pre-defined fields as data keys will be parsed based on
* their data type. Any unrecognized query parameter will always be parsed as a string.
*
*
Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
*
*
Upgrade warning: starting in version 1.18 this parses the encodedUrl using new
* URL(encodedUrl). In previous versions it used new URI(encodedUrl). In particular, this means
* that only a limited set of schemes are allowed such as "http" and "https", but that parsing is
* compliant with, at least, RFC 3986.
*
* @param encodedUrl encoded URL, including any existing query parameters that should be parsed
* @throws IllegalArgumentException if the URL has a syntax error
*/
public GenericUrl(String encodedUrl) {
this(encodedUrl, false);
}
/**
* Constructs a GenericUrl from a string.
*
*
Any known query parameters with pre-defined fields as data keys will be parsed based on
* their data type. Any unrecognized query parameter will always be parsed as a string.
*
*
Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
*
* @param encodedUrl encoded URL, including any existing query parameters that should be parsed
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and
* escaping)
* @throws IllegalArgumentException if URL has a syntax error
*/
public GenericUrl(String encodedUrl, boolean verbatim) {
this(parseURL(encodedUrl), verbatim);
}
/**
* Constructs from a URI.
*
* @param uri URI
* @since 1.14
*/
public GenericUrl(URI uri) {
this(uri, false);
}
/**
* Constructs from a URI.
*
* @param uri URI
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and
* escaping)
*/
public GenericUrl(URI uri, boolean verbatim) {
this(
uri.getScheme(),
uri.getHost(),
uri.getPort(),
uri.getRawPath(),
uri.getRawFragment(),
uri.getRawQuery(),
uri.getRawUserInfo(),
verbatim);
}
/**
* Constructs from a URL.
*
* @param url URL
* @since 1.14
*/
public GenericUrl(URL url) {
this(url, false);
}
/**
* Constructs from a URL.
*
* @param url URL
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and
* escaping)
* @since 1.14
*/
public GenericUrl(URL url, boolean verbatim) {
this(
url.getProtocol(),
url.getHost(),
url.getPort(),
url.getPath(),
url.getRef(),
url.getQuery(),
url.getUserInfo(),
verbatim);
}
private GenericUrl(
String scheme,
String host,
int port,
String path,
String fragment,
String query,
String userInfo,
boolean verbatim) {
this.scheme = scheme.toLowerCase(Locale.US);
this.host = host;
this.port = port;
this.pathParts = toPathParts(path, verbatim);
this.verbatim = verbatim;
if (verbatim) {
this.fragment = fragment;
if (query != null) {
UrlEncodedParser.parse(query, this, false);
}
this.userInfo = userInfo;
} else {
this.fragment = fragment != null ? CharEscapers.decodeUri(fragment) : null;
if (query != null) {
UrlEncodedParser.parse(query, this);
}
this.userInfo = userInfo != null ? CharEscapers.decodeUri(userInfo) : null;
}
}
@Override
public int hashCode() {
return build().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj) || !(obj instanceof GenericUrl)) {
return false;
}
GenericUrl other = (GenericUrl) obj;
return build().equals(other.build());
}
@Override
public String toString() {
return build();
}
@Override
public GenericUrl clone() {
GenericUrl result = (GenericUrl) super.clone();
if (pathParts != null) {
result.pathParts = new ArrayList(pathParts);
}
return result;
}
@Override
public GenericUrl set(String fieldName, Object value) {
return (GenericUrl) super.set(fieldName, value);
}
/**
* Returns the scheme (lowercase), for example {@code "https"}.
*
* @since 1.5
*/
public final String getScheme() {
return scheme;
}
/**
* Sets the scheme (lowercase), for example {@code "https"}.
*
* @since 1.5
*/
public final void setScheme(String scheme) {
this.scheme = Preconditions.checkNotNull(scheme);
}
/**
* Returns the host.
*
* @since 1.5
*/
public String getHost() {
return host;
}
/**
* Sets the host.
*
* @since 1.5
*/
public final void setHost(String host) {
this.host = Preconditions.checkNotNull(host);
}
/**
* Returns the user info or {@code null} for none, for example {@code "username:password"}.
*
* @since 1.15
*/
public final String getUserInfo() {
return userInfo;
}
/**
* Sets the user info or {@code null} for none, for example {@code "username:password"}.
*
* @since 1.15
*/
public final void setUserInfo(String userInfo) {
this.userInfo = userInfo;
}
/**
* Returns the port number or {@code -1} if undefined, for example {@code 443}.
*
* @since 1.5
*/
public int getPort() {
return port;
}
/**
* Sets the port number, for example {@code 443}.
*
* @since 1.5
*/
public final void setPort(int port) {
Preconditions.checkArgument(port >= -1, "expected port >= -1");
this.port = port;
}
/**
* Returns the decoded path component by parts with each part separated by a {@code '/'} or {@code
* null} for none.
*
* @since 1.5
*/
public List getPathParts() {
return pathParts;
}
/**
* Sets the decoded path component by parts with each part separated by a {@code '/'} or {@code
* null} for none.
*
* For example {@code "/m8/feeds/contacts/default/full"} is represented by {@code "", "m8",
* "feeds", "contacts", "default", "full"}.
*
*
Use {@link #appendRawPath(String)} to append to the path, which ensures that no extra slash
* is added.
*
* @since 1.5
*/
public void setPathParts(List pathParts) {
this.pathParts = pathParts;
}
/**
* Returns the fragment component or {@code null} for none.
*
* @since 1.5
*/
public String getFragment() {
return fragment;
}
/**
* Sets the fragment component or {@code null} for none.
*
* @since 1.5
*/
public final void setFragment(String fragment) {
this.fragment = fragment;
}
/**
* Constructs the string representation of the URL, including the path specified by {@link
* #pathParts} and the query parameters specified by this generic URL.
*/
public final String build() {
return buildAuthority() + buildRelativeUrl();
}
/**
* Constructs the portion of the URL containing the scheme, host and port.
*
* For the URL {@code "http://example.com/something?action=add"} this method would return
* {@code "http://example.com"}.
*
* @return scheme://[user-info@]host[:port]
* @since 1.9
*/
public final String buildAuthority() {
// scheme, [user info], host, [port]
StringBuilder buf = new StringBuilder();
buf.append(Preconditions.checkNotNull(scheme));
buf.append("://");
if (userInfo != null) {
buf.append(verbatim ? userInfo : CharEscapers.escapeUriUserInfo(userInfo)).append('@');
}
buf.append(Preconditions.checkNotNull(host));
int port = this.port;
if (port != -1) {
buf.append(':').append(port);
}
return buf.toString();
}
/**
* Constructs the portion of the URL beginning at the rooted path.
*
*
For the URL {@code "http://example.com/something?action=add"} this method would return
* {@code "/something?action=add"}.
*
* @return path with with leading '/' if the path is non-empty, query parameters and fragment
* @since 1.9
*/
public final String buildRelativeUrl() {
StringBuilder buf = new StringBuilder();
if (pathParts != null) {
appendRawPathFromParts(buf);
}
addQueryParams(entrySet(), buf, verbatim);
// URL fragment
String fragment = this.fragment;
if (fragment != null) {
buf.append('#').append(verbatim ? fragment : URI_FRAGMENT_ESCAPER.escape(fragment));
}
return buf.toString();
}
/**
* Constructs the URI based on the string representation of the URL from {@link #build()}.
*
*
Any {@link URISyntaxException} is wrapped in an {@link IllegalArgumentException}.
*
* @return new URI instance
* @since 1.14
*/
public final URI toURI() {
return toURI(build());
}
/**
* Constructs the URL based on the string representation of the URL from {@link #build()}.
*
*
Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
*
* @return new URL instance
* @since 1.14
*/
public final URL toURL() {
return parseURL(build());
}
/**
* Constructs the URL based on {@link URL#URL(URL, String)} with this URL representation from
* {@link #toURL()} and a relative url.
*
*
Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
*
* @return new URL instance
* @since 1.14
*/
public final URL toURL(String relativeUrl) {
try {
URL url = toURL();
return new URL(url, relativeUrl);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Returns the first query parameter value for the given query parameter name.
*
* @param name query parameter name
* @return first query parameter value
*/
public Object getFirst(String name) {
Object value = get(name);
if (value instanceof Collection>) {
@SuppressWarnings("unchecked")
Collection