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}