io.inverno.mod.http.server.internal.GenericResponseBody Maven / Gradle / Ivy
Show all versions of inverno-http-server Show documentation
/*
* Copyright 2020 Jeremy KUHN
*
* 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 io.inverno.mod.http.server.internal;
import io.inverno.mod.base.Charsets;
import io.inverno.mod.base.resource.MediaTypes;
import io.inverno.mod.http.base.InternalServerErrorException;
import io.inverno.mod.http.base.NotFoundException;
import io.inverno.mod.http.base.OutboundData;
import io.inverno.mod.http.base.header.Headers;
import io.inverno.mod.http.server.ResponseBody;
import io.inverno.mod.http.server.ResponseBody.Sse.Event;
import io.inverno.mod.http.server.ResponseBody.Sse.EventFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpConstants;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
/**
*
* Generic {@link ResponseBody} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
public class GenericResponseBody implements ResponseBody {
private static final String SSE_CONTENT_TYPE = MediaTypes.TEXT_EVENT_STREAM + ";charset=utf-8";
protected final AbstractResponse response;
protected OutboundData rawData;
protected OutboundData stringData;
protected ResponseBody.Resource resourceData;
protected ResponseBody.Sse, ResponseBody.Sse.EventFactory>> sseData;
protected ResponseBody.Sse, ResponseBody.Sse.EventFactory>> sseStringData;
private MonoSink> dataEmitter;
private Publisher data;
private boolean subscribed;
private boolean dataSet;
private boolean single;
private Function, Publisher> transformer;
/**
*
* Creates a response body for the specified response.
*
*
* @param response the response
*/
public GenericResponseBody(AbstractResponse response) {
this.response = response;
}
/**
*
* Sets the response payload data.
*
*
* @param data the payload data publisher
*
* @throws IllegalStateException if response data have already been set
*/
protected final void setData(Publisher data) {
if(this.subscribed && this.dataSet) {
throw new IllegalStateException("Response data already posted");
}
Publisher transformedData = this.transformer != null ? this.transformer.apply(data) : data;
if(transformedData instanceof Mono) {
this.single = true;
}
if(this.dataEmitter != null) {
this.dataEmitter.success(transformedData);
}
else {
this.data = transformedData;
}
this.dataSet = true;
}
/**
*
* Subscribes to the response payload data publisher, creates a switchable publisher if unset.
*
*
* @param s the Subscriber that will consume signals from this Publisher
*/
public void dataSubscribe(Subscriber super ByteBuf> s) {
// No need to synchronize this code since we are in an EventLoop
if(this.subscribed) {
throw new IllegalStateException("Response data already subscribed");
}
if(this.data == null) {
this.data = Flux.switchOnNext(Mono.>create(emitter -> this.dataEmitter = emitter));
}
Flux.from(this.data).doOnDiscard(ByteBuf.class, ByteBuf::release).subscribe(s);
this.subscribed = true;
}
/**
*
* Returns true if the response payload is composed of a single chunk of data.
*
*
* @return true if the response payload is single, false otherwise
*/
public boolean isSingle() {
return this.single;
}
@Override
public ResponseBody transform(Function, Publisher> transformer) throws IllegalArgumentException {
if(this.subscribed && this.dataSet) {
throw new IllegalStateException("Response data already consumed");
}
if(this.transformer == null) {
this.transformer = transformer;
}
else {
this.transformer = this.transformer.andThen(transformer);
}
if(this.dataSet) {
this.data = transformer.apply(this.data);
}
return this;
}
@Override
public void empty() {
this.setData(Mono.empty());
}
@Override
public OutboundData raw() {
if(this.rawData == null) {
this.rawData = new RawOutboundData();
}
return this.rawData;
}
@Override
@SuppressWarnings("unchecked")
public OutboundData string() {
if(this.stringData == null) {
this.stringData = new StringOutboundData();
}
return (OutboundData) this.stringData;
}
@Override
public ResponseBody.Resource resource() {
if(this.resourceData == null) {
this.resourceData = new GenericResponseBodyResourceData();
}
return this.resourceData;
}
@Override
public ResponseBody.Sse, ResponseBody.Sse.EventFactory>> sse() {
if(this.sseData == null) {
this.sseData = new SseRawOutboundData();
}
return this.sseData;
}
// This result in a compilation warning for unchecked conversion
// This is not ideal but it actually does the job: we want to be able to set any type that extends CharSequence
// This seems to be ok all the way since GenericEvent eventually accepts CharSequence stream and value.
@Override
public ResponseBody.Sse, EventFactory>> sseString() {
if(this.sseStringData == null) {
this.sseStringData = new SseStringOutboundData();
}
return this.sseStringData;
}
/**
*
* Generic raw {@link OutboundData} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected class RawOutboundData implements OutboundData {
@SuppressWarnings("unchecked")
@Override
public void stream(Publisher data) {
GenericResponseBody.this.setData((Publisher) data);
}
}
/**
*
* Generic string {@link OutboundData} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected class StringOutboundData implements OutboundData {
@Override
public void stream(Publisher value) throws IllegalStateException {
Publisher data;
if(value instanceof Mono) {
data = ((Mono)value).map(chunk -> Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(chunk, Charsets.DEFAULT)));
}
else {
data = Flux.from(value).map(chunk -> Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(chunk, Charsets.DEFAULT)));
}
GenericResponseBody.this.setData(data);
}
@Override
public void value(T value) throws IllegalStateException {
GenericResponseBody.this.setData(value != null ? Mono.just(Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(value, Charsets.DEFAULT))) : Mono.empty());
}
}
/**
*
* Generic {@link ResponseBody.Resource} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected class GenericResponseBodyResourceData implements ResponseBody.Resource {
/**
*
* Tries to determine resource content type in which case sets the content type
* header.
*
*
* @param resource the resource
*/
protected void populateHeaders(io.inverno.mod.base.resource.Resource resource) {
GenericResponseBody.this.response.headers(h -> {
if(GenericResponseBody.this.response.headers().getContentLength() == null) {
resource.size().ifPresent(h::contentLength);
}
if(GenericResponseBody.this.response.headers().getCharSequence(Headers.NAME_CONTENT_TYPE) == null) {
String mediaType = resource.getMediaType();
if(mediaType != null) {
h.contentType(mediaType);
}
}
if(GenericResponseBody.this.response.headers().getCharSequence(Headers.NAME_LAST_MODIFIED) == null) {
resource.lastModified().ifPresent(lastModified -> {
h.set(Headers.NAME_LAST_MODIFIED, Headers.FORMATTER_RFC_5322_DATE_TIME.format(lastModified.toInstant()));
});
}
});
}
@Override
public void value(io.inverno.mod.base.resource.Resource resource) {
Objects.requireNonNull(resource);
// In case of file resources we should always be able to determine existence
// For other resources with a null exists we can still try, worst case scenario:
// internal server error
if(resource.exists().orElse(true)) {
this.populateHeaders(resource);
GenericResponseBody.this.setData(resource.read().orElseThrow(() -> new InternalServerErrorException("Resource is not readable: " + resource.getURI())));
}
else {
throw new NotFoundException();
}
}
}
/**
*
* Generic raw {@link ResponseBody.Sse} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected class SseRawOutboundData implements ResponseBody.Sse, ResponseBody.Sse.EventFactory>> {
@Override
public void from(BiConsumer>, OutboundData>> data) {
data.accept(this::create, this::stream);
}
/**
*
* Raw server-sent events producer.
*
*
* @param the server-sent event type
* @param value the server-sent events publisher
*/
protected > void stream(Publisher value) {
GenericResponseBody.this.response.headers(headers -> headers
.contentType(GenericResponseBody.SSE_CONTENT_TYPE)
);
GenericResponseBody.this.setData(Flux.from(value)
.cast(GenericEvent.class)
.flatMapSequential(sse -> {
StringBuilder sseMetaData = new StringBuilder();
if(sse.getId() != null) {
sseMetaData.append("id:").append(sse.getId()).append("\n");
}
if(sse.getEvent() != null) {
sseMetaData.append("event:").append(sse.getEvent()).append("\n");
}
if(sse.getComment() != null) {
sseMetaData.append(":").append(sse.getComment().replaceAll("\\r\\n|\\r|\\n", "\r\n:")).append("\n");
}
if(sse.getData() != null) {
sseMetaData.append("data:");
}
Flux sseData = Flux.just(Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(sseMetaData, Charsets.UTF_8)));
if(sse.getData() != null) {
sseData = sseData
.concatWith(Flux.from(sse.getData())
.map(chunk -> {
ByteBuf escapedChunk = Unpooled.unreleasableBuffer(Unpooled.buffer(chunk.readableBytes(), Integer.MAX_VALUE));
while(chunk.isReadable()) {
byte nextByte = chunk.readByte();
if(nextByte == HttpConstants.CR) {
if(chunk.getByte(chunk.readerIndex()) == HttpConstants.LF) {
chunk.readByte();
}
escapedChunk.writeCharSequence("\r\ndata:", Charsets.UTF_8);
}
else if(nextByte == HttpConstants.LF) {
escapedChunk.writeCharSequence("\r\ndata:", Charsets.UTF_8);
}
else {
escapedChunk.writeByte(nextByte);
}
}
return escapedChunk;
})
);
}
sseData = sseData.concatWith(Mono.just(Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("\r\n\r\n", Charsets.UTF_8))));
return sseData;
}));
}
/**
*
* Raw server-sent events factory.
*
*
* @param configurer a raw server-sent event configurer
*
* @return a new raw server-sent event
*/
protected ResponseBody.Sse.Event create(Consumer> configurer) {
GenericEvent sse = new GenericEvent();
configurer.accept(sse);
return sse;
}
/**
*
* Generic raw {@link ResponseBody.Sse.Event} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected final class GenericEvent implements ResponseBody.Sse.Event {
private String id;
private String comment;
private String event;
private Publisher data;
@SuppressWarnings("unchecked")
@Override
public void stream(Publisher data) {
this.data = (Publisher) data;
}
@Override
public void value(T data) {
this.data = Mono.just(data);
}
@Override
public GenericEvent id(String id) {
this.id = id;
return this;
}
@Override
public GenericEvent comment(String comment) {
this.comment = comment;
return this;
}
@Override
public GenericEvent event(String event) {
this.event = event;
return this;
}
public String getId() {
return id;
}
public String getComment() {
return comment;
}
public String getEvent() {
return event;
}
public Publisher getData() {
return data;
}
}
}
/**
*
* Generic string {@link ResponseBody.Sse} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected class SseStringOutboundData implements ResponseBody.Sse, ResponseBody.Sse.EventFactory>> {
@Override
public void from(BiConsumer>, OutboundData>> data) {
data.accept(this::create, this::stream);
}
/**
*
* String server-sent events producer.
*
*
* @param the server-sent event type
* @param value the server-sent events publisher
*/
protected > void stream(Publisher value) {
GenericResponseBody.this.response.headers(headers -> headers
.contentType(GenericResponseBody.SSE_CONTENT_TYPE)
);
GenericResponseBody.this.setData(Flux.from(value)
.cast(GenericEvent.class)
.flatMapSequential(sse -> {
StringBuilder sseMetaData = new StringBuilder();
if(sse.getId() != null) {
sseMetaData.append("id:").append(sse.getId()).append("\n");
}
if(sse.getEvent() != null) {
sseMetaData.append("event:").append(sse.getEvent()).append("\n");
}
if(sse.getComment() != null) {
sseMetaData.append(":").append(sse.getComment().replaceAll("\\r\\n|\\r|\\n", "\r\n:")).append("\n");
}
if(sse.getData() != null) {
sseMetaData.append("data:");
}
Flux sseData = Flux.just(Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(sseMetaData, Charsets.UTF_8)));
if(sse.getData() != null) {
sseData = sseData
.concatWith(Flux.from(sse.getData())
.map(chunk -> {
ByteBuf escapedChunk = Unpooled.unreleasableBuffer(Unpooled.buffer(chunk.length(), Integer.MAX_VALUE));
for(int i=0;i
* String server-sent events factory.
*
*
* @param configurer a string server-sent event configurer
*
* @return a new string server-sent event
*/
protected ResponseBody.Sse.Event create(Consumer> configurer) {
GenericEvent sse = new GenericEvent();
configurer.accept(sse);
return sse;
}
/**
*
* Generic string {@link ResponseBody.Sse.Event} implementation.
*
*
* @author Jeremy Kuhn
* @since 1.0
*/
protected final class GenericEvent implements ResponseBody.Sse.Event {
private String id;
private String comment;
private String event;
private Publisher data;
@SuppressWarnings("unchecked")
@Override
public void stream(Publisher data) {
this.data = (Publisher) data;
}
@Override
public void value(T data) {
this.data = Mono.justOrEmpty(data);
}
@Override
public GenericEvent id(String id) {
this.id = id;
return this;
}
@Override
public GenericEvent comment(String comment) {
this.comment = comment;
return this;
}
@Override
public GenericEvent event(String event) {
this.event = event;
return this;
}
public String getId() {
return id;
}
public String getComment() {
return comment;
}
public String getEvent() {
return event;
}
public Publisher getData() {
return data;
}
}
}
}