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

org.zalando.riptide.Requester Maven / Gradle / Ivy

package org.zalando.riptide;

import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.apiguardian.api.API;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.AsyncClientHttpRequest;
import org.springframework.http.client.AsyncClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.concurrent.ListenableFuture;
import org.zalando.fauxpas.ThrowingUnaryOperator;

import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static java.util.Objects.nonNull;
import static org.apiguardian.api.API.Status.STABLE;
import static org.zalando.riptide.CancelableCompletableFuture.preserveCancelability;

@API(status = STABLE)
public final class Requester extends Dispatcher {

    private final AsyncClientHttpRequestFactory requestFactory;
    private final MessageWorker worker;
    private final RequestArguments arguments;
    private final Plugin plugin;

    private final Multimap query = LinkedHashMultimap.create();
    private final HttpHeaders headers = new HttpHeaders();

    Requester(final AsyncClientHttpRequestFactory requestFactory, final MessageWorker worker,
            final RequestArguments arguments, final List plugins) {
        this.requestFactory = requestFactory;
        this.worker = worker;
        this.arguments = arguments;
        this.plugin = Plugin.compound(plugins);
    }

    public Requester queryParam(final String name, final String value) {
        query.put(name, value);
        return this;
    }

    public Requester queryParams(final Multimap params) {
        query.putAll(params);
        return this;
    }

    public Requester accept(final MediaType acceptableMediaType, final MediaType... acceptableMediaTypes) {
        headers.setAccept(Lists.asList(acceptableMediaType, acceptableMediaTypes));
        return this;
    }

    public Requester contentType(final MediaType contentType) {
        headers.setContentType(contentType);
        return this;
    }

    public Requester ifModifiedSince(final OffsetDateTime since) {
        headers.setIfModifiedSince(since.toInstant().toEpochMilli());
        return this;
    }

    public Requester ifUnmodifiedSince(final OffsetDateTime since) {
        headers.setIfUnmodifiedSince(since.toInstant().toEpochMilli());
        return this;
    }

    public Requester ifNoneMatch(final String... entityTags) {
        return ifNoneMatch(Arrays.asList(entityTags));
    }

    public Requester ifMatch(final String... entityTags) {
        return ifMatch(Arrays.asList(entityTags));
    }

    private Requester ifMatch(final List entityTags) {
        headers.setIfMatch(entityTags);
        return this;
    }

    private Requester ifNoneMatch(final List entityTags) {
        headers.setIfNoneMatch(entityTags);
        return this;
    }

    public Requester header(final String name, final String value) {
        headers.add(name, value);
        return this;
    }

    public Requester headers(final HttpHeaders headers) {
        this.headers.putAll(headers);
        return this;
    }

    public  Dispatcher body(@Nullable final T body) {
        return execute(body);
    }

    @Override
    public CompletableFuture call(final Route route) {
        return execute(null).call(route);
    }

    private  Dispatcher execute(final @Nullable T body) {
        final ImmutableMultimap.Builder builder = ImmutableMultimap.builder();
        headers.forEach(builder::putAll);

        return new ResponseDispatcher(new HttpEntity<>(body, headers), arguments
                .withQueryParams(ImmutableMultimap.copyOf(query))
                .withRequestUri()
                .withHeaders(builder.build())
                .withBody(body)
        );
    }

    private final class ResponseDispatcher extends Dispatcher {

        private final HttpEntity entity;
        private final RequestArguments arguments;

        ResponseDispatcher(final HttpEntity entity, final RequestArguments arguments) {
            this.entity = entity;
            this.arguments = arguments;
        }

        @Override
        public CompletableFuture call(final Route route) {
            try {
                final RequestExecution original = this::send;
                final RequestExecution before = plugin.interceptBeforeRouting(arguments, original);
                final RequestExecution after = plugin.interceptAfterRouting(arguments, dispatch(before, route));
                final CompletableFuture future = after.execute();

                // TODO why not return CompletableFuture here?
                // we need a CompletableFuture

                // TODO: replace with thenApply call in Java 9
                final CompletableFuture result = preserveCancelability(future);
                future.whenComplete((response, throwable) -> {
                    if (nonNull(response)) {
                        result.complete(null);
                    }
                    if (nonNull(throwable)) {
                        result.completeExceptionally(throwable);
                    }
                });
                return result;
            } catch (final IOException e) {
                throw new UncheckedIOException(e);
            }
        }

        private CompletableFuture send() throws IOException {
            final AsyncClientHttpRequest request = createRequest();
            worker.write(request, entity);
            final ListenableFuture original = request.executeAsync();

            final CompletableFuture future = preserveCancelability(original);
            original.addCallback(future::complete, future::completeExceptionally);
            return future;
        }

        private AsyncClientHttpRequest createRequest() throws IOException {
            final URI requestUri = arguments.getRequestUri();
            final HttpMethod method = arguments.getMethod();
            return requestFactory.createAsyncRequest(requestUri, method);
        }

        private RequestExecution dispatch(final RequestExecution execution, final Route route) {
            return () -> execution.execute().thenApply(dispatch(route));
        }

        private ThrowingUnaryOperator dispatch(final Route route) {
            return response -> {
                try {
                    route.execute(response, worker);
                } catch (final NoWildcardException e) {
                    throw new NoRouteException(response);
                }

                return response;
            };
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy