001package io.jstach.jstachio.spi; 002 003import java.nio.charset.Charset; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.Comparator; 008import java.util.IdentityHashMap; 009import java.util.List; 010import java.util.NoSuchElementException; 011import java.util.Objects; 012import java.util.Set; 013import java.util.function.Function; 014 015import org.eclipse.jdt.annotation.Nullable; 016 017import io.jstach.jstache.JStache; 018import io.jstach.jstachio.JStachio; 019import io.jstach.jstachio.Template; 020import io.jstach.jstachio.TemplateInfo; 021import io.jstach.jstachio.spi.JStachioTemplateFinder.SimpleTemplateFinder; 022 023/** 024 * Finds templates based on the model type (class). 025 * <p> 026 * The default {@link JStachio} uses a combination of relection and the ServiceLoader to 027 * find templates. 028 * <p> 029 * Other implementations may want to use their DI framework like Spring or CDI to find 030 * templates. 031 * 032 * @author agentgt 033 * 034 */ 035public non-sealed interface JStachioTemplateFinder extends JStachioExtension { 036 037 /** 038 * Finds a {@link Template} if possible otherwise possibly falling back to a 039 * {@link TemplateInfo} based on annotation metadata or some other mechanism. 040 * 041 * @apiNote Callers can do an <code>instanceof Template t</code> to see if a generated 042 * template was returned instead of the fallback {@link TemplateInfo} metadata. 043 * @param modelType the models class (<em>the one annotated with {@link JStache} and 044 * not the Templates class</em>) 045 * @return the template info which might be a {@link Template} if the generated 046 * template was found. 047 * @throws Exception if any reflection or runtime error happens 048 * @throws NoSuchElementException if the template is not found and there were no other 049 * errors 050 * @throws NullPointerException if the modelType is null 051 */ 052 public TemplateInfo findTemplate(Class<?> modelType) throws Exception; 053 054 /** 055 * Finds a template or null if no template is found. Should not throw an exception if 056 * a template is not found. 057 * @param modelType the models class (<em>the one annotated with {@link JStache} and 058 * not the Templates class</em>) 059 * @return <code>null</code> if the template is was not found or the template info 060 * which might be a {@link Template} if the generated template was found. 061 * @throws NullPointerException if the modelType is null 062 * @see #findTemplate(Class) 063 */ 064 default @Nullable TemplateInfo findOrNull(Class<?> modelType) { 065 Objects.requireNonNull(modelType, "modelType"); 066 if (Templates.isIgnoredType(modelType)) { 067 return null; 068 } 069 try { 070 return findTemplate(modelType); 071 } 072 catch (Exception e) { 073 return null; 074 } 075 } 076 077 /** 078 * Determines if this template finder has a template for the model type (the class 079 * annotated by JStache). 080 * @param modelType the models class (<em>the one annotated with {@link JStache} and 081 * not the Templates class</em>) 082 * @return true if this finder has template for modelType 083 * @throws NullPointerException if the modelType is null 084 */ 085 default boolean supportsType(Class<?> modelType) { 086 if (Templates.isIgnoredType(modelType)) { 087 return false; 088 } 089 var t = findOrNull(modelType); 090 if (t == null) { 091 return false; 092 } 093 return true; 094 } 095 096 /** 097 * Hint on order of template finders. The found {@link JStachioTemplateFinder}s are 098 * sorted naturally (lower number comes first) based on the returned number. Thus a 099 * template finder with a lower order number that {@link #supportsType(Class)} the 100 * model class will be used. 101 * @return default returns zero 102 */ 103 default int order() { 104 return 0; 105 } 106 107 /** 108 * The default template finder that uses reflection and or the ServiceLoader. 109 * <p> 110 * <em>This implementation performs no caching. If you would like caching call 111 * {@link #cachedTemplateFinder(JStachioTemplateFinder)} on the returned finder.</em> 112 * @param config used to help find templates as well as logging. 113 * @return default template finder. 114 */ 115 public static JStachioTemplateFinder defaultTemplateFinder(JStachioConfig config) { 116 return new DefaultTemplateFinder(config); 117 } 118 119 /** 120 * Decorates a template finder with a cache using {@link ClassValue} with the 121 * modelType as the key. 122 * <p> 123 * <em>The returned finder will only call {@link #findTemplate(Class)} on the passed 124 * in delegate finder to resolve {@link #supportsType(Class)} and 125 * {@link #findOrNull(Class)}! </em> 126 * <p> 127 * While the finder does not provide any eviction the cache will not prevent garbage 128 * collection of the model classes. 129 * @param finder to be decorated unless the finder is already decorated thus it is a 130 * noop to repeateadly call this method on already cached template finder. 131 * @return caching template finder 132 */ 133 public static JStachioTemplateFinder cachedTemplateFinder(JStachioTemplateFinder finder) { 134 if (finder instanceof ClassValueCacheTemplateFinder) { 135 return finder; 136 } 137 return new ClassValueCacheTemplateFinder(finder); 138 } 139 140 /** 141 * Creates a template finder from an iterable of templates. The returned finder will 142 * just loop through the templates and call {@link TemplateInfo#supportsType(Class)}. 143 * To avoid the looping cost wrap the return with 144 * {@link #cachedTemplateFinder(JStachioTemplateFinder)}. 145 * @param templates templates to be searched in order of the iterable 146 * @param order order hint see {@link #order()}. 147 * @return adapted template finder 148 */ 149 public static JStachioTemplateFinder of(Iterable<? extends TemplateInfo> templates, int order) { 150 return new IterableTemplateFinder(templates, order); 151 } 152 153 /** 154 * Creates a composite template finder from a list. If the list only has a single 155 * element then it is returned without being wrapped. 156 * @param templateFinders list of template finders. 157 * @return templateFinder searching in order of {@link #order()} then the list order 158 */ 159 public static JStachioTemplateFinder of(List<? extends JStachioTemplateFinder> templateFinders) { 160 return CompositeTemplateFinder.of(templateFinders); 161 } 162 163 /** 164 * An easier to implement template finder based on a sequence of templates. 165 * 166 * @author agentgt 167 * 168 */ 169 public interface SimpleTemplateFinder extends JStachioTemplateFinder { 170 171 @Override 172 default TemplateInfo findTemplate(Class<?> modelType) throws Exception { 173 Objects.requireNonNull(modelType, "modelType"); 174 var t = findOrNull(modelType); 175 if (t == null) { 176 throw new TemplateNotFoundException(modelType); 177 } 178 return t; 179 } 180 181 @Override 182 default boolean supportsType(Class<?> modelType) { 183 Objects.requireNonNull(modelType, "modelType"); 184 var t = findOrNull(modelType); 185 if (t == null) { 186 return false; 187 } 188 return true; 189 } 190 191 @Override 192 default @Nullable TemplateInfo findOrNull(Class<?> modelType) { 193 var resolvedType = Templates.findJStache(modelType).getKey(); 194 for (var t : templates()) { 195 if (t.supportsType(resolvedType)) { 196 return t; 197 } 198 } 199 return null; 200 } 201 202 /** 203 * Sequence of templates used to find matching template from model. 204 * @return templates 205 */ 206 Iterable<? extends TemplateInfo> templates(); 207 208 } 209 210} 211 212final class DefaultTemplateFinder implements JStachioTemplateFinder { 213 214 private final JStachioConfig config; 215 216 DefaultTemplateFinder(JStachioConfig config) { 217 super(); 218 this.config = config; 219 } 220 221 @Override 222 public TemplateInfo findTemplate(Class<?> modelType) throws Exception { 223 return Templates.findTemplate(modelType, config); 224 } 225 226 @Override 227 public @Nullable TemplateInfo findOrNull(Class<?> modelType) { 228 return Templates.findTemplateOrNull(modelType, config); 229 } 230 231 @Override 232 public int order() { 233 return Integer.MAX_VALUE; 234 } 235 236} 237 238class TemplateNotFoundException extends NoSuchElementException { 239 240 private static final long serialVersionUID = -4016359589653582060L; 241 242 private final Class<?> modelType; 243 244 public TemplateNotFoundException(Class<?> modelType) { 245 super(errorMessage(modelType)); 246 this.modelType = modelType; 247 248 } 249 250 public TemplateNotFoundException(Class<?> modelType, Collection<Templates.TemplateLoadStrategy> strategies) { 251 super(errorMessage(modelType, strategies)); 252 this.modelType = modelType; 253 254 } 255 256 public TemplateNotFoundException(String message, Class<?> modelType) { 257 super(message + " " + errorMessage(modelType)); 258 this.modelType = modelType; 259 260 } 261 262 protected static String errorMessage(Class<?> modelType) { 263 return "Template not found for type: '" + modelType + "'"; 264 } 265 266 protected static String errorMessage(Class<?> modelType, Collection<Templates.TemplateLoadStrategy> strategies) { 267 return errorMessage(modelType) + ", using strategies: " + strategies; 268 } 269 270 public Class<?> modelType() { 271 return modelType; 272 } 273 274} 275 276final class IterableTemplateFinder implements SimpleTemplateFinder { 277 278 private final Iterable<? extends TemplateInfo> templates; 279 280 private final int order; 281 282 public IterableTemplateFinder(Iterable<? extends TemplateInfo> templates, int order) { 283 super(); 284 this.templates = templates; 285 this.order = order; 286 } 287 288 @Override 289 public int order() { 290 return this.order; 291 } 292 293 @Override 294 public Iterable<? extends TemplateInfo> templates() { 295 return templates; 296 } 297 298} 299 300sealed interface MissingTemplateInfo extends TemplateInfo { 301 302 @Override 303 default String templateName() { 304 throw new UnsupportedOperationException(); 305 } 306 307 @Override 308 default String templatePath() { 309 throw new UnsupportedOperationException(); 310 311 } 312 313 @Override 314 default Class<?> templateContentType() { 315 throw new UnsupportedOperationException(); 316 317 } 318 319 @Override 320 default Charset templateCharset() { 321 throw new UnsupportedOperationException(); 322 323 } 324 325 @Override 326 default String templateMediaType() { 327 throw new UnsupportedOperationException(); 328 } 329 330 @Override 331 default Function<String, String> templateEscaper() { 332 throw new UnsupportedOperationException(); 333 334 } 335 336 @Override 337 default Function<@Nullable Object, String> templateFormatter() { 338 throw new UnsupportedOperationException(); 339 340 } 341 342 @Override 343 default boolean supportsType(Class<?> type) { 344 throw new UnsupportedOperationException(); 345 346 } 347 348 @Override 349 default Class<?> modelClass() { 350 throw new UnsupportedOperationException(); 351 352 } 353 354} 355 356record ExceptionTemplateInfo(Exception exception) implements MissingTemplateInfo { 357} 358 359final class ClassValueCacheTemplateFinder implements JStachioTemplateFinder { 360 361 private final ClassValue<TemplateInfo> cache; 362 363 private final JStachioTemplateFinder delegate; 364 365 public ClassValueCacheTemplateFinder(JStachioTemplateFinder delegate) { 366 super(); 367 this.delegate = delegate; 368 this.cache = new ClassValue<>() { 369 370 @Override 371 protected TemplateInfo computeValue(Class<?> type) { 372 try { 373 return delegate.findTemplate(type); 374 } 375 catch (TemplateNotFoundException e) { 376 return new ExceptionTemplateInfo(e); 377 } 378 catch (Exception e) { 379 Templates.sneakyThrow(e); 380 throw new RuntimeException(); 381 } 382 } 383 }; 384 } 385 386 @Override 387 public TemplateInfo findTemplate(Class<?> modelType) throws Exception { 388 Objects.requireNonNull(modelType, "modelType"); 389 /* 390 * TODO JMH whether accessing cache is faster than checking if type is not is on 391 * the ignore list. 392 * 393 * The original idea was to save memory but maybe better to use cache. 394 */ 395 if (Templates.isIgnoredType(modelType)) { 396 throw new TemplateNotFoundException(modelType); 397 } 398 var info = cache.get(modelType); 399 if (info instanceof ExceptionTemplateInfo et) { 400 throw et.exception(); 401 } 402 return Objects.requireNonNull(info); 403 } 404 405 @Override 406 public @Nullable TemplateInfo findOrNull(Class<?> modelType) { 407 Objects.requireNonNull(modelType, "modelType"); 408 /* 409 * TODO JMH whether accessing cache is faster than checking if type is not is on 410 * the ignore list. 411 */ 412 if (Templates.isIgnoredType(modelType)) { 413 return null; 414 } 415 try { 416 var info = cache.get(modelType); 417 if (info instanceof ExceptionTemplateInfo) { 418 return null; 419 } 420 return info; 421 } 422 catch (Exception e) { 423 return null; 424 } 425 } 426 427 @Override 428 public int order() { 429 return delegate.order(); 430 } 431 432} 433 434final class CompositeTemplateFinder implements JStachioTemplateFinder { 435 436 private final Iterable<? extends JStachioTemplateFinder> finders; 437 438 private CompositeTemplateFinder(Iterable<? extends JStachioTemplateFinder> finders) { 439 super(); 440 this.finders = finders; 441 } 442 443 public static JStachioTemplateFinder of(List<? extends JStachioTemplateFinder> finders) { 444 if (finders.size() == 1) { 445 return finders.get(0); 446 } 447 ArrayList<JStachioTemplateFinder> sorted = new ArrayList<>(); 448 sorted.addAll(finders); 449 sorted.sort(Comparator.comparingInt(JStachioTemplateFinder::order)); 450 /* 451 * Flatten and remove duplicates 452 */ 453 ArrayList<JStachioTemplateFinder> flatten = new ArrayList<>(); 454 @SuppressWarnings("null") 455 Set<JStachioTemplateFinder> found = Collections 456 .newSetFromMap(new IdentityHashMap<JStachioTemplateFinder, Boolean>()); 457 for (var f : sorted) { 458 if (f instanceof CompositeTemplateFinder ctf) { 459 for (var sub : ctf.finders) { 460 if (found.add(sub)) { 461 flatten.add(sub); 462 } 463 } 464 } 465 else { 466 if (found.add(f)) { 467 flatten.add(f); 468 } 469 } 470 } 471 472 if (flatten.size() == 1) { 473 return flatten.get(0); 474 } 475 476 return new CompositeTemplateFinder(List.copyOf(flatten)); 477 } 478 479 @Override 480 public TemplateInfo findTemplate(Class<?> modelType) throws Exception { 481 for (var f : finders) { 482 var t = f.findOrNull(modelType); 483 if (t != null) { 484 return t; 485 } 486 } 487 throw new TemplateNotFoundException(modelType); 488 } 489 490 @Override 491 public boolean supportsType(Class<?> modelType) { 492 for (var f : finders) { 493 var b = f.supportsType(modelType); 494 if (b) 495 return true; 496 } 497 return false; 498 } 499 500}