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}