org.jboss.resteasy.reactive.server.handlers.PublisherResponseHandler Maven / Gradle / Ivy
package org.jboss.resteasy.reactive.server.handlers;
import static org.jboss.resteasy.reactive.server.jaxrs.SseEventSinkImpl.EMPTY_BUFFER;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.sse.OutboundSseEvent;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestMulti;
import org.jboss.resteasy.reactive.common.util.RestMediaType;
import org.jboss.resteasy.reactive.common.util.ServerMediaType;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.core.SseUtil;
import org.jboss.resteasy.reactive.server.core.StreamingUtil;
import org.jboss.resteasy.reactive.server.jaxrs.OutboundSseEventImpl;
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer.Phase;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
import org.jboss.resteasy.reactive.server.spi.StreamingResponse;
import mutiny.zero.flow.adapters.AdaptersToFlow;
/**
* This handler is used to push streams of data to the client.
* This handler is added to the chain in the {@link Phase#AFTER_METHOD_INVOKE} phase and is essentially the terminal phase
* of the handler chain, as no other handlers will be called after this one.
*/
public class PublisherResponseHandler implements ServerRestHandler {
private static final String JSON = "json";
private static final ServerMediaType REST_MULTI_DEFAULT_SERVER_MEDIA_TYPE = new ServerMediaType(
List.of(MediaType.APPLICATION_OCTET_STREAM_TYPE), StandardCharsets.UTF_8.name(), false);
private List streamingResponseCustomizers = Collections.emptyList();
public void setStreamingResponseCustomizers(List streamingResponseCustomizers) {
this.streamingResponseCustomizers = streamingResponseCustomizers;
}
private static class SseMultiSubscriber extends AbstractMultiSubscriber {
SseMultiSubscriber(ResteasyReactiveRequestContext requestContext, List staticCustomizers,
long demand) {
super(requestContext, staticCustomizers, demand);
}
@Override
public void onNext(Object item) {
OutboundSseEvent event;
if (item instanceof OutboundSseEvent) {
event = (OutboundSseEvent) item;
} else {
event = new OutboundSseEventImpl.BuilderImpl().data(item).build();
}
SseUtil.send(requestContext, event, staticCustomizers).whenComplete((v, t) -> {
if (t != null) {
// need to cancel because the exception didn't come from the Multi
subscription.cancel();
handleException(requestContext, t);
} else {
// send in the next item
subscription.request(demand);
}
});
}
}
@SuppressWarnings("rawtypes")
private static class StreamingMultiSubscriber extends AbstractMultiSubscriber {
private static final String LINE_SEPARATOR = "\n";
private final Publisher publisher;
private final boolean json;
private final boolean encodeAsJsonArray;
// Huge hack to stream valid json
private volatile String nextJsonPrefix;
private volatile boolean hadItem;
StreamingMultiSubscriber(ResteasyReactiveRequestContext requestContext,
List staticCustomizers, Publisher publisher,
boolean json, long demand, boolean encodeAsJsonArray) {
super(requestContext, staticCustomizers, demand);
this.publisher = publisher;
this.json = json;
// encodeAsJsonArray == true means JSON array "encoding"
// encodeAsJsonArray == false mean no prefix, no suffix and LF as message separator,
// also used for/same as chunked-streaming
this.encodeAsJsonArray = encodeAsJsonArray;
this.nextJsonPrefix = encodeAsJsonArray ? "[" : null;
this.hadItem = false;
}
@Override
public void onNext(Object item) {
List customizers = determineCustomizers(!hadItem);
hadItem = true;
StreamingUtil.send(requestContext, customizers, item, messagePrefix(), messageSuffix())
.handle((v, t) -> {
if (t != null) {
// need to cancel because the exception didn't come from the Multi
try {
subscription.cancel();
} catch (Throwable t2) {
t2.printStackTrace();
}
handleException(requestContext, t);
} else {
// next item will need this prefix if json
nextJsonPrefix = encodeAsJsonArray ? "," : null;
// send in the next item
subscription.request(demand);
}
return null;
});
}
private List determineCustomizers(boolean isFirst) {
// we only need to obtain the customizers from the Publisher if it's the first time we are sending data and the Publisher has customizable data
// at this point no matter the type of RestMulti we can safely obtain the headers and status
if (isFirst && (publisher instanceof RestMulti> restMulti)) {
Map> headers = restMulti.getHeaders();
Integer status = restMulti.getStatus();
if (headers.isEmpty() && (status == null)) {
return staticCustomizers;
}
List result = new ArrayList<>(staticCustomizers.size() + 2);
result.addAll(staticCustomizers); // these are added first so that the result specific values will take precedence if there are conflicts
if (!headers.isEmpty()) {
result.add(new StreamingResponseCustomizer.AddHeadersCustomizer(headers));
}
if (status != null) {
result.add(new StreamingResponseCustomizer.StatusCustomizer(status));
}
return result;
}
return staticCustomizers;
}
@Override
public void onComplete() {
if (!hadItem) {
StreamingUtil.setHeaders(requestContext, requestContext.serverResponse(), this.determineCustomizers(true));
}
if (json) {
String postfix = onCompleteText();
if (postfix != null) {
byte[] postfixBytes = postfix.getBytes(StandardCharsets.US_ASCII);
requestContext.serverResponse().write(postfixBytes).handle((v, t) -> {
super.onComplete();
return null;
});
} else {
super.onComplete();
}
} else {
super.onComplete();
}
}
protected String onCompleteText() {
if (!encodeAsJsonArray) {
return null;
}
String postfix;
// check if we never sent the open prefix
if (!hadItem) {
postfix = "[]";
} else {
postfix = "]";
}
return postfix;
}
protected String messagePrefix() {
// if it's json, the message prefix starts with `[`.
return json ? nextJsonPrefix : null;
}
protected String messageSuffix() {
return !encodeAsJsonArray ? LINE_SEPARATOR : null;
}
}
static abstract class AbstractMultiSubscriber implements Subscriber
© 2015 - 2024 Weber Informatics LLC | Privacy Policy