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}