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

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

The newest version!
/*
 * Copyright (c) 2008 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.eclipse.jetty.util.thread.AutoLock;
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 AutoLock lock = new AutoLock();
    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);
        this.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;
        try (AutoLock ignored = lock.lock()) {
            super.abort(failure);
            requests = new ArrayList<>(calls);
            calls.clear();
        }
        requests.forEach(Call::cancel);
    }

    @Override
    public void send(TransportListener listener, List messages) {
        try {
            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)));
        } catch (Throwable x) {
            listener.onFailure(x, messages);
        }
    }

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

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

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

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

        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()));
            }
        }

        try (AutoLock ignored = lock.lock()) {
            if (!isAborted()) {
                calls.add(call);
            }
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Sending request {}", request);
        }
        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Received response {}", response);
                }
                try (AutoLock ignored = lock.lock()) {
                    calls.remove(call);
                }
                int code = response.code();
                if (code == 200) {
                    storeCookies(cookieURI, response.headers().toMultimap());
                    // Blocking I/O, unfortunately.
                    try (ResponseBody body = response.body()) {
                        cancelTimeoutTask(timeoutRef);
                        String content = body == null ? "" : body.string();
                        processResponseContent(listener, messages, content);
                    }
                } else {
                    cancelTimeoutTask(timeoutRef);
                    processWrongResponseCode(listener, messages, code);
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Received response failure", e);
                }
                try (AutoLock ignored = lock.lock()) {
                    calls.remove(call);
                }
                cancelTimeoutTask(timeoutRef);
                listener.onFailure(e, messages);
            }

            private void cancelTimeoutTask(AtomicReference> timeoutRef) {
                ScheduledFuture task = timeoutRef.get();
                if (task != null) {
                    task.cancel(false);
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Cancelled request timeout task@{}", Integer.toHexString(task.hashCode()));
                    }
                }
            }
        });
    }

    private void onTimeout(TransportListener listener, List messages, Call call, long delay, AtomicReference> timeoutRef) {
        try (AutoLock ignored = lock.lock()) {
            calls.remove(call);
        }
        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() {
            this(new OkHttpClient());
        }

        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