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

de.taimos.httputils.HTTPRequest Maven / Gradle / Ivy

There is a newer version: 2.2
Show newest version
package de.taimos.httputils;

/*
 * #%L
 * Taimos HTTPUtils
 * %%
 * Copyright (C) 2012 - 2015 Taimos GmbH
 * %%
 * 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.
 * #L%
 */

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

/**
 * @author thoeger
 */
public final class HTTPRequest {

    private static final Executor DEFAULT_EXECUTOR = Executors.newCachedThreadPool();

    private static final CloseableHttpClient DEFAULT_HTTP_CLIENT = HttpClientBuilder.create().build();

    private final String url;

    private final Map> headers = new ConcurrentHashMap<>();

    private final Map> queryParams = new ConcurrentHashMap<>();

    private final Map pathParams = new ConcurrentHashMap<>();

    private volatile Integer timeout;

    private volatile boolean followRedirect = true;

    private volatile String body = "";

    private volatile String userAgent = null;

    private volatile int maxRetries = 0;

    private volatile Retryable retryable = null;

    private volatile WaitStrategy waitStrategy = null;

    interface Request {
        HttpRequestBase request(URI uri);
    }

    public enum Method implements Request {
        GET {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpGet(uri);
            }
        },
        HEAD {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpHead(uri);
            }
        },
        POST {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpPost(uri);
            }
        },
        PUT {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpPut(uri);
            }
        },
        DELETE {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpDelete(uri);
            }
        },
        PATCH {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpPatch(uri);
            }
        },
        OPTIONS {
            @Override
            public HttpRequestBase request(final URI uri) {
                return new HttpOptions(uri);
            }
        }
    }

    /**
     * @param url URL
     */
    HTTPRequest(final String url) {
        this.url = url;
    }

    /**
     * @param name  the name of the header
     * @param value the value of the header
     * @return this
     */
    public HTTPRequest header(final String name, final String value) {
        if (!this.headers.containsKey(name)) {
            this.headers.put(name, new CopyOnWriteArrayList());
        }
        this.headers.get(name).add(value);
        return this;
    }

    /**
     * @param name  the name of the query parameter
     * @param value the value of the query parameter
     * @return this
     */
    public HTTPRequest queryParam(final String name, final String value) {
        if (!this.queryParams.containsKey(name)) {
            this.queryParams.put(name, new CopyOnWriteArrayList());
        }
        this.queryParams.get(name).add(value);
        return this;
    }

    /**
     * @param name  the name of the path parameter
     * @param value the value of the path parameter
     * @return this
     */
    public HTTPRequest pathParam(final String name, final String value) {
        this.pathParams.put(name, value);
        return this;
    }

    /**
     * @param newTimeout Timeout in ms
     * @return this
     */
    public HTTPRequest timeout(final int newTimeout) {
        this.timeout = newTimeout;
        return this;
    }

    /**
     * @param follow true to automatically follow redirects; false otherwise
     * @return this
     */
    public HTTPRequest followRedirect(final boolean follow) {
        this.followRedirect = follow;
        return this;
    }

    /**
     * Retry.
     *
     * @param maxRetries   Maximum number of retries
     * @param retryable    Function to determine if call should be retried
     * @param waitStrategy Function to calculate the time to wait between two retries
     * @return this
     */
    public HTTPRequest retry(final int maxRetries, final Retryable retryable, final WaitStrategy waitStrategy) {
        if (maxRetries <= 0) {
            throw new IllegalArgumentException("maxRetries must be > 0");
        }
        if (retryable == null) {
            throw new IllegalArgumentException("retryable must not be null");
        }
        if (waitStrategy == null) {
            throw new IllegalArgumentException("waitStrategy must not be null");
        }
        this.maxRetries = maxRetries;
        this.retryable = retryable;
        this.waitStrategy = waitStrategy;
        return this;
    }

    /**
     * Retry 5 times on Exception or 5XX status code with exponential backoff.
     *
     * @return this
     */
    public HTTPRequest retry() {
        return this.retry(5, Retryable.standard(), WaitStrategy.exponentialBackoff());
    }

    /**
     * @param agent the user agent string to use
     * @return this
     */
    public HTTPRequest userAgent(final String agent) {
        this.userAgent = agent;
        return this;
    }

    // #######################
    // Some header shortcuts
    // #######################

    /**
     * @param type the Content-Type
     * @return this
     */
    public HTTPRequest contentType(final String type) {
        return this.header(WSConstants.HEADER_CONTENT_TYPE, type);
    }

    /**
     * @param authString the Authorization header
     * @return this
     */
    public HTTPRequest auth(final String authString) {
        return this.header(WSConstants.HEADER_AUTHORIZATION, authString);
    }

    /**
     * @param user     the username
     * @param password the password
     * @return this
     */
    public HTTPRequest authBasic(final String user, final String password) {
        if ((user == null) || (password == null)) {
            throw new IllegalArgumentException("Neither user nor password can be null");
        }
        if (user.contains(":")) {
            throw new IllegalArgumentException("Colon not allowed in user according to RFC2617 Sec. 2");
        }
        final String credentials = user + ":" + password;
        final String auth = Base64.encodeBase64String(credentials.getBytes());
        return this.auth("Basic " + auth);
    }

    /**
     * @param accessToken the OAuth2 Bearer access token
     * @return this
     */
    public HTTPRequest authBearer(final String accessToken) {
        return this.auth("Bearer " + accessToken);
    }

    /**
     * @param type the Accept type
     * @return this
     */
    public HTTPRequest accept(final String type) {
        return this.header(WSConstants.HEADER_ACCEPT, type);
    }

    /**
     * @param bodyString the body entity
     * @return this
     */
    public HTTPRequest body(final String bodyString) {
        this.body = bodyString;
        return this;
    }

    /**
     * @param form the form content
     * @return this
     */
    public HTTPRequest form(final Map form) {
        final StringBuilder formString = new StringBuilder();
        final Iterator> parts = form.entrySet().iterator();
        if (parts.hasNext()) {
            final Entry firstEntry = parts.next();
            formString.append(firstEntry.getKey());
            formString.append("=");
            formString.append(firstEntry.getValue());
            while (parts.hasNext()) {
                final Entry entry = parts.next();
                formString.append("&");
                formString.append(entry.getKey());
                formString.append("=");
                formString.append(entry.getValue());
            }
        }
        return this.contentType("application/x-www-form-urlencoded").body(formString.toString());
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse get() {
        return this.execute(Method.GET);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse put() {
        return this.execute(Method.PUT);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse patch() {
        return this.execute(Method.PATCH);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse post() {
        return this.execute(Method.POST);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse delete() {
        return this.execute(Method.DELETE);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse options() {
        return this.execute(Method.OPTIONS);
    }

    /**
     * @return the {@link HTTPResponse}
     */
    public HTTPResponse head() {
        return this.execute(Method.HEAD);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void getAsync(final HTTPResponseCallback callback) {
        this.getAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void getAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.GET, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void putAsync(final HTTPResponseCallback callback) {
        this.putAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void putAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.PUT, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void patchAsync(final HTTPResponseCallback callback) {
        this.patchAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void patchAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.PATCH, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void postAsync(final HTTPResponseCallback callback) {
        this.postAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void postAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.POST, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void deleteAsync(final HTTPResponseCallback callback) {
        this.deleteAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void deleteAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.DELETE, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void optionsAsync(final HTTPResponseCallback callback) {
        this.optionsAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void optionsAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.OPTIONS, callback);
    }

    /**
     * @param callback {@link HTTPResponseCallback}
     */
    public void headAsync(final HTTPResponseCallback callback) {
        this.headAsync(HTTPRequest.DEFAULT_EXECUTOR, callback);
    }

    /**
     * @param executor Thread pool
     * @param callback {@link HTTPResponseCallback}
     */
    public void headAsync(final Executor executor, final HTTPResponseCallback callback) {
        this.executeAsync(executor, Method.HEAD, callback);
    }

    private void executeAsync(final Executor executor, final Method method, final HTTPResponseCallback cb) {
        final Runnable execute = () -> {
            final HTTPResponse res;
            try {
                res = HTTPRequest.this.execute(method);
            } catch (final Exception e) {
                cb.fail(e);
                return;
            }
            try {
                // TODO find a solution for exceptions thrown in callback
                cb.response(res);
            } finally {
                res.close();
            }
        };
        executor.execute(execute);
    }

    private HTTPResponse execute(final Method method) {
        final URI uri = this.buildURI();
        // attempt == 0 is not a retry, attempt > 0 are retries
        for (int attempt = 0; attempt <= this.maxRetries; attempt++) {
            if (attempt > 0) {
                final int wait = this.waitStrategy.milliseconds(attempt - 1);
                if (wait > 0) {
                    try {
                        Thread.sleep(wait);
                    } catch (final InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
            try {
                final HTTPResponse response = this.attempt(method, uri);
                if (this.retryable != null) {
                    final int statusCode = response.getStatus();
                    if (this.retryable.retry(Optional.empty(), Optional.of(statusCode))) {
                        response.close(); // we are not interested in the body
                        if (attempt < this.maxRetries) {
                            continue; // retry
                        } else {
                            throw new RuntimeException("status code " + statusCode);
                        }
                    }
                }
                return response;
            } catch (final IOException e) {
                if (this.retryable != null) {
                    if (attempt < this.maxRetries) {
                        continue; // retry
                    } else {
                        throw new RuntimeException("retry exhausted", e);
                    }
                } else {
                    throw new RuntimeException(e);
                }
            } catch (final RuntimeException e) {
                if (this.retryable != null) {
                    if (attempt < this.maxRetries) {
                        continue; // retry
                    } else {
                        throw new RuntimeException("retry exhausted", e);
                    }
                } else {
                    throw e;
                }
            }
        }
        throw new RuntimeException("retry failed"); // should never be reached
    }

    private HTTPResponse attempt(final Method method, final URI uri) throws IOException {
        // prepare request configuration
        final Builder requestConfigBuilder = RequestConfig.custom();
        if (this.timeout != null) {
            requestConfigBuilder.setConnectTimeout(this.timeout);
            requestConfigBuilder.setConnectionRequestTimeout(this.timeout);
            requestConfigBuilder.setSocketTimeout(this.timeout);
        }
        requestConfigBuilder.setRedirectsEnabled(this.followRedirect);
        final RequestConfig requestConfig = requestConfigBuilder.build();

        // prepare request
        final HttpRequestBase request = method.request(uri);
        request.setConfig(requestConfig);
        if ((this.userAgent != null) && !this.userAgent.isEmpty()) {
            request.setHeader(WSConstants.HEADER_USER_AGENT, this.userAgent);
        }
        for (final Entry> entry : this.headers.entrySet()) {
            final List list = entry.getValue();
            for (final String string : list) {
                request.addHeader(entry.getKey(), string);
            }
        }
        if (request instanceof HttpEntityEnclosingRequestBase) {
            final HttpEntityEnclosingRequestBase entityBase = (HttpEntityEnclosingRequestBase) request;
            entityBase.setEntity(new StringEntity(this.body, "UTF-8"));
        }

        return new HTTPResponse(DEFAULT_HTTP_CLIENT.execute(request));
    }

    private URI buildURI() {
        try {
            String u = this.url;
            for (final Entry pathEntry : this.pathParams.entrySet()) {
                u = u.replace("{" + pathEntry.getKey() + "}", pathEntry.getValue());
            }
            final URIBuilder builder = new URIBuilder(u);
            final Set>> entrySet = this.queryParams.entrySet();
            for (final Entry> entry : entrySet) {
                final List list = entry.getValue();
                for (final String string : list) {
                    builder.addParameter(entry.getKey(), string);
                }
            }
            return builder.build();
        } catch (final URISyntaxException e) {
            throw new RuntimeException("Invalid URI", e);
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy