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

org.jboss.resteasy.reactive.client.impl.MultiInvoker Maven / Gradle / Ivy

There is a newer version: 3.17.5
Show newest version
package org.jboss.resteasy.reactive.client.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;

import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.client.SseEvent;
import org.jboss.resteasy.reactive.client.SseEventFilter;
import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl;
import org.jboss.resteasy.reactive.common.util.RestMediaType;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.subscription.MultiEmitter;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.net.impl.ConnectionBase;
import io.vertx.core.parsetools.RecordParser;

public class MultiInvoker extends AbstractRxInvoker> {

    private final InvocationBuilderImpl invocationBuilder;

    public MultiInvoker(InvocationBuilderImpl target) {
        this.invocationBuilder = target;
    }

    @SuppressWarnings("unchecked")
    @Override
    public  Multi get(Class responseType) {
        return (Multi) super.get(responseType);
    }

    @SuppressWarnings("unchecked")
    @Override
    public  Multi get(GenericType responseType) {
        return (Multi) super.get(responseType);
    }

    /**
     * We need this class to work around a bug in Mutiny where we can register our cancel listener
     * after the subscription is cancelled, and we never get notified
     * See ...
     */
    static class MultiRequest {

        private final AtomicReference onCancel = new AtomicReference<>();

        private final MultiEmitter emitter;

        private static final Runnable CLEARED = () -> {
        };

        public MultiRequest(MultiEmitter emitter) {
            this.emitter = emitter;
            emitter.onTermination(() -> {
                if (emitter.isCancelled()) {
                    this.cancel();
                }
            });
        }

        void emit(R item) {
            if (!isCancelled()) {
                emitter.emit(item);
            }
        }

        void fail(Throwable t) {
            if (!isCancelled()) {
                emitter.fail(t);
                cancel();
            }
        }

        void complete() {
            if (!isCancelled()) {
                emitter.complete();
                cancel();
            }
        }

        public boolean isCancelled() {
            return onCancel.get() == CLEARED;
        }

        private void cancel() {
            Runnable action = onCancel.getAndSet(CLEARED);
            if (action != null && action != CLEARED) {
                action.run();
            }
        }

        public void onCancel(Runnable onCancel) {
            if (this.onCancel.compareAndSet(null, onCancel)) {
                // this was a first set
            } else if (this.onCancel.get() == CLEARED) {
                // already cleared
                if (onCancel != null)
                    onCancel.run();
            } else {
                // it was already set
                throw new IllegalArgumentException("onCancel was already called");
            }
        }
    }

    @Override
    public  Multi method(String name, Entity entity, GenericType responseType) {
        AsyncInvokerImpl invoker = (AsyncInvokerImpl) invocationBuilder.rx();
        // FIXME: backpressure setting?
        return Multi.createFrom().emitter(emitter -> {
            MultiRequest multiRequest = new MultiRequest<>(emitter);
            RestClientRequestContext restClientRequestContext = invoker.performRequestInternal(name, entity, responseType,
                    false);
            restClientRequestContext.getResult().handle((response, connectionError) -> {
                if (connectionError != null) {
                    emitter.fail(connectionError);
                } else {
                    HttpClientResponse vertxResponse = restClientRequestContext.getVertxClientResponse();
                    if (!emitter.isCancelled()) {
                        if (response.getStatus() == 200
                                && MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(response.getMediaType())) {
                            registerForSse(
                                    multiRequest, responseType, vertxResponse,
                                    (String) restClientRequestContext.getProperties()
                                            .get(RestClientRequestContext.DEFAULT_CONTENT_TYPE_PROP),
                                    restClientRequestContext.getInvokedMethod());
                        } else if (response.getStatus() == 200
                                && RestMediaType.APPLICATION_STREAM_JSON_TYPE.isCompatible(response.getMediaType())) {
                            registerForJsonStream(multiRequest, restClientRequestContext, responseType, response,
                                    vertxResponse);
                        } else {
                            // read stuff in chunks
                            registerForChunks(multiRequest, restClientRequestContext, responseType, response, vertxResponse);
                        }
                        vertxResponse.resume();
                    } else {
                        vertxResponse.request().connection().close();
                    }
                }
                return null;
            });
        });
    }

    private boolean isNewlineDelimited(ResponseImpl response) {
        return RestMediaType.APPLICATION_STREAM_JSON_TYPE.isCompatible(response.getMediaType()) ||
                RestMediaType.APPLICATION_NDJSON_TYPE.isCompatible(response.getMediaType());
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private  void registerForSse(MultiRequest multiRequest,
            GenericType responseType,
            HttpClientResponse vertxResponse, String defaultContentType,
            Method invokedMethod) {

        boolean returnSseEvent = SseEvent.class.equals(responseType.getRawType());
        GenericType responseTypeFirstParam = responseType.getType() instanceof ParameterizedType
                ? new GenericType(((ParameterizedType) responseType.getType()).getActualTypeArguments()[0])
                : null;

        Predicate> eventPredicate = createEventPredicate(invokedMethod);

        // honestly, isn't reconnect contradictory with completion?
        // FIXME: Reconnect settings?
        // For now we don't want multi to reconnect
        SseEventSourceImpl sseSource = new SseEventSourceImpl(invocationBuilder.getTarget(),
                invocationBuilder, Integer.MAX_VALUE, TimeUnit.SECONDS, defaultContentType);

        multiRequest.onCancel(sseSource::close);
        sseSource.register(event -> {

            // TODO: we might want to cut down on the allocations here...

            if (eventPredicate != null) {
                boolean keep = eventPredicate.test(new SseEvent<>() {
                    @Override
                    public String id() {
                        return event.getId();
                    }

                    @Override
                    public String name() {
                        return event.getName();
                    }

                    @Override
                    public String comment() {
                        return event.getComment();
                    }

                    @Override
                    public String data() {
                        return event.readData();
                    }
                });
                if (!keep) {
                    return;
                }
            }

            // DO NOT pass the response mime type because it's SSE: let the event pick between the X-SSE-Content-Type header or
            // the content-type SSE field

            if (returnSseEvent) {
                multiRequest.emit((R) new SseEvent() {
                    @Override
                    public String id() {
                        return event.getId();
                    }

                    @Override
                    public String name() {
                        return event.getName();
                    }

                    @Override
                    public String comment() {
                        return event.getComment();
                    }

                    @Override
                    public Object data() {
                        if (responseTypeFirstParam != null) {
                            return event.readData(responseTypeFirstParam);
                        } else {
                            return event.readData(); // TODO: is this correct?
                        }
                    }
                });
            } else {
                R item = event.readData(responseType);
                if (item != null) { // we don't emit null because it breaks Multi (by design)
                    multiRequest.emit(item);
                }
            }

        }, multiRequest::fail, multiRequest::complete);
        // watch for user cancelling
        sseSource.registerAfterRequest(vertxResponse);
    }

    private Predicate> createEventPredicate(Method invokedMethod) {
        if (invokedMethod == null) {
            return null; // should never happen
        }

        SseEventFilter filterAnnotation = invokedMethod.getAnnotation(SseEventFilter.class);
        if (filterAnnotation == null) {
            return null;
        }

        try {
            return filterAnnotation.value().getConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    private  void registerForChunks(MultiRequest multiRequest,
            RestClientRequestContext restClientRequestContext,
            GenericType responseType,
            ResponseImpl response,
            HttpClientResponse vertxClientResponse) {
        boolean isNewlineDelimited = isNewlineDelimited(response);
        // make sure we get exceptions on the response, like close events, otherwise they
        // will be logged as errors by vertx
        vertxClientResponse.exceptionHandler(t -> {
            if (t == ConnectionBase.CLOSED_EXCEPTION) {
                // we can ignore this one since we registered a closeHandler
            } else {
                multiRequest.emitter.fail(t);
            }
        });
        // we don't add a closeHandler handler on the connection as it can race with this handler
        // and close before the emitter emits anything
        // see: https://github.com/quarkusio/quarkus/pull/16438
        vertxClientResponse.handler(new Handler() {
            @Override
            public void handle(Buffer buffer) {
                try {
                    byte[] bytes = buffer.getBytes();
                    MediaType mediaType = response.getMediaType();

                    if (isNewlineDelimited) {
                        String charset = mediaType.getParameters().get(MediaType.CHARSET_PARAMETER);
                        charset = charset == null ? "UTF-8" : charset;
                        byte[] separator = "\n".getBytes(charset);
                        int start = 0;
                        while (start < bytes.length) {
                            int end = bytes.length;
                            for (int i = start; i < end; i++) {
                                if (bytes[i] == separator[0]) {
                                    int j;
                                    boolean matches = true;
                                    for (j = 1; j < separator.length; j++) {
                                        if (bytes[i + j] != separator[j]) {
                                            matches = false;
                                            break;
                                        }
                                    }
                                    if (matches) {
                                        end = i;
                                        break;
                                    }
                                }
                            }

                            if (start < end) {
                                ByteArrayInputStream in = new ByteArrayInputStream(bytes, start, end);
                                R item = restClientRequestContext.readEntity(
                                        in,
                                        responseType,
                                        mediaType,
                                        restClientRequestContext.getMethodDeclaredAnnotationsSafe(),
                                        response.getMetadata());
                                multiRequest.emitter.emit(item);
                            }
                            start = end + separator.length;
                        }
                    } else {
                        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
                        R item = restClientRequestContext.readEntity(
                                in,
                                responseType,
                                mediaType,
                                restClientRequestContext.getMethodDeclaredAnnotationsSafe(),
                                response.getMetadata());
                        multiRequest.emitter.emit(item);
                    }
                } catch (Throwable t) {
                    // FIXME: probably close the client too? watch out that it doesn't call our close handler
                    // which calls emitter.complete()
                    multiRequest.emitter.fail(t);
                }
            }
        });

        // this captures the end of the response
        vertxClientResponse.endHandler(v -> {
            multiRequest.emitter.complete();
        });
        // watch for user cancelling
        multiRequest.onCancel(() -> {
            vertxClientResponse.request().connection().close();
        });
    }

    private  void registerForJsonStream(MultiRequest multiRequest,
            RestClientRequestContext restClientRequestContext,
            GenericType responseType,
            ResponseImpl response,
            HttpClientResponse vertxClientResponse) {
        RecordParser parser = RecordParser.newDelimited("\n");
        parser.handler(new Handler() {
            @Override
            public void handle(Buffer chunk) {

                ByteArrayInputStream in = new ByteArrayInputStream(chunk.getBytes());
                try {
                    R item = restClientRequestContext.readEntity(in,
                            responseType,
                            response.getMediaType(),
                            restClientRequestContext.getMethodDeclaredAnnotationsSafe(),
                            response.getMetadata());
                    multiRequest.emit(item);
                } catch (IOException e) {
                    multiRequest.fail(e);
                }
            }
        });
        vertxClientResponse.exceptionHandler(t -> {
            if (t == ConnectionBase.CLOSED_EXCEPTION) {
                // we can ignore this one since we registered a closeHandler
            } else {
                multiRequest.fail(t);
            }
        });
        vertxClientResponse.endHandler(new Handler() {
            @Override
            public void handle(Void c) {
                multiRequest.complete();
            }
        });

        vertxClientResponse.handler(parser);

        // watch for user cancelling
        multiRequest.onCancel(() -> {
            vertxClientResponse.request().connection().close();
        });
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy