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"> ⚠ 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}