All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.facebook.airlift.http.client.HttpUriBuilder Maven / Gradle / Ivy

The newest version!
package com.facebook.airlift.http.client;

import com.google.common.annotations.Beta;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.net.HostAndPort;
import com.google.common.primitives.Bytes;

import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.util.Iterator;
import java.util.Map;

import static com.google.common.base.CharMatcher.ascii;
import static java.lang.Character.forDigit;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;

/**
 * An RFC-3986-compatible HTTP URI builder
 */
@Beta
public class HttpUriBuilder
{
    private String scheme;
    private String host;
    private int port = -1;
    private String path = ""; // decoded path
    private final ListMultimap params = LinkedListMultimap.create(); // decoded query params

    private static final byte[] PCHAR = {
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            '-', '.', '_', '~', '!', '$', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@',
    };

    private static final byte[] ALLOWED_PATH_CHARS = Bytes.concat(PCHAR, new byte[] {'/', '&'});
    private static final byte[] ALLOWED_QUERY_CHARS = Bytes.concat(PCHAR, new byte[] {'/', '?'});

    private HttpUriBuilder()
    {
    }

    private HttpUriBuilder(URI previous)
    {
        scheme = previous.getScheme();
        host = previous.getHost();
        port = previous.getPort();
        path = percentDecode(previous.getRawPath());
        params.putAll(parseParams(previous.getRawQuery()));
    }

    public static HttpUriBuilder uriBuilder()
    {
        return new HttpUriBuilder();
    }

    public static HttpUriBuilder uriBuilderFrom(URI uri)
    {
        requireNonNull(uri, "uri is null");

        return new HttpUriBuilder(uri);
    }

    public HttpUriBuilder scheme(String scheme)
    {
        requireNonNull(scheme, "scheme is null");

        this.scheme = scheme;
        return this;
    }

    public HttpUriBuilder host(String host)
    {
        requireNonNull(host, "host is null");
        Preconditions.checkArgument(!host.startsWith("["), "host starts with a bracket");
        Preconditions.checkArgument(!host.endsWith("]"), "host ends with a bracket");
        if (host.contains(":")) {
            host = "[" + host + "]";
        }
        this.host = host;
        return this;
    }

    public HttpUriBuilder port(int port)
    {
        Preconditions.checkArgument(port >= 1 && port <= 65535, "port must be in the range 1-65535");
        this.port = port;
        return this;
    }

    public HttpUriBuilder defaultPort()
    {
        this.port = -1;
        return this;
    }

    public HttpUriBuilder hostAndPort(HostAndPort hostAndPort)
    {
        requireNonNull(hostAndPort, "hostAndPort is null");
        this.host = bracketedHostString(hostAndPort);
        this.port = hostAndPort.hasPort() ? hostAndPort.getPort() : -1;
        return this;
    }

    /**
     * Replace the current path with the given unencoded path
     */
    public HttpUriBuilder replacePath(String path)
    {
        requireNonNull(path, "path is null");

        if (!path.isEmpty() && !path.startsWith("/")) {
            path = "/" + path;
        }

        this.path = path;
        return this;
    }

    /**
     * Append an unencoded path.
     * 

* All reserved characters except '/' will be percent-encoded. '/' are considered as path separators and * appended verbatim. */ public HttpUriBuilder appendPath(String path) { requireNonNull(path, "path is null"); StringBuilder builder = new StringBuilder(this.path); if (!this.path.endsWith("/")) { builder.append('/'); } if (path.startsWith("/")) { path = path.substring(1); } builder.append(path); this.path = builder.toString(); return this; } public HttpUriBuilder replaceParameter(String name, String... values) { return replaceParameter(name, asList(values)); } public HttpUriBuilder replaceParameter(String name, Iterable values) { requireNonNull(name, "name is null"); params.removeAll(name); addParameter(name, values); return this; } public HttpUriBuilder addParameter(String name, String... values) { return addParameter(name, asList(values)); } public HttpUriBuilder addParameter(String name, Iterable values) { requireNonNull(name, "name is null"); if (Iterables.isEmpty(values)) { params.put(name, null); } for (String value : values) { params.put(name, value); } return this; } // return an RFC-3986-compatible URI public String toString() { StringBuilder builder = new StringBuilder(); builder.append(scheme); builder.append("://"); if (host != null) { builder.append(host); } if (port != -1) { builder.append(':'); builder.append(port); } String path = this.path; if (path.isEmpty() && !params.isEmpty()) { path = "/"; } builder.append(encode(path, ALLOWED_PATH_CHARS)); if (!params.isEmpty()) { builder.append('?'); for (Iterator> iterator = params.entries().iterator(); iterator.hasNext(); ) { Map.Entry entry = iterator.next(); builder.append(encode(entry.getKey(), ALLOWED_QUERY_CHARS)); if (entry.getValue() != null) { builder.append('='); builder.append(encode(entry.getValue(), ALLOWED_QUERY_CHARS)); } if (iterator.hasNext()) { builder.append('&'); } } } return builder.toString(); } public URI build() { Preconditions.checkState(scheme != null, "scheme has not been set"); return URI.create(toString()); } private ListMultimap parseParams(String query) { LinkedListMultimap result = LinkedListMultimap.create(); if (query != null) { Iterable pairs = Splitter.on("&") .omitEmptyStrings() .split(query); for (String pair : pairs) { String[] parts = pair.split("=", 2); result.put(percentDecode(parts[0]), percentDecode(parts[1])); } } return result; } private String encode(String input, byte... allowed) { StringBuilder builder = new StringBuilder(); ByteBuffer buffer = UTF_8.encode(input); while (buffer.remaining() > 0) { byte b = buffer.get(); if (Bytes.contains(allowed, b)) { builder.append((char) b); // b is ASCII } else { builder.append('%'); builder.append(Ascii.toUpperCase(forDigit((b >>> 4) & 0xF, 16))); builder.append(Ascii.toUpperCase(forDigit(b & 0xF, 16))); } } return builder.toString(); } /** * input must be an ASCII string representing a percent-encoded UTF-8 byte sequence */ private static String percentDecode(String encoded) { Preconditions.checkArgument(ascii().matchesAllOf(encoded), "string must be ASCII"); ByteArrayOutputStream out = new ByteArrayOutputStream(encoded.length()); for (int i = 0; i < encoded.length(); i++) { char c = encoded.charAt(i); if (c == '%') { Preconditions.checkArgument(i + 2 < encoded.length(), "percent encoded value is truncated"); int high = Character.digit(encoded.charAt(i + 1), 16); int low = Character.digit(encoded.charAt(i + 2), 16); Preconditions.checkArgument(high != -1 && low != -1, "percent encoded value is not a valid hex string: ", encoded.substring(i, i + 2)); int value = (high << 4) | (low); out.write(value); i += 2; } else { out.write((int) c); } } try { return UTF_8.newDecoder() .onMalformedInput(CodingErrorAction.REPORT) .decode(ByteBuffer.wrap(out.toByteArray())) .toString(); } catch (CharacterCodingException e) { throw new IllegalArgumentException("input does not represent a proper UTF8-encoded string"); } } private static String bracketedHostString(HostAndPort hostAndPort) { // HostAndPort cannot return just the bracketed host String host = hostAndPort.getHost(); if (hostAndPort.toString().startsWith("[")) { return "[" + host + "]"; } return host; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy