001package io.jstach.opt.spring.webflux;
002
003import java.util.List;
004import java.util.Map;
005
006import org.eclipse.jdt.annotation.Nullable;
007import org.springframework.core.io.buffer.DataBuffer;
008import org.springframework.http.MediaType;
009import org.springframework.web.reactive.result.view.AbstractView;
010import org.springframework.web.reactive.result.view.View;
011import org.springframework.web.server.ServerWebExchange;
012
013import io.jstach.jstache.JStache;
014import io.jstach.jstachio.JStachio;
015import reactor.core.publisher.Mono;
016
017/**
018 * One way to use JStachio with Spring Webflux is to use this special View that will
019 * delegate to JStachio to render.
020 * <p>
021 * By default this view interface will use the static jstachio singleton which normally is
022 * the correct Spring wired version if configured correctly.
023 *
024 * @author agentgt
025 * @author dsyer
026 *
027 */
028@SuppressWarnings("exports")
029public interface JStachioModelView extends View {
030
031        @Override
032        default Mono<Void> render( //
033                        @Nullable Map<String, ?> model, //
034                        @Nullable MediaType contentType, //
035                        ServerWebExchange exchange) {
036                if (contentType != null) {
037                        exchange.getResponse().getHeaders().setContentType(contentType);
038                }
039                if (contentType != null) {
040                        exchange.getResponse().getHeaders().setContentType(contentType);
041                }
042
043                View view = new AbstractView() {
044
045                        @Override
046                        protected Mono<Void> renderInternal(Map<String, Object> model, @Nullable MediaType contentType,
047                                        ServerWebExchange exchange) {
048                                var response = exchange.getResponse();
049                                return response.writeWith(Mono.fromCallable(() -> {
050                                        var bufferFactory = response.bufferFactory();
051                                        var mediaType = mediaType();
052                                        DataBuffer buffer = JStachioEncoder.encode(jstachio(), model(), bufferFactory, bufferSize(),
053                                                        mediaType.getCharset());
054                                        var headers = response.getHeaders();
055                                        headers.setContentType(mediaType);
056                                        /*
057                                         * For some reason the webflux WebTestClient gets content-length of -1
058                                         * during unit test but when actually deployed the pipeline will
059                                         * implicitly set the content length.
060                                         *
061                                         * So we just go ahead and explicitely set hoping that does not hurt
062                                         * anything.
063                                         */
064                                        int length = buffer.readableByteCount();
065                                        headers.setContentLength(length);
066                                        return buffer;
067                                }));
068                        }
069
070                };
071
072                return view.render(model, contentType, exchange);
073        }
074
075        @Override
076        default List<MediaType> getSupportedMediaTypes() {
077                return List.of(mediaType());
078        }
079
080        /**
081         * Returns the jstachio singleton by default.
082         * @return stachio singleton by default.
083         * @see JStachio#setStatic(java.util.function.Supplier)
084         */
085        default JStachio jstachio() {
086                return JStachio.of();
087        }
088
089        /**
090         * The model to be rendered by {@link #jstachio()}.
091         * @return model defaulting to <code>this</code> instance.
092         */
093        default Object model() {
094                return this;
095        }
096
097        /**
098         * The initial size of the buffer allocated to be used for rendering.
099         * @return buffer size the default is 4K.
100         */
101        default int bufferSize() {
102                return JStachioEncoder.DEFAULT_BUFFER_SIZE;
103        }
104
105        /**
106         * The default media type for the view.
107         * @return media type the default is "<code>text/html; charset=UTF-8</code>"
108         */
109        default MediaType mediaType() {
110                return JStachioEncoder.DEFAULT_MEDIA_TYPE;
111        }
112
113        /**
114         * Creates a spring view from a model
115         * @param model an instance of a class annotated with {@link JStache}.
116         * @return view ready for rendering
117         */
118        public static JStachioModelView of(Object model) {
119                /*
120                 * TODO in theory we could resolve media type here by finding the template right
121                 * away.
122                 */
123                return new JStachioModelView() {
124                        @Override
125                        public Object model() {
126                                return model;
127                        }
128                };
129        }
130
131}