001package io.jstach.opt.spring.web; 002 003import java.io.IOException; 004import java.io.OutputStream; 005import java.nio.charset.Charset; 006import java.nio.charset.StandardCharsets; 007 008import org.springframework.http.HttpInputMessage; 009import org.springframework.http.HttpOutputMessage; 010import org.springframework.http.MediaType; 011import org.springframework.http.converter.AbstractHttpMessageConverter; 012import org.springframework.http.converter.HttpMessageNotReadableException; 013import org.springframework.http.converter.HttpMessageNotWritableException; 014import org.springframework.web.bind.annotation.ResponseBody; 015 016import io.jstach.jstachio.JStachio; 017import io.jstach.jstachio.Output.CloseableEncodedOutput; 018import io.jstach.jstachio.output.ByteBufferEncodedOutput; 019import io.jstach.jstachio.output.ChunkEncodedOutput; 020import io.jstach.jstachio.output.LimitEncodedOutput; 021import io.jstach.jstachio.output.ThresholdEncodedOutput; 022 023/** 024 * Type-safe way to use JStachio in Spring Web. 025 * <p> 026 * For this to work the controllers need to return JStache models and have the controller 027 * method return annotated with {@link ResponseBody}. 028 * <p> 029 * <strong>Example:</strong> <pre><code class="language-java"> 030 * @JStache 031 * public record HelloModel(String message){} 032 * 033 * @GetMapping(value = "/") 034 * @ResponseBody 035 * public HelloModel hello() { 036 * return new HelloModel("Spring Boot is now JStachioed!"); 037 * } 038 * </code> </pre> Because JStachio by default pre-encodes the static text parts of the 039 * template the output strategy handles the buffering instead of the framework (usually 040 * servlet) to improve performance and to reliable set <code>Content-Length</code>. This 041 * can be changed by overriding {@link #createOutput(HttpOutputMessage)}. 042 * 043 * @author agentgt 044 * 045 */ 046public class JStachioHttpMessageConverter extends AbstractHttpMessageConverter<Object> { 047 048 /** 049 * The default media type is "<code>text/html; charset=UTF-8</code>". 050 */ 051 @SuppressWarnings("exports") 052 public static final MediaType DEFAULT_MEDIA_TYPE = new MediaType(MediaType.TEXT_HTML, StandardCharsets.UTF_8); 053 054 /** 055 * The default buffer limit before bailing on trying to set 056 * <code>Content-Length</code>. The default is "{@value #DEFAULT_BUFFER_LIMIT}". 057 */ 058 public static final int DEFAULT_BUFFER_LIMIT = 1024 * 64; 059 060 private final JStachio jstachio; 061 062 private final MediaType mediaType; 063 064 /** 065 * The maximum amount of bytes to buffer. 066 */ 067 protected final int bufferLimit; 068 069 /** 070 * Create http converter from jstachio 071 * @param jstachio an instance usually created by spring 072 */ 073 public JStachioHttpMessageConverter(JStachio jstachio) { 074 this(jstachio, DEFAULT_MEDIA_TYPE, DEFAULT_BUFFER_LIMIT); 075 } 076 077 /** 078 * Creates a message converter with media type and buffer limit. 079 * @param jstachio an instance usually created by spring 080 * @param mediaType used to set ContentType 081 * @param bufferLimit buffer limit before bailing on trying to set 082 * <code>Content-Length</code>. 083 */ 084 protected JStachioHttpMessageConverter(JStachio jstachio, MediaType mediaType, int bufferLimit) { 085 super(resolveCharset(mediaType), mediaType, MediaType.ALL); 086 this.jstachio = jstachio; 087 this.mediaType = mediaType; 088 this.bufferLimit = bufferLimit; 089 } 090 091 private static Charset resolveCharset(MediaType mediaType) { 092 var charset = mediaType.getCharset(); 093 if (charset == null) { 094 return StandardCharsets.UTF_8; 095 } 096 return charset; 097 } 098 099 @Override 100 protected boolean supports(Class<?> clazz) { 101 return jstachio.supportsType(clazz); 102 } 103 104 @Override 105 public boolean canRead(Class<?> clazz, @SuppressWarnings("exports") MediaType mediaType) { 106 return jstachio.supportsType(clazz); 107 } 108 109 @Override 110 protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) 111 throws IOException, HttpMessageNotReadableException { 112 throw new HttpMessageNotReadableException("Input not supported by JStachio", inputMessage); 113 114 } 115 116 @Override 117 protected void writeInternal(Object t, HttpOutputMessage outputMessage) 118 throws IOException, HttpMessageNotWritableException { 119 120 /* 121 * We have to override the content type here because if we do not Spring appears 122 * to default to application/json if the Accept does not include HTML. 123 */ 124 var headers = outputMessage.getHeaders(); 125 126 /* 127 * If we just write directly to the body without resolving content length first it 128 * might not get set and we MIGHT get Transfer-Encoding: chunked which is almost 129 * never desired for HTML. 130 */ 131 headers.setContentType(mediaType); 132 try (CloseableEncodedOutput<IOException> output = createOutput(outputMessage)) { 133 jstachio.write(t, output); 134 } 135 } 136 137 /** 138 * Create the buffered output to use when executing JStachio. 139 * @param message response. 140 * @return the output ready for writing to. 141 * @see ByteBufferEncodedOutput 142 * @see ChunkEncodedOutput 143 * @see LimitEncodedOutput 144 */ 145 protected CloseableEncodedOutput<IOException> createOutput(HttpOutputMessage message) { 146 return new HttpOutputMessageEncodedOutput(getDefaultCharset(), message, bufferLimit); 147 } 148 149} 150 151class HttpOutputMessageEncodedOutput extends ThresholdEncodedOutput.OutputStreamThresholdEncodedOutput { 152 153 private final HttpOutputMessage response; 154 155 public HttpOutputMessageEncodedOutput(Charset charset, HttpOutputMessage response, int limit) { 156 super(charset, limit); 157 this.response = response; 158 } 159 160 @Override 161 protected OutputStream createConsumer(int size) throws IOException { 162 if (size > -1) { 163 response.getHeaders().setContentLength(size); 164 } 165 return response.getBody(); 166 } 167 168}