001package io.jstach.opt.spring.webflux; 002 003import java.nio.charset.Charset; 004import java.nio.charset.StandardCharsets; 005import java.util.List; 006import java.util.Map; 007 008import org.eclipse.jdt.annotation.Nullable; 009import org.springframework.core.ResolvableType; 010import org.springframework.core.codec.AbstractSingleValueEncoder; 011import org.springframework.core.codec.Hints; 012import org.springframework.core.io.buffer.DataBuffer; 013import org.springframework.core.io.buffer.DataBufferFactory; 014import org.springframework.http.MediaType; 015import org.springframework.util.MimeType; 016import org.springframework.web.server.NotAcceptableStatusException; 017 018import io.jstach.jstachio.JStachio; 019import io.jstach.jstachio.Template; 020import reactor.core.publisher.Flux; 021 022/** 023 * Encodes a JStachio model into a bytes to be used as output from a webflux reactive 024 * controller. 025 * 026 * @author agentgt 027 * @author dsyer 028 */ 029@SuppressWarnings("exports") 030public class JStachioEncoder extends AbstractSingleValueEncoder<Object> { 031 032 private final JStachio jstachio; 033 034 private final int allocateBufferSize; 035 036 private final MediaType mediaType; 037 038 /** 039 * TODO make public on minor release 040 */ 041 static final int DEFAULT_BUFFER_SIZE = 4 * 1024; 042 043 /** 044 * The default media type is "<code>text/html; charset=UTF-8</code>". 045 */ 046 static final MediaType DEFAULT_MEDIA_TYPE = new MediaType(MediaType.TEXT_HTML, StandardCharsets.UTF_8); 047 048 /** 049 * Create the encoder from a JStachio 050 * @param jstachio not <code>null</code>. 051 */ 052 public JStachioEncoder(JStachio jstachio) { 053 this(jstachio, DEFAULT_BUFFER_SIZE); 054 } 055 056 /** 057 * Create the encoder from a JStachio 058 * @param jstachio not <code>null</code>. 059 * @param allocateBufferSize how much to initially allocate from the buffer factory 060 */ 061 public JStachioEncoder(JStachio jstachio, int allocateBufferSize) { 062 this(jstachio, allocateBufferSize, DEFAULT_MEDIA_TYPE); 063 } 064 065 /* 066 * TODO possibly make public on minor release 067 */ 068 JStachioEncoder(JStachio jstachio, int allocateBufferSize, MediaType mediaType) { 069 super(mediaType); 070 this.jstachio = jstachio; 071 this.allocateBufferSize = allocateBufferSize; 072 this.mediaType = mediaType; 073 } 074 075 @Override 076 public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { 077 Class<?> clazz = elementType.toClass(); 078 /* 079 * WE MUST BLOCK all other encoders regardless of mimetype if it is a jstache 080 * model otherwise we could serialize a model that has sensitive data as JSON. 081 */ 082 return clazz != Object.class && !CharSequence.class.isAssignableFrom(clazz) && jstachio.supportsType(clazz); 083 } 084 085 @Override 086 protected Flux<DataBuffer> encode(Object event, DataBufferFactory bufferFactory, ResolvableType type, 087 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 088 return Flux.just(encodeValue(event, bufferFactory, type, mimeType, hints)); 089 } 090 091 @Override 092 public DataBuffer encodeValue(Object event, DataBufferFactory bufferFactory, ResolvableType valueType, 093 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 094 if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { 095 String logPrefix = Hints.getLogPrefix(hints); 096 logger.debug(logPrefix + "Writing [" + event + "]"); 097 } 098 099 /* 100 * We check the media type to see if it matches otherwise Spring will default to 101 * application/json and will send Content-Type application/json back but return 102 * our almost always NOT JSON body (jstachio should obviously not be used for 103 * generating json). 104 * 105 * See #176 106 */ 107 if (mimeType != null && !mimeType.isCompatibleWith(this.mediaType)) { 108 /* 109 * Returning 406 is not ideal for HTML responses 110 * 111 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 112 * 113 * > In practice, this error is very rarely used. Instead of responding using 114 * this > error code, which would be cryptic for the end user and difficult to 115 * fix, > servers ignore the relevant header and serve an actual page to the 116 * user. It > is assumed that even if the user won't be completely happy, they 117 * will prefer > this to an error code. 118 */ 119 throw new NotAcceptableStatusException(List.of(this.mediaType)); 120 } 121 122 return encode(jstachio, event, bufferFactory, allocateBufferSize, this.mediaType.getCharset()); 123 124 } 125 126 static DataBuffer encode( // 127 JStachio jstachio, // 128 Object event, // 129 DataBufferFactory bufferFactory, // 130 int bufferSize, // 131 @Nullable Charset charset) { 132 Template<Object> template; 133 try { 134 template = jstachio.findTemplate(event); 135 } 136 catch (Exception e) { 137 throw new RuntimeException(e); 138 } 139 if (charset == null) { 140 charset = template.templateCharset(); 141 } 142 143 DataBufferOutput output = new DataBufferOutput(bufferFactory.allocateBuffer(bufferSize), charset); 144 145 return template.write(event, output).getBuffer(); 146 } 147 148}