001package io.jstach.jstachio.spi;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Comparator;
006import java.util.List;
007
008import io.jstach.jstachio.Template;
009import io.jstach.jstachio.TemplateInfo;
010
011/**
012 * Advises or filters a previously applied template.
013 *
014 * @apiNote Implementations should be threadsafe!
015 * @author agentgt
016 *
017 */
018public non-sealed interface JStachioFilter extends JStachioExtension {
019
020        /**
021         * A fully composed chain that renders a model by applying filtering.
022         *
023         * @apiNote The filter chain should be stateless and threadsafe as there is no
024         * guarantee that a filter chain will be recreated for the same {@link TemplateInfo}.
025         * @author agentgt
026         *
027         */
028        public interface FilterChain {
029
030                /**
031                 * Renders the passed in model.
032                 * @param model a model assumed never to be <code>null</code>.
033                 * @param appendable the appendable to write to.
034                 * @throws IOException if there is an error writing to the appendable
035                 */
036                public void process(Object model, Appendable appendable) throws IOException;
037
038                /**
039                 * A marker method that the filter is broken and should not be used. This mainly
040                 * for the filter pipeline to determine if filter should be called.
041                 * @param model the model that would be rendered
042                 * @return by default false
043                 */
044                default boolean isBroken(Object model) {
045                        return false;
046                }
047
048        }
049
050        /**
051         * Advises or filters a previously created filter.
052         * @param template info about the template
053         * @param previous the function returned early in the chain.
054         * @return an advised render function or often the previous render function if no
055         * advise is needed.
056         */
057        FilterChain filter( //
058                        TemplateInfo template, //
059                        FilterChain previous);
060
061        /**
062         * Applies filter with previous filter broken unless the parameter template is a
063         * {@link FilterChain} or is a{@link Template} which generated renderers usually are.
064         * @param template info about the template
065         * @return an advised render function or often the previous render function if no
066         * advise is needed.
067         */
068        @SuppressWarnings("unchecked")
069        default FilterChain filter( //
070                        TemplateInfo template) {
071                FilterChain previous = BrokenFilter.INSTANCE;
072                if (template instanceof FilterChain c) {
073                        previous = c;
074                }
075                else if (template instanceof @SuppressWarnings("rawtypes") Template t) {
076                        /*
077                         * This is sort of abusing that filter chains happen to be a functional
078                         * interface
079                         */
080                        previous = (model, appendable) -> {
081                                t.execute(model, appendable);
082                        };
083                }
084                return filter(template, previous);
085        }
086
087        /**
088         * Hint on order of filter chain. The found {@link JStachioFilter}s are sorted
089         * naturally (lower number comes first) based on the returned number. Thus the filter
090         * that has the greatest say is the filter with the highest number.
091         * @return default returns zero
092         */
093        default int order() {
094                return 0;
095        }
096
097        /**
098         * Creates a composite filter of a many filters.
099         * @param filters not null.
100         * @return a composite filter ordered by {@link JStachioFilter#order()}
101         */
102        public static JStachioFilter compose(Iterable<JStachioFilter> filters) {
103                List<JStachioFilter> fs = new ArrayList<>();
104                for (var f : filters) {
105                        fs.add(f);
106                }
107                fs.sort(Comparator.comparingInt(JStachioFilter::order));
108                return new CompositeFilterChain(List.copyOf(fs));
109        }
110
111}
112
113enum BrokenFilter implements io.jstach.jstachio.spi.JStachioFilter.FilterChain {
114
115        INSTANCE;
116
117        @Override
118        public void process(Object model, Appendable appendable) throws IOException {
119                throw new IllegalStateException();
120        }
121
122        @Override
123        public boolean isBroken(Object model) {
124                return true;
125        }
126
127}
128
129class CompositeFilterChain implements JStachioFilter {
130
131        private final List<JStachioFilter> filters;
132
133        public CompositeFilterChain(List<JStachioFilter> filters) {
134                super();
135                this.filters = filters;
136        }
137
138        @Override
139        public FilterChain filter(TemplateInfo template, FilterChain previous) {
140                var current = previous;
141                for (var f : filters) {
142                        current = f.filter(template, current);
143                }
144                return current;
145        }
146
147}