001package io.jstach.jstachio.spi;
002
003import java.io.IOException;
004import java.nio.charset.Charset;
005import java.util.ArrayList;
006import java.util.Comparator;
007import java.util.List;
008import java.util.function.Function;
009
010import org.eclipse.jdt.annotation.Nullable;
011
012import io.jstach.jstachio.JStachio;
013import io.jstach.jstachio.Output;
014import io.jstach.jstachio.Template;
015import io.jstach.jstachio.TemplateInfo;
016
017/**
018 * Advises, intercepts or filters a template before being rendered.
019 * <p>
020 * This extension point is largely to support dynamic updates of templates where a
021 * template is being edited while the JVM is fully loaded and we need to intercept the
022 * call to provide rendering of the updated template.
023 * <p>
024 * The extension will only be executed if {@link JStachio} render (and execute) methods
025 * are used and not the generated classes render methods.
026 * <p>
027 * When JStachio renders a model through the runtime it:
028 * <ol>
029 * <li>Loads the template. In some cases it may use reflection and thus
030 * {@link TemplateInfo} may not be a generated {@link Template}.</li>
031 * <li>Loads the composite filter which is all the filters combined in order (see
032 * {@link #order()}).</li>
033 * <li>{@linkplain FilterChain#of(JStachioFilter, TemplateInfo) Creates the filter chain}
034 * with the loaded template.</li>
035 * <li>Then tells the chain to {@linkplain FilterChain#process(Object, Output) process}
036 * the rendering.</li>
037 * </ol>
038 *
039 * @apiNote <strong class="warn"> &#x26A0; WARNING! While this extension point is public
040 * API it is recommended you do not use it.</strong> It is less stable than the rest of
041 * the API and is subject to change in the future. Implementations should be threadsafe!
042 * @author agentgt
043 *
044 */
045public non-sealed interface JStachioFilter extends JStachioExtension {
046
047        /**
048         * A fully composed chain that renders a model by applying filtering.
049         *
050         * @apiNote The filter chain should be stateless and threadsafe as there is no
051         * guarantee that a filter chain will be recreated for the same {@link TemplateInfo}.
052         * @author agentgt
053         *
054         */
055        public interface FilterChain {
056
057                /**
058                 * Renders the passed in model.
059                 * @param model a model assumed never to be <code>null</code>.
060                 * @param appendable the appendable to write to.
061                 * @throws IOException if there is an error writing to the appendable
062                 */
063                public void process(Object model, Output<?> appendable) throws Exception;
064
065                /**
066                 * A marker method that the filter is broken and should not be used. This mainly
067                 * for the filter pipeline to determine if filter should be called.
068                 * @param model the model that would be rendered
069                 * @return by default false
070                 */
071                default boolean isBroken(Object model) {
072                        return false;
073                }
074
075                /**
076                 * Converts the filter chain into a template if it is not already one.
077                 * @param chain process will be used when the template is executed.
078                 * @param templateInfo template info to use if the filter chain is not a template.
079                 * @return chain as a template
080                 */
081                public static Template<?> toTemplate(FilterChain chain, TemplateInfo templateInfo) {
082                        if (chain instanceof Template<?> template) {
083                                return template;
084                        }
085                        return new Template<Object>() {
086
087                                @Override
088                                public String templateName() {
089                                        return templateInfo.templateName();
090                                }
091
092                                @Override
093                                public String templatePath() {
094                                        return templateInfo.templatePath();
095                                }
096
097                                @Override
098                                public Class<?> templateContentType() {
099                                        return templateInfo.templateContentType();
100                                }
101
102                                @Override
103                                public Charset templateCharset() {
104                                        return templateInfo.templateCharset();
105                                }
106
107                                @Override
108                                public String templateMediaType() {
109                                        return templateInfo.templateMediaType();
110                                }
111
112                                @Override
113                                public Function<String, String> templateEscaper() {
114                                        return templateInfo.templateEscaper();
115                                }
116
117                                @Override
118                                public Function<@Nullable Object, String> templateFormatter() {
119                                        return templateInfo.templateFormatter();
120                                }
121
122                                @Override
123                                public boolean supportsType(Class<?> type) {
124                                        return templateInfo.supportsType(type);
125                                }
126
127                                @Override
128                                public <A extends Output<E>, E extends Exception> A execute(Object model, A appendable) throws E {
129                                        try {
130                                                chain.process(model, appendable);
131                                                return appendable;
132                                        }
133                                        catch (Exception e) {
134                                                Templates.sneakyThrow(e);
135                                                throw new RuntimeException(e);
136                                        }
137                                }
138
139                                @Override
140                                public Class<?> modelClass() {
141                                        return templateInfo.modelClass();
142                                }
143
144                        };
145                }
146
147                /**
148                 * Create the filter chain from the filter and a template by resolving the first
149                 * filter.
150                 * <p>
151                 * The first filter (previous) will be broken unless the parameter template is a
152                 * {@link FilterChain} which generated renderers usually are.
153                 * @param filter usually the composite filter
154                 * @param template info about the template
155                 * @return an advised render function or often the previous render function if no
156                 * advise is needed.
157                 */
158                @SuppressWarnings("unchecked")
159                public static FilterChain of( //
160                                JStachioFilter filter, TemplateInfo template) {
161                        FilterChain previous = BrokenFilter.INSTANCE;
162                        if (template instanceof FilterChain c) {
163                                previous = c;
164                        }
165                        else if (template instanceof Template t) {
166                                /*
167                                 * This is sort of abusing that filter chains happen to be a functional
168                                 * interface
169                                 */
170                                previous = (model, appendable) -> {
171                                        t.execute(model, appendable);
172                                };
173                        }
174                        return filter.filter(template, previous);
175                }
176
177        }
178
179        /**
180         * Advises or filters a previously created filter.
181         * @param template info about the template
182         * @param previous the function returned early in the chain.
183         * @return an advised render function or often the previous render function if no
184         * advise is needed.
185         */
186        FilterChain filter( //
187                        TemplateInfo template, //
188                        FilterChain previous);
189
190        /**
191         * Hint on order of filter chain. The found {@link JStachioFilter}s are sorted
192         * naturally (lower number comes first) based on the returned number. Thus the filter
193         * that has the greatest say is the filter with the highest number.
194         * @return default returns zero
195         */
196        default int order() {
197                return 0;
198        }
199
200        /**
201         * Creates a composite filter of a many filters.
202         * @param filters not null.
203         * @return a composite filter ordered by {@link JStachioFilter#order()}
204         */
205        public static JStachioFilter compose(Iterable<JStachioFilter> filters) {
206                List<JStachioFilter> fs = new ArrayList<>();
207                for (var f : filters) {
208                        fs.add(f);
209                }
210                if (fs.isEmpty()) {
211                        return NoFilter.NO_FILTER;
212                }
213                if (fs.size() == 1) {
214                        return fs.get(0);
215                }
216                fs.sort(Comparator.comparingInt(JStachioFilter::order));
217                return new CompositeFilterChain(List.copyOf(fs));
218        }
219
220}
221
222/**
223 * Thrown if process is called on a broken filter. Currently this is a private API.
224 *
225 * @author agentgt
226 */
227class BrokenFilterException extends RuntimeException {
228
229        private static final long serialVersionUID = -1206760388422768739L;
230
231        /**
232         * Invoked if filter is brken
233         * @param s message
234         */
235        public BrokenFilterException(String s) {
236                super(s);
237        }
238
239}
240
241enum BrokenFilter implements io.jstach.jstachio.spi.JStachioFilter.FilterChain {
242
243        INSTANCE;
244
245        @Override
246        public void process(Object model, Output<?> appendable) {
247                throw new BrokenFilterException("Unable to process model: " + model.getClass().getName()
248                                + " probably because a template could not be found.");
249        }
250
251        @Override
252        public boolean isBroken(Object model) {
253                return true;
254        }
255
256}
257
258enum NoFilter implements JStachioFilter {
259
260        NO_FILTER;
261
262        @Override
263        public FilterChain filter(TemplateInfo template, FilterChain previous) {
264                return previous;
265        }
266
267}
268
269class CompositeFilterChain implements JStachioFilter {
270
271        private final List<JStachioFilter> filters;
272
273        public CompositeFilterChain(List<JStachioFilter> filters) {
274                super();
275                this.filters = filters;
276        }
277
278        @Override
279        public FilterChain filter(TemplateInfo template, FilterChain previous) {
280                var current = previous;
281                for (var f : filters) {
282                        current = f.filter(template, current);
283                }
284                return current;
285        }
286
287}