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

estonlabs.cxtl.common.http.JsonRestClient Maven / Gradle / Ivy

package estonlabs.cxtl.common.http;

import estonlabs.cxtl.common.codec.Codec;
import estonlabs.cxtl.common.codec.JacksonCodec;
import estonlabs.cxtl.common.exception.CxtlEventException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;

import java.io.IOException;
import java.net.Proxy;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

/**
 * okhttp3 backed Rest Client for sending and receiving JSON
 */
public class JsonRestClient implements RestClient{
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonRestClient.class);
    private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
    private final String urlPrefix;
    private final OkHttpClient client;
    private final Codec codec;
    public JsonRestClient(URI uri, Codec codec, Proxy proxy) {
        this.urlPrefix = uri.toString();
        this.codec = codec;
        OkHttpClient.Builder httpClientBuilder = new OkHttpClient().newBuilder();
        this.client = (proxy == null ? httpClientBuilder: httpClientBuilder.proxy(proxy)).build();
    }

    @Override
    public  Mono> deleteAsForm(HeaderBuilder header, String path, IN message, Class responseType) {
        final Map map = codec.toMap(message);
        final String strMessage = JacksonCodec.mapToStringBuilder(map,new StringBuilder()).toString();
        RequestBody body = formBody(map);

        LOGGER.info("DELETE: {}{} [{}]",urlPrefix,path, strMessage);

        return delete(header, path, responseType, strMessage, body);
    }

    @NonNull
    private  Mono> delete(HeaderBuilder header, String path, Class responseType, String strMessage, RequestBody body) {
        Request.Builder requestBuilder = buildRequest(header, path, strMessage);
        return Mono.fromSupplier(() ->
                requestBuilder
                        .delete(body)
                        .build()).flatMap(r ->
                Mono.create(sink ->
                        client.newCall(r).enqueue(
                                new FnCallback<>(sink, new Event<>(path + "(" + strMessage + ")" + headers(r)), res -> codec.fromJson(res, responseType)))));
    }

    @Override
    public  Mono> deleteAsParams(HeaderBuilder header, String path, IN message, Class responseType) {
        final Map map = codec.toMap(message);
        final StringBuilder strMessage = JacksonCodec.mapToStringBuilder(map,new StringBuilder());

        return parameterisedRequest(Method.DELETE,header, path, strMessage, json -> codec.fromJson(json, responseType));
    }

    @Override
    public  Mono> putEmpty(HeaderBuilder header, String path, Class responseType) {
        return parameterisedRequest(Method.PUT,header, path, null, json -> codec.fromJson(json, responseType));
    }

    @Override
    public  Mono> postEmpty(HeaderBuilder header, String path, Class responseType) {
        return parameterisedRequest(Method.POST,header, path, null, json -> codec.fromJson(json, responseType));
    }
    @Override
    public  Mono> postAsJson(HeaderBuilder header, String path, IN message, Class responseType) {
        final String json = codec.toJson(message);
        RequestBody body = RequestBody.create(json, MEDIA_TYPE);

        return post(header, path, responseType, json, body);
    }

    @Override
    public  Mono> postAsForm(HeaderBuilder header, String path, IN message, Class responseType) {
        final Map map = codec.toMap(message);
        final String strMessage = JacksonCodec.mapToStringBuilder(map,new StringBuilder()).toString();
        RequestBody body = formBody(map);
        return post(header, path, responseType, strMessage, body);
    }

    @Override
    public  Mono> postAsParams(HeaderBuilder header, String path, IN message, Class responseType) {
        final Map map = codec.toMap(message);
        final StringBuilder strMessage = JacksonCodec.mapToStringBuilder(map,new StringBuilder());

        return parameterisedRequest(Method.POST,header, path, strMessage, json -> codec.fromJson(json, responseType));
    }

    @Override
    public  Mono> get(HeaderBuilder header, String path, Class responseType) {
        return parameterisedRequest(Method.GET, header,path,null, json -> codec.fromJson(json, responseType));
    }

    @Override
    public  Mono> get(HeaderBuilder header, String path, IN message, Class responseType) {
        return parameterisedRequest(Method.GET, header,path,message, json -> codec.fromJson(json, responseType));
    }

    @Override
    public  Mono>> getMany(HeaderBuilder header, String path, IN message, Class responseType) {
        return parameterisedRequest(Method.GET, header,path,message, json -> codec.fromJsonArray(json, responseType));
    }

    @Override
    public  Mono> postAsJson(String path, IN message, Class responseType) {
        return postAsJson(null,path,message,responseType);
    }

    @Override
    public  Mono> get(String path, IN message, Class responseType) {
        return get(null,path,message,responseType);
    }

    @Override
    public  Mono> get(String path, Class responseType) {
        return get(null,path,null,responseType);
    }

    @Override
    public  Mono>> getMany(String path, IN message, Class responseType) {
        return getMany(null, path,message,responseType);
    }

    @Override
    public  Mono>> getMany(HeaderBuilder header, String path, Class responseType) {
        return getMany(header, path,null,responseType);
    }

    @Override
    public  Mono>> getMany(String path, Class responseType) {
        return getMany(null, path,null,responseType);
    }

    @Override
    public Codec getCodec() {
        return codec;
    }

    @Override
    public  Mono> put(HeaderBuilder header, String path, IN message, Class responseType) {
        RequestBody body = RequestBody.create(codec.toJson(message), MEDIA_TYPE);
        return parameterisedRequest(Method.PUT, header, path, body, json -> codec.fromJson(json, responseType));
    }

    @NonNull
    private  Mono> post(HeaderBuilder header, String path, Class responseType, String message, RequestBody body) {
        LOGGER.info("POST: {}{} [{}]",urlPrefix, path, message);
        Request.Builder requestBuilder = buildRequest(header, path, message);
        return Mono.fromSupplier(() ->
                requestBuilder
                        .post(body)
                        .build()).flatMap(r ->
                Mono.create(sink ->
                        client.newCall(r).enqueue(
                                new FnCallback<>(sink, new Event<>(path + "(" + message + ")" + headers(r)), res -> codec.fromJson(res, responseType)))));
    }


    private  Mono> doParameterisedRequest(
            Method method,
            Map headers,
            String path,
            String queryString,
            RequestBody body,
            Function postProcessor
    ){
        HttpUrl url = Objects.requireNonNull(HttpUrl.parse(urlPrefix + path)).newBuilder()
                .encodedQuery(queryString)
                .build();

        Request.Builder builder = new Request.Builder();
        builder.url(url);
        headers.forEach(builder::addHeader);
        builder.method(method.name(), body);
        LOGGER.debug("{}: {}",method, url);
        return Mono.fromSupplier(builder::build)
                .flatMap(r->
                        Mono.create( sink ->
                                client.newCall(r).enqueue(
                                        new FnCallback<>(sink, new Event<>(method + ": " + url), postProcessor))));
    }

    @Override
    public  Mono> parameterisedRequest(
            Method method,
            Map headers,
            String path,
            String queryString,
            RequestBody payload,
            Class responseType
    ) {
        return doParameterisedRequest(method, headers, path, queryString, payload, json -> codec.fromJson(json, responseType));
    }

    @Override
    public   Mono>> parameterisedRequestMany(
            Method method,
            Map headers,
            String path,
            String queryString,
            RequestBody payload,
            Class responseType
    ) {
        return doParameterisedRequest(method, headers, path, queryString, payload, json -> codec.fromJsonArray(json, responseType));
    }

    private  Mono> parameterisedRequest(Method method, HeaderBuilder header, String path, StringBuilder queryString, Function fn) {
        String params = queryString==null?null:queryString.toString();
        String url = queryString==null?path:queryString.insert(0,"?").insert(0,path).toString();

        LOGGER.debug("{}/{}: {}", urlPrefix,method.name(),url);
        return Mono.fromSupplier(() ->
                        addMethod(method, buildRequest(header, url, params)).build())
                .flatMap(r->
                        Mono.create( sink ->
                                client.newCall(r).enqueue(
                                        new FnCallback<>(sink, new Event<>(url), fn))));
    }
    @NonNull
    private  Mono> parameterisedRequest(Method method, HeaderBuilder header, String path, IN message, Function fn) {
        return parameterisedRequest(method,header,path,codec.toQueryString(message),fn);

    }

    private Request.Builder buildRequest(HeaderBuilder header, String path, String message) {
        Request.Builder builder = new Request.Builder();
        builder.url(urlPrefix + path);
        if(header!=null){
            return header.addHeaders(builder, message);
        }else return builder;
    }

    @NonNull
    private static Request.Builder addMethod(Method method, Request.Builder builder) {
        if(method == Method.GET){
            builder = builder.get();
        }else if(method == Method.POST){
            builder = builder.post(RequestBody.create("", MEDIA_TYPE));
        }else if(method == Method.PUT){
            builder = builder.put(RequestBody.create("", MEDIA_TYPE));
        }else{
            builder = builder.delete(RequestBody.create("", MEDIA_TYPE));
        }
        return builder;
    }

    @NonNull
    private static RequestBody formBody(Map map) {
        FormBody.Builder builder = new FormBody.Builder();
        for(Map.Entry e : map.entrySet()){
            builder.add(e.getKey(), e.getValue().toString());
        }
        return builder.build();
    }

    @NonNull
    private static String headers(Request request){
        StringBuilder b = new StringBuilder("[");
        Headers headers = request.headers();
        headers.names().forEach(n -> b.append(n).append("=").append(headers.values(n)).append(","));
        b.append("]");
        return b.toString();
    }

    @RequiredArgsConstructor
    private static class FnCallback implements Callback{
        private final MonoSink> sink;
        private final Event event;
        private final Function fn;
        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            sink.error(e);
        }

        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
            try (response; var responseBody = response.body()) {
                String json = responseBody.string();
                try {
                    O value = fn.apply(json);
                    event.response(json, value);
                    sink.success(event);
                } catch (Exception e) {
                    LOGGER.error("Failed to process {} response: {}",event.getRequestInfo(), json, e);
                    event.response(json, null);
                    sink.error(new CxtlEventException(e, event));
                }
            }
        }
    }
}