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

io.inverno.mod.http.server.internal.GenericResponseBody Maven / Gradle / Ivy

There is a newer version: 1.11.0
Show newest version
/*
 * 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 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; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy