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

org.cometd.client.http.okhttp.OkHttpClientTransport Maven / Gradle / Ivy

/*
 * Copyright (c) 2008-2020 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 org.cometd.client.http.okhttp;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.CookieJar;
import okhttp3.Dispatcher;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.Promise;
import org.cometd.client.http.common.AbstractHttpClientTransport;
import org.cometd.client.transport.ClientTransport;
import org.cometd.client.transport.TransportListener;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OkHttpClientTransport extends AbstractHttpClientTransport {
    private static final Logger LOGGER = LoggerFactory.getLogger(OkHttpClientTransport.class);
    private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json;charset=UTF-8");

    private final List _calls = new ArrayList<>();
    private final OkHttpClient _client;

    public OkHttpClientTransport(Map options, OkHttpClient client) {
        this(null, options, client);
    }

    public OkHttpClientTransport(String url, Map options, OkHttpClient client) {
        this(url, options, null, client);
    }

    public OkHttpClientTransport(String url, Map options, ScheduledExecutorService scheduler, OkHttpClient client) {
        super(url, options, scheduler);
        _client = client.newBuilder()
                .cookieJar(CookieJar.NO_COOKIES)
                .addInterceptor(new SendingInterceptor())
                .build();
    }

    protected OkHttpClient getOkHttpClient() {
        return _client;
    }

    @Override
    public void init() {
        super.init();
        setMaxNetworkDelay(10000);
        // OkHttpClient default values are maxRequestsPerHost=5 and
        // maxRequests=64, both too small. Using Jetty HttpClient defaults.
        Dispatcher dispatcher = getOkHttpClient().dispatcher();
        dispatcher.setMaxRequestsPerHost(64);
        dispatcher.setMaxRequests(Integer.MAX_VALUE);
    }

    @Override
    public void abort(Throwable failure) {
        List requests;
        synchronized (this) {
            super.abort(failure);
            requests = new ArrayList<>(_calls);
            _calls.clear();
        }
        requests.forEach(Call::cancel);
    }

    @Override
    public void send(TransportListener listener, List messages) {
        Request.Builder request = new Request.Builder()
                .url(newRequestURI(messages))
                .post(RequestBody.create(generateJSON(messages), JSON_MEDIA_TYPE));

        URI cookieURI = URI.create(getURL());
        String cookies = getCookies(cookieURI).stream()
                .map(cookie -> cookie.getName() + "=" + cookie.getValue())
                .collect(Collectors.joining(";"));
        if (!cookies.isEmpty()) {
            request = request.header("Cookie", cookies);
        }

        customize(request, Promise.from(
                customizedRequest -> send(listener, messages, cookieURI, customizedRequest),
                failure -> listener.onFailure(failure, messages)));
    }

    protected void customize(Request.Builder request, Promise promise) {
        promise.succeed(request);
    }

    private void send(TransportListener listener, List messages, URI cookieURI, Request.Builder request) {
        long maxNetworkDelay = calculateMaxNetworkDelay(messages);
        OkHttpClient client = _client.newBuilder()
                // Disable the read timeout.
                .readTimeout(0, TimeUnit.MILLISECONDS)
                // Schedule a task to timeout the request.
                .build();

        request = request
                .tag(TransportListener.class, listener)
                .tag(List.class, messages);

        Call call = client.newCall(request.build());

        AtomicReference> timeoutRef = new AtomicReference<>();
        ScheduledExecutorService scheduler = getScheduler();
        if (scheduler != null) {
            ScheduledFuture newTask = scheduler.schedule(() -> onTimeout(listener, messages, call, maxNetworkDelay, timeoutRef), maxNetworkDelay, TimeUnit.MILLISECONDS);
            timeoutRef.set(newTask);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Started waiting for message replies, {} ms, task@{}", maxNetworkDelay, Integer.toHexString(newTask.hashCode()));
            }
        }

        synchronized (this) {
            if (!isAborted()) {
                _calls.add(call);
            }
        }

        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                int code = response.code();
                if (code == 200) {
                    storeCookies(cookieURI, response.headers().toMultimap());
                    // Blocking I/O, unfortunately.
                    try (ResponseBody body = response.body()) {
                        String content = body == null ? "" : body.string();
                        ScheduledFuture task = timeoutRef.get();
                        if (task != null) {
                            task.cancel(false);
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug("Cancelled waiting for message replies (HTTP {}) task@{}", code, Integer.toHexString(task.hashCode()));
                            }
                        }
                        processResponseContent(listener, messages, content);
                    }
                } else {
                    ScheduledFuture task = timeoutRef.get();
                    if (task != null) {
                        task.cancel(false);
                        if (LOGGER.isDebugEnabled()) {
                            LOGGER.debug("Cancelled waiting for message replies (HTTP {}) task@{}", code, Integer.toHexString(task.hashCode()));
                        }
                    }
                    processWrongResponseCode(listener, messages, code);
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                ScheduledFuture task = timeoutRef.get();
                if (task != null) {
                    task.cancel(false);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Cancelled waiting for failed message replies task@{}", Integer.toHexString(task.hashCode()) , e);
                    }
                }
                listener.onFailure(e, messages);
            }
        });
    }

    private void onTimeout(TransportListener listener, List messages, Call call, long delay, AtomicReference> timeoutRef) {
        listener.onTimeout(messages, Promise.from(result -> {
            if (result > 0) {
                ScheduledExecutorService scheduler = getScheduler();
                if (scheduler != null) {
                    ScheduledFuture newTask = scheduler.schedule(() -> onTimeout(listener, messages, call, delay + result, timeoutRef), result, TimeUnit.MILLISECONDS);
                    ScheduledFuture oldTask = timeoutRef.getAndSet(newTask);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Extended waiting for message replies, {} ms, oldTask@{}, newTask@{}", result, Integer.toHexString(oldTask.hashCode()), Integer.toHexString(newTask.hashCode()));
                    }
                }
            } else {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Network delay expired: {} ms", delay);
                }
                call.cancel();
            }
        }, x -> call.cancel()));
    }

    public static class Factory extends ContainerLifeCycle implements ClientTransport.Factory {
        private final OkHttpClient _client;

        public Factory(OkHttpClient httpClient) {
            _client = httpClient;
            addBean(httpClient);
        }

        @Override
        public ClientTransport newClientTransport(String url, Map options) {
            return new OkHttpClientTransport(url, options, _client);
        }
    }

    private static class SendingInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            TransportListener listener = request.tag(TransportListener.class);
            if (listener != null) {
                @SuppressWarnings("unchecked")
                List messages = request.tag(List.class);
                if (messages != null) {
                    listener.onSending(messages);
                }
            }
            return chain.proceed(request);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy