UriBuilder.java
/**
* Copyright (C) 2017 HttpBuilder-NG Project
*
* 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 groovyx.net.http;
import groovy.lang.GString;
import org.codehaus.groovy.runtime.GStringImpl;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static groovyx.net.http.Traverser.notValue;
import static groovyx.net.http.Traverser.traverse;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
/**
* Provides a simple means of creating a request URI and optionally overriding its parts.
*
* [source,groovy]
* ----
* def uri = UriBuilder.basic(UriBuilder.root())
* .setFull('http://localhost:10101')
* .setPath('/foo')
* .toURI()
* ----
*
* Generally, this class is not instantiated directly, but created by the {@link HttpConfig} instance and modified.
*/
public abstract class UriBuilder {
public static final int DEFAULT_PORT = -1;
/**
* Sets the scheme part of the URI.
*
* @param val the value to use as the scheme part of the URI
* @return a reference to this builder
*/
public abstract UriBuilder setScheme(String val);
/**
* Retrieves the scheme part of the URI.
*
* @return the URI scheme
*/
public abstract String getScheme();
/**
* Sets the port part of the URI.
*
* @param val the value to use as the port part of the URI
* @return a reference to this builder
*/
public abstract UriBuilder setPort(int val);
/**
* Retrieves the port part of the URI.
*
* @return the port part of the URI
*/
public abstract int getPort();
/**
* Sets the host part of the URI.
*
* @param val the value to use as the host part of the URI
* @return a reference to this builder
*/
public abstract UriBuilder setHost(String val);
/**
* Retrieves the host part of the URI.
*
* @return the host part of the URI
*/
public abstract String getHost();
/**
* Sets the path part of the URI.
*
* @param val the path part of the URI
* @return a reference to the builder
*/
public abstract UriBuilder setPath(GString val);
/**
* Retrieves the path part of the URI.
*
* @return the path part of the URI
*/
public abstract GString getPath();
/**
* Sets the query string part of the `URI` from the provided map. The query string key-value pairs will be generated from the key-value pairs
* of the map and are NOT URL-encoded. Nested maps or other data structures are not supported.
*
* @param val the map of query string parameters
* @return a reference to the builder
*/
public abstract UriBuilder setQuery(Map<String, ?> val);
/**
* Retrieves the `Map` of query string parameters for the `URI`.
*
* @return the `Map` of query string parameters for the `URI`.
*/
public abstract Map<String, ?> getQuery();
/**
* Sets the fragment part of the `URI`.
*
* @param val the fragment part of the `URI`
* @return a reference to the builder
*/
public abstract UriBuilder setFragment(String val);
/**
* Retrieves the fragment part of the `URI`.
*
* @return the fragment part of the `URI`
*/
public abstract String getFragment();
/**
* Sets the user info part of the `URI`.
*
* @param val the user info part of the `URI`
* @return a reference to the builder
*/
public abstract UriBuilder setUserInfo(String val);
/**
* Retrieves the user info part of the `URI`.
*
* @return the user info part of the `URI`
*/
public abstract String getUserInfo();
public abstract UriBuilder getParent();
/**
* Sets the path part of the URI.
*
* @param str the path part of the URI
* @return a reference to the builder
*/
public UriBuilder setPath(final String str) {
final String[] parts;
if (str.startsWith("/")) {
parts = new String[]{str};
} else {
final String base = getPath().toString();
parts = new String[]{base, base.endsWith("/") ? "" : "/", str};
}
return setPath(new GStringImpl(EMPTY, parts));
}
public URI forCookie(final HttpCookie cookie) throws URISyntaxException {
final String scheme = traverse(this, UriBuilder::getParent, UriBuilder::getScheme, Traverser::notNull);
final Integer port = traverse(this, UriBuilder::getParent, UriBuilder::getPort, notValue(DEFAULT_PORT));
final String host = traverse(this, UriBuilder::getParent, UriBuilder::getHost, Traverser::notNull);
final String path = cookie.getPath();
final String query = null;
final String fragment = null;
final String userInfo = null;
return new URI(scheme, userInfo, host, (port == null ? -1 : port), path, query, fragment);
}
/**
* Converts the parts of the `UriBuilder` to the `URI` object instance.
*
* @return the generated `URI` representing the parts contained in the builder
*/
public URI toURI() throws URISyntaxException {
final String scheme = traverse(this, UriBuilder::getParent, UriBuilder::getScheme, Traverser::notNull);
final Integer port = traverse(this, UriBuilder::getParent, UriBuilder::getPort, notValue(DEFAULT_PORT));
final String host = traverse(this, UriBuilder::getParent, UriBuilder::getHost, Traverser::notNull);
final GString path = traverse(this, UriBuilder::getParent, UriBuilder::getPath, Traverser::notNull);
final String query = populateQueryString(traverse(this, UriBuilder::getParent, UriBuilder::getQuery, Traverser::nonEmptyMap));
final String fragment = traverse(this, UriBuilder::getParent, UriBuilder::getFragment, Traverser::notNull);
final String userInfo = traverse(this, UriBuilder::getParent, UriBuilder::getUserInfo, Traverser::notNull);
final Boolean useRaw = traverse(this, UriBuilder::getParent, UriBuilder::getUseRawValues, Traverser::notNull);
if (useRaw != null && useRaw) {
return toRawURI(scheme, port, host, path, query, fragment, userInfo);
} else {
return new URI(scheme, userInfo, host, (port == null ? -1 : port), ((path == null) ? null : path.toString()), query, fragment);
}
}
private URI toRawURI(final String scheme, final Integer port, final String host, final GString path, final String query, final String fragment, final String userInfo) throws URISyntaxException {
return new URI(format("%s%s%s%s%s%s%s",
scheme == null ? "" : (scheme.endsWith("://") ? scheme : scheme + "://"),
userInfo == null ? "" : (userInfo.endsWith("@") ? userInfo : userInfo + "@"),
host == null ? "" : host,
port == null ? "" : ":" + port.toString(),
path == null ? "" : (!path.toString().startsWith("/") && !path.toString().isEmpty() ? "/" + path : path),
query != null ? "?" + query : "",
fragment == null ? "" : (!fragment.startsWith("#") ? "#" + fragment : fragment)
));
}
private static final Object[] EMPTY = new Object[0];
private static String populateQueryString(final Map<String, ?> queryMap) {
if (queryMap == null || queryMap.isEmpty()) {
return null;
} else {
final List<String> nvps = new LinkedList<>();
queryMap.entrySet().forEach((Consumer<Map.Entry<String, ?>>) entry -> {
final Collection<?> values = entry.getValue() instanceof Collection ? (Collection<?>) entry.getValue() : singletonList(entry.getValue().toString());
if(values.isEmpty()){
nvps.add(entry.getKey());
} else {
values.forEach(value -> nvps.add(entry.getKey() + "=" + value));
}
});
return nvps.stream().collect(Collectors.joining("&"));
}
}
private Boolean useRawValues;
public void setUseRawValues(final boolean useRaw) {
this.useRawValues = useRaw;
}
public Boolean getUseRawValues() {
return useRawValues;
}
protected final void populateFrom(final URI uri) {
boolean useRaw = useRawValues != null ? useRawValues : false;
try {
setScheme(uri.getScheme());
setPort(uri.getPort());
setHost(uri.getHost());
final String path = useRaw ? uri.getRawPath() : uri.getPath();
if (path != null) {
setPath(new GStringImpl(EMPTY, new String[]{path}));
}
final String rawQuery = useRaw ? uri.getRawQuery() : uri.getQuery();
if (rawQuery != null) {
if (useRaw) {
setQuery(extractQueryMap(rawQuery));
} else {
setQuery(Form.decode(new StringBuilder(rawQuery), UTF_8));
}
}
setFragment(useRaw ? uri.getRawFragment() : uri.getFragment());
setUserInfo(useRaw ? uri.getRawUserInfo() : uri.getUserInfo());
} catch (IOException e) {
//this seems o.k. to just convert to a runtime exception,
//we started with a valid URI, so this should never happen.
throw new RuntimeException(e);
}
}
// does not do any encoding
private static Map<String, Collection<String>> extractQueryMap(final String queryString) {
final Map<String, Collection<String>> map = new HashMap<>();
for (final String nvp : queryString.split("&")) {
final String[] pair = nvp.split("=");
map.computeIfAbsent(pair[0], k -> {
List<String> list = new LinkedList<>();
list.add(pair[1]);
return list;
});
}
return map;
}
/**
* Sets the full URI (all parts) as a String.
*
* @param str the full URI to be used by the `UriBuilder`
* @return a reference to the builder
* @throws IllegalArgumentException if there is a problem with the URI syntax
*/
public final UriBuilder setFull(final String str) {
try {
return setFull(new URI(str));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex.getMessage());
}
}
/**
* Sets the full URI (all parts) as a URI object.
*
* @param uri the full URI to be used by the `UriBuilder`
* @return a reference to the builder
*/
public final UriBuilder setFull(final URI uri) {
populateFrom(uri);
return this;
}
/**
* Creates a basic `UriBuilder` from the provided parent builder. An empty `UriBuilder` may be created using the `root()` method as the `parent` value,
* otherwise a new `UriBuilder` may be created from an existing builder:
*
* [source,groovy]
* ----
* def parent = UriBuilder.basic(UriBuilder.root()).setFull('http://localhost:10101/foo')
* def child = UriBuilder.basic(parent)
* child.setPath('/bar').toURI() == new URI('http://localhost:10101/bar')
* ----
*
* The `UriBuilder` implementation generated with this method is _not_ thread-safe.
*
* @param parent the `UriBuilder` parent
* @return the created `UriBuilder`
*/
public static UriBuilder basic(final UriBuilder parent) {
return new Basic(parent);
}
/**
* Creates a thread-safe `UriBuilder` from the provided parent builder. An empty `UriBuilder` may be created using the `root()` method as the
* `parent` value, otherwise a new `UriBuilder` may be created from an existing builder:
*
* [source,groovy]
* ----
* def parent = UriBuilder.threadSafe(UriBuilder.root()).setFull('http://localhost:10101/foo')
* def child = UriBuilder.threadSafe(parent)
* child.setPath('/bar').toURI() == new URI('http://localhost:10101/bar')
* ----
*
* The `UriBuilder` implementation generated with this method is thread-safe.
*
* @param parent the `UriBuilder` parent
* @return the created `UriBuilder`
*/
public static UriBuilder threadSafe(final UriBuilder parent) {
return new ThreadSafe(parent);
}
public static UriBuilder root() {
return new ThreadSafe(null);
}
private static final class Basic extends UriBuilder {
private String scheme;
public UriBuilder setScheme(String val) {
scheme = val;
return this;
}
public String getScheme() {
return scheme;
}
private int port = DEFAULT_PORT;
public UriBuilder setPort(int val) {
port = val;
return this;
}
public int getPort() {
return port;
}
private String host;
public UriBuilder setHost(String val) {
host = val;
return this;
}
public String getHost() {
return host;
}
private GString path;
public UriBuilder setPath(GString val) {
path = val;
return this;
}
public GString getPath() {
return path;
}
private Map<String, Object> query = new LinkedHashMap<>(1);
public UriBuilder setQuery(final Map<String, ?> val) {
if (val != null) {
query.putAll(val);
}
return this;
}
public Map<String, Object> getQuery() {
return query;
}
private String fragment;
public UriBuilder setFragment(String val) {
fragment = val;
return this;
}
public String getFragment() {
return fragment;
}
private String userInfo;
public UriBuilder setUserInfo(String val) {
userInfo = val;
return this;
}
public String getUserInfo() {
return userInfo;
}
private final UriBuilder parent;
public UriBuilder getParent() {
return parent;
}
public Basic(final UriBuilder parent) {
this.parent = parent;
}
}
private static final class ThreadSafe extends UriBuilder {
private volatile String scheme;
public UriBuilder setScheme(String val) {
scheme = val;
return this;
}
public String getScheme() {
return scheme;
}
private volatile int port = DEFAULT_PORT;
public UriBuilder setPort(int val) {
port = val;
return this;
}
public int getPort() {
return port;
}
private volatile String host;
public UriBuilder setHost(String val) {
host = val;
return this;
}
public String getHost() {
return host;
}
private volatile GString path;
public UriBuilder setPath(GString val) {
path = val;
return this;
}
public GString getPath() {
return path;
}
private Map<String, Object> query = new ConcurrentHashMap<>();
public UriBuilder setQuery(Map<String, ?> val) {
query.putAll(val);
return this;
}
public Map<String, ?> getQuery() {
return query;
}
private volatile String fragment;
public UriBuilder setFragment(String val) {
fragment = val;
return this;
}
public String getFragment() {
return fragment;
}
private volatile String userInfo;
public UriBuilder setUserInfo(String val) {
userInfo = val;
return this;
}
public String getUserInfo() {
return userInfo;
}
private final UriBuilder parent;
public UriBuilder getParent() {
return parent;
}
public ThreadSafe(final UriBuilder parent) {
this.parent = parent;
}
}
}