001package io.jstach.opt.spring.webmvc; 002 003import java.io.IOException; 004import java.nio.charset.Charset; 005import java.nio.charset.StandardCharsets; 006import java.util.Map; 007 008import org.springframework.http.MediaType; 009import org.springframework.web.servlet.View; 010import org.springframework.web.servlet.view.RedirectView; 011 012import io.jstach.jstache.JStache; 013import io.jstach.jstache.JStacheInterfaces; 014import io.jstach.jstachio.JStachio; 015import io.jstach.jstachio.Output.CloseableEncodedOutput; 016import io.jstach.jstachio.context.ContextJStachio; 017import io.jstach.jstachio.context.ContextNode; 018import io.jstach.opt.spring.web.JStachioHttpMessageConverter; 019import jakarta.servlet.http.HttpServletRequest; 020import jakarta.servlet.http.HttpServletResponse; 021 022/** 023 * Another way to use JStachio with Spring MVC is to have models implement Springs 024 * {@link View} interface. You can enforce that your models implement this interface with 025 * {@link JStacheInterfaces}. Alternatively one can call {@link #of(Object)} on the model 026 * and return the result. 027 * <p> 028 * This view will by default use the static jstachio singleton and if configured correctly 029 * that will be the spring version. 030 * <p> 031 * This approach has pros and cons. It makes your models slightly coupled to Spring MVC 032 * but allows you to return different views if say you had to redirect on some inputs 033 * ({@link RedirectView}). 034 * 035 * @author agentgt 036 * 037 */ 038public interface JStachioModelView extends View { 039 040 /** 041 * The default media type is "<code>text/html; charset=UTF-8</code>". 042 */ 043 @SuppressWarnings("exports") 044 static final MediaType DEFAULT_MEDIA_TYPE = JStachioHttpMessageConverter.DEFAULT_MEDIA_TYPE; 045 046 /** 047 * The default buffer limit before bailing on trying to set 048 * <code>Content-Length</code>. The default is 049 * "{@value JStachioHttpMessageConverter#DEFAULT_BUFFER_LIMIT}". 050 */ 051 static final int DEFAULT_BUFFER_LIMIT = JStachioHttpMessageConverter.DEFAULT_BUFFER_LIMIT; 052 053 @SuppressWarnings("exports") 054 @Override 055 default void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) 056 throws Exception { 057 058 String contentType = getContentType(); 059 response.setContentType(contentType); 060 Charset charset = getMediaType().getCharset(); 061 if (charset == null) { 062 charset = StandardCharsets.UTF_8; 063 } 064 try (var o = createOutput(charset, response)) { 065 var context = createContext(model, request, response); 066 jstachio().write(model(), context, o); 067 } 068 } 069 070 /* 071 * If you want this public file an issue. 072 */ 073 private ContextNode createContext(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { 074 return ContextNode.of(model::get); 075 } 076 077 /** 078 * Creates the output from the servlet response to use for rendering. 079 * @param charset charset resolved from {@link #getMediaType()} 080 * @param response servlet response 081 * @return output that should be closed when finished 082 */ 083 @SuppressWarnings("exports") 084 default CloseableEncodedOutput<IOException> createOutput(Charset charset, HttpServletResponse response) { 085 return new ServletThresholdEncodedOutput(charset, response, DEFAULT_BUFFER_LIMIT); 086 } 087 088 /** 089 * Returns the jstachio singleton by default. 090 * @return stachio singleton by default. 091 * @see JStachio#setStatic(java.util.function.Supplier) 092 */ 093 default ContextJStachio jstachio() { 094 return ContextJStachio.of(JStachio.of()); 095 } 096 097 /** 098 * The model to be rendered by {@link #jstachio()}. 099 * @return model defaulting to <code>this</code> instance. 100 */ 101 default Object model() { 102 return this; 103 } 104 105 @Override 106 default String getContentType() { 107 return getMediaType().toString(); 108 } 109 110 /** 111 * The media type for this view. The default is 112 * "<code>text/html; charset=UTF-8</code>". 113 * @return the media type 114 */ 115 @SuppressWarnings("exports") 116 default MediaType getMediaType() { 117 return DEFAULT_MEDIA_TYPE; 118 } 119 120 /** 121 * Creates a spring view from a model with content type: 122 * "<code>text/html; charset=UTF-8</code>". 123 * @param model an instance of a class annotated with {@link JStache}. 124 * @return view ready for rendering 125 */ 126 public static JStachioModelView of(Object model) { 127 return of(model, MediaType.TEXT_HTML.toString()); 128 } 129 130 /** 131 * Creates a spring view from a model. 132 * @param model an instance of a class annotated with {@link JStache}. 133 * @param contentType See {@link #getContentType()} 134 * @return view ready for rendering 135 */ 136 public static JStachioModelView of(Object model, String contentType) { 137 MediaType mediaType = MediaType.parseMediaType(contentType); 138 return JStachioModelView.of(model, mediaType); 139 } 140 141 /** 142 * Creates a spring view from a model. 143 * @param model an instance of a class annotated with {@link JStache}. 144 * @param mediaType the mediaType 145 * @return view ready for rendering 146 */ 147 static JStachioModelView of(Object model, @SuppressWarnings("exports") MediaType mediaType) { 148 return of(model, mediaType, JStachio.of()); 149 } 150 151 /** 152 * Creates a spring view from a model. 153 * @param model an instance of a class annotated with {@link JStache}. 154 * @param mediaType the mediaType 155 * @param jstachio jstachio to use. 156 * @return view ready for rendering 157 */ 158 static JStachioModelView of(Object model, @SuppressWarnings("exports") MediaType mediaType, JStachio jstachio) { 159 ContextJStachio contextJStachio = ContextJStachio.of(jstachio); 160 /* 161 * TODO potentially make this public on the next minor version release. 162 */ 163 return new JStachioModelView() { 164 @Override 165 public Object model() { 166 return model; 167 } 168 169 @Override 170 public MediaType getMediaType() { 171 return mediaType; 172 } 173 174 @Override 175 public ContextJStachio jstachio() { 176 return contextJStachio; 177 } 178 }; 179 } 180 181}