reactivefeign.jetty.client.JettyReactiveHttpClient Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2013-2015 the original author or authors.
*
* 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 reactivefeign.jetty.client;
import com.fasterxml.jackson.core.async_.JsonFactory;
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import feign.MethodMetadata;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.reactive.client.ReactiveRequest;
import org.reactivestreams.Publisher;
import reactivefeign.client.ReactiveFeignException;
import reactivefeign.client.ReactiveHttpClient;
import reactivefeign.client.ReactiveHttpRequest;
import reactivefeign.client.ReactiveHttpResponse;
import reactivefeign.client.ReadTimeoutException;
import reactivefeign.utils.SerializedFormData;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.concurrent.TimeUnit;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
import static org.eclipse.jetty.http.HttpHeader.ACCEPT;
import static org.eclipse.jetty.http.HttpHeader.ACCEPT_ENCODING;
import static reactivefeign.utils.FeignUtils.getBodyActualType;
import static reactivefeign.utils.FeignUtils.returnActualType;
import static reactivefeign.utils.FeignUtils.returnPublisherType;
import static reactivefeign.utils.HttpUtils.APPLICATION_JSON;
import static reactivefeign.utils.HttpUtils.APPLICATION_JSON_UTF_8;
import static reactivefeign.utils.HttpUtils.APPLICATION_OCTET_STREAM;
import static reactivefeign.utils.HttpUtils.APPLICATION_STREAM_JSON;
import static reactivefeign.utils.HttpUtils.APPLICATION_STREAM_JSON_UTF_8;
import static reactivefeign.utils.HttpUtils.FORM_URL_ENCODED;
import static reactivefeign.utils.HttpUtils.GZIP;
import static reactivefeign.utils.HttpUtils.NEWLINE_SEPARATOR;
import static reactivefeign.utils.HttpUtils.TEXT;
import static reactivefeign.utils.HttpUtils.TEXT_UTF_8;
/**
* Uses reactive Jetty client to execute http requests
*
* @author Sergii Karpenko
*/
public class JettyReactiveHttpClient implements ReactiveHttpClient {
private final HttpClient httpClient;
private final Class bodyActualClass;
private final Class returnPublisherClass;
private final Class returnActualClass;
private final JsonFactory jsonFactory;
private final ObjectWriter bodyWriter;
private final ObjectReader responseReader;
private long requestTimeout = -1;
private boolean tryUseCompression;
public static JettyReactiveHttpClient jettyClient(
MethodMetadata methodMetadata,
HttpClient httpClient,
JsonFactory jsonFactory, ObjectMapper objectMapper) {
Class returnPublisherType = returnPublisherType(methodMetadata);
Type returnActualType = returnActualType(methodMetadata);
Type bodyActualType = getBodyActualType(methodMetadata.bodyType());
ObjectWriter bodyWriter = bodyActualType != null
? objectMapper.writerFor(objectMapper.constructType(bodyActualType)) : null;
ObjectReader responseReader = objectMapper.readerFor(objectMapper.constructType(returnActualType));
return new JettyReactiveHttpClient(httpClient,
getClass(bodyActualType), returnPublisherType, getClass(returnActualType),
jsonFactory, bodyWriter, responseReader);
}
public JettyReactiveHttpClient(HttpClient httpClient,
Class bodyActualClass, Class returnPublisherClass, Class returnActualClass,
JsonFactory jsonFactory, ObjectWriter bodyWriter, ObjectReader responseReader) {
this.httpClient = httpClient;
this.bodyActualClass = bodyActualClass;
this.returnPublisherClass = returnPublisherClass;
this.returnActualClass = returnActualClass;
this.jsonFactory = jsonFactory;
this.bodyWriter = bodyWriter;
this.responseReader = responseReader;
}
public JettyReactiveHttpClient setRequestTimeout(long timeoutInMillis) {
this.requestTimeout = timeoutInMillis;
return this;
}
public JettyReactiveHttpClient setTryUseCompression(boolean tryUseCompression) {
this.tryUseCompression = tryUseCompression;
return this;
}
@Override
public Mono executeRequest(ReactiveHttpRequest request) {
Request jettyRequest = httpClient
.newRequest(request.uri())
.headers(httpFields -> setUpHeaders(request, httpFields))
.method(request.method());
if (requestTimeout > 0) {
jettyRequest.timeout(requestTimeout, TimeUnit.MILLISECONDS);
}
ReactiveRequest.Builder requestBuilder = ReactiveRequest.newBuilder(jettyRequest);
if (bodyActualClass != null
|| request.body() instanceof SerializedFormData) {
requestBuilder.content(provideBody(request));
}
return Mono.from(requestBuilder.build().response((response, content) -> Mono.just(
new JettyReactiveHttpResponse(
request,
response.getResponse(),
content,
returnPublisherClass,
returnActualClass,
jsonFactory,
responseReader)
)))
.onErrorMap(ex -> {
if (ex instanceof java.util.concurrent.TimeoutException) {
return new ReadTimeoutException(ex, request);
} else {
return new ReactiveFeignException(ex, request);
}
});
}
protected void setUpHeaders(ReactiveHttpRequest request, HttpFields.Mutable httpHeaders) {
request.headers().forEach(httpHeaders::put);
String acceptHeader;
if (CharSequence.class.isAssignableFrom(returnActualClass) && returnPublisherClass == Mono.class) {
acceptHeader = TEXT;
} else if (returnActualClass == ByteBuffer.class || returnActualClass == byte[].class) {
acceptHeader = APPLICATION_OCTET_STREAM;
} else if (returnPublisherClass == Mono.class) {
acceptHeader = APPLICATION_JSON;
} else {
acceptHeader = APPLICATION_STREAM_JSON;
}
httpHeaders.put(ACCEPT.asString(), singletonList(acceptHeader));
if (tryUseCompression) {
httpHeaders.put(ACCEPT_ENCODING.asString(), singletonList(GZIP));
} else {
httpHeaders.remove(ACCEPT_ENCODING.asString());
}
}
protected ReactiveRequest.Content provideBody(ReactiveHttpRequest request) {
Publisher bodyPublisher;
String contentType;
if (request.body() instanceof SerializedFormData) {
bodyPublisher = Mono.just(toByteBufferChunk(((SerializedFormData) request.body()).getFormData(), false));
contentType = FORM_URL_ENCODED;
} else if (request.body() instanceof Mono) {
if (bodyActualClass == ByteBuffer.class) {
bodyPublisher = ((Mono) request.body()).map(data -> toByteBufferChunk(data, false));
contentType = APPLICATION_OCTET_STREAM;
} else if (CharSequence.class.isAssignableFrom(bodyActualClass)) {
bodyPublisher = Flux.from(request.body()).map(data -> toCharSequenceChunk(data, false));
contentType = TEXT_UTF_8;
} else {
bodyPublisher = Flux.from(request.body()).map(data -> toJsonChunk(data, false));
contentType = APPLICATION_JSON_UTF_8;
}
} else {
if (bodyActualClass == ByteBuffer.class) {
bodyPublisher = Flux.concat(
Flux.from(request.body()).map(data -> toByteBufferChunk(data, true)),
Flux.just(Content.Chunk.EOF)
);
contentType = APPLICATION_OCTET_STREAM;
} else {
bodyPublisher = Flux.concat(
Flux.from(request.body()).map(data -> toJsonChunk(data, true)),
Flux.just(Content.Chunk.EOF)
);
contentType = APPLICATION_STREAM_JSON_UTF_8;
}
}
//TODO
//String originalContentType = request.headers().get(CONTENT_TYPE_HEADER).get(0);
return ReactiveRequest.Content.fromPublisher(bodyPublisher, contentType);
}
protected Content.Chunk toByteBufferChunk(Object data, boolean stream) {
return Content.Chunk.from((ByteBuffer) data, !stream);
}
protected Content.Chunk toCharSequenceChunk(Object data, boolean stream) {
CharBuffer charBuffer = CharBuffer.wrap((CharSequence) data);
ByteBuffer byteBuffer = UTF_8.encode(charBuffer);
return Content.Chunk.from(byteBuffer, !stream);
}
protected Content.Chunk toJsonChunk(Object data, boolean stream) {
try {
ByteArrayBuilder byteArrayBuilder = new ByteArrayBuilder();
bodyWriter.writeValue(byteArrayBuilder, data);
if (stream) {
byteArrayBuilder.write(NEWLINE_SEPARATOR);
}
ByteBuffer buffer = ByteBuffer.wrap(byteArrayBuilder.toByteArray());
return Content.Chunk.from(buffer, !stream);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static Class getClass(Type type) {
return (Class) (type instanceof ParameterizedType pt ? pt.getRawType() : type);
}
}