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.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.zalando.fauxpas.ThrowingUnaryOperator;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
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<String, String> query = LinkedHashMultimap.create();
private final HttpHeaders headers = new HttpHeaders();
public Requester(final AsyncClientHttpRequestFactory requestFactory, final MessageWorker worker,
final RequestArguments arguments, final Plugin plugin) {
this.requestFactory = requestFactory;
this.worker = worker;
this.arguments = arguments;
this.plugin = plugin;
}
public final Requester queryParam(final String name, final String value) {
query.put(name, value);
return this;
}
public final Requester queryParams(final Multimap<String, String> params) {
query.putAll(params);
return this;
}
public final Requester header(final String name, final String value) {
headers.add(name, value);
return this;
}
public final Requester headers(final HttpHeaders headers) {
this.headers.putAll(headers);
return this;
}
public final Requester accept(final MediaType acceptableMediaType, final MediaType... acceptableMediaTypes) {
headers.setAccept(Lists.asList(acceptableMediaType, acceptableMediaTypes));
return this;
}
public final Requester contentType(final MediaType contentType) {
headers.setContentType(contentType);
return this;
}
public final <T> Dispatcher body(@Nullable final T body) {
return execute(body);
}
@Override
public CompletableFuture<Void> call(final Route route) {
return execute(null).call(route);
}
private <T> Dispatcher execute(final @Nullable T body) {
final ImmutableMultimap.Builder<String, String> 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<Void> call(final Route route) {
try {
final RequestExecution execution = plugin.prepare(arguments, () ->
execute(entity).thenApply(dispatch(route)));
final CompletableFuture<ClientHttpResponse> future = execution.execute();
// we need a CompletableFuture<Void>
return future.thenApply(response -> null);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
private <T> CompletableFuture<ClientHttpResponse> execute(final HttpEntity<T> entity) throws IOException {
final URI requestUri = arguments.getRequestUri();
final HttpMethod method = arguments.getMethod();
final AsyncClientHttpRequest request = requestFactory.createAsyncRequest(requestUri, method);
worker.write(request, entity);
return new ListenableCompletableFutureAdapter<>(request.executeAsync());
}
private ThrowingUnaryOperator<ClientHttpResponse, Exception> dispatch(final Route route) {
return response -> {
try {
route.execute(response, worker);
} catch (final NoWildcardException e) {
throw new NoRouteException(response);
}
return response;
};
}
}
}