/** * 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; } } }