001package io.jstach.opt.spring.web; 002 003import java.io.IOException; 004import java.nio.charset.Charset; 005import java.nio.charset.StandardCharsets; 006import java.util.Objects; 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; 018import io.jstach.jstachio.output.ByteBufferedOutputStream; 019 020/** 021 * Typesafe way to use JStachio in Spring Web. 022 * <p> 023 * For this to work the controllers need to return JStache models and have the controller 024 * method return annotated with {@link ResponseBody}. 025 * <p> 026 * <strong>Example:</strong> <pre><code class="language-java"> 027 * @JStache 028 * public record HelloModel(String message){} 029 * 030 * @GetMapping(value = "/") 031 * @ResponseBody 032 * public HelloModel hello() { 033 * return new HelloModel("Spring Boot is now JStachioed!"); 034 * } 035 * </code> </pre> 036 * 037 * @author agentgt 038 * 039 */ 040public class JStachioHttpMessageConverter extends AbstractHttpMessageConverter<Object> { 041 042 /** 043 * The default media type is "<code>text/html; charset=UTF-8</code>". 044 */ 045 static final MediaType DEFAULT_MEDIA_TYPE = new MediaType(MediaType.TEXT_HTML, StandardCharsets.UTF_8); 046 047 private final JStachio jstachio; 048 049 /** 050 * Create http converter from jstachio 051 * @param jstachio an instance usually created by spring 052 */ 053 public JStachioHttpMessageConverter(JStachio jstachio) { 054 super(StandardCharsets.UTF_8, MediaType.TEXT_HTML, MediaType.ALL); 055 this.jstachio = jstachio; 056 } 057 058 @Override 059 protected boolean supports(Class<?> clazz) { 060 return jstachio.supportsType(clazz); 061 } 062 063 @Override 064 public boolean canRead(Class<?> clazz, @SuppressWarnings("exports") MediaType mediaType) { 065 return jstachio.supportsType(clazz); 066 } 067 068 @Override 069 protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) 070 throws IOException, HttpMessageNotReadableException { 071 throw new HttpMessageNotReadableException("Input not supported by JStachio", inputMessage); 072 073 } 074 075 @Override 076 protected void writeInternal(Object t, HttpOutputMessage outputMessage) 077 throws IOException, HttpMessageNotWritableException { 078 /* 079 * If we just write directly to the body we will get Transfer-Encoding: chunked 080 * which is almost never desired for HTML. 081 * 082 * TODO we should explore making this configurable or other options as this 083 * requires copying all the data. 084 */ 085 try (ByteBufferedOutputStream buffer = new ByteBufferedOutputStream()) { 086 // The try - with is not necessary but keeps linters happy 087 jstachio.write(t, Output.of(buffer, charset())); 088 int size = buffer.size(); 089 var headers = outputMessage.getHeaders(); 090 headers.setContentLength(size); 091 /* 092 * We have to override the content type here because if we do not Spring 093 * appears to default to application/json if the Accept does not include HTML. 094 */ 095 headers.setContentType(DEFAULT_MEDIA_TYPE); 096 /* 097 * buffer.toByteArray copies which we do not want so we use toBuffer which 098 * does not 099 */ 100 var bytes = buffer.toBuffer().array(); 101 var body = outputMessage.getBody(); 102 body.write(bytes, 0, size); 103 } 104 } 105 106 protected Charset charset() { 107 return Objects.requireNonNull(getDefaultCharset()); 108 } 109 110}