001package io.jstach.jstachio.spi; 002 003import static java.util.Objects.requireNonNull; 004 005import java.lang.System.Logger; 006import java.lang.System.Logger.Level; 007import java.lang.annotation.Annotation; 008import java.lang.reflect.AnnotatedElement; 009import java.lang.reflect.Constructor; 010import java.nio.charset.Charset; 011import java.nio.charset.StandardCharsets; 012import java.nio.charset.UnsupportedCharsetException; 013import java.util.Arrays; 014import java.util.EnumSet; 015import java.util.List; 016import java.util.Map; 017import java.util.Map.Entry; 018import java.util.NoSuchElementException; 019import java.util.Objects; 020import java.util.Optional; 021import java.util.ServiceConfigurationError; 022import java.util.ServiceLoader; 023import java.util.Set; 024import java.util.Spliterators.AbstractSpliterator; 025import java.util.function.Consumer; 026import java.util.function.Function; 027import java.util.stream.Stream; 028import java.util.stream.StreamSupport; 029 030import org.eclipse.jdt.annotation.Nullable; 031 032import io.jstach.jstache.JStache; 033import io.jstach.jstache.JStacheConfig; 034import io.jstach.jstache.JStacheContentType; 035import io.jstach.jstache.JStacheContentType.UnspecifiedContentType; 036import io.jstach.jstache.JStacheFormatter; 037import io.jstach.jstache.JStacheFormatter.UnspecifiedFormatter; 038import io.jstach.jstache.JStacheName; 039import io.jstach.jstache.JStachePath; 040import io.jstach.jstachio.JStachio; 041import io.jstach.jstachio.Output.EncodedOutput; 042import io.jstach.jstachio.Template; 043import io.jstach.jstachio.TemplateConfig; 044import io.jstach.jstachio.TemplateInfo; 045import io.jstach.jstachio.escapers.Html; 046import io.jstach.jstachio.escapers.PlainText; 047import io.jstach.jstachio.formatters.DefaultFormatter; 048import io.jstach.jstachio.formatters.SpecFormatter; 049import io.jstach.jstachio.spi.Templates.TemplateInfos.SimpleTemplateInfo; 050 051/** 052 * 053 * Locates generated templates by their model via reflection. 054 * <p> 055 * This utility class is useful if you plan on implementing your own {@link JStachio} and 056 * or other integrations. 057 * 058 * @apiNote In order to use reflection in a modular setup one must <code>open</code> 059 * packages to the {@link io.jstach.jstachio/ } module. 060 * @author agentgt 061 * 062 */ 063public final class Templates { 064 065 private Templates() { 066 } 067 068 /** 069 * A utility method that will check if the templates encoding matches the outputs 070 * encoding. 071 * @param template template charset to check 072 * @param output an encoded output expecting the template charset to be the same. 073 * @throws UnsupportedCharsetException if the charsets do not match 074 */ 075 public static void validateEncoding(TemplateInfo template, EncodedOutput<?> output) { 076 if (!template.templateCharset().equals(output.charset())) { 077 throw new UnsupportedCharsetException( 078 "The encoding of the template does not match the output. template charset=" 079 + template.templateCharset() + ", output charset=" + output.charset()); 080 } 081 } 082 083 /** 084 * Finds a {@link Template} if possible otherwise falling back to a 085 * {@link TemplateInfo} based on annotation metadata. A call first resolves the type 086 * that is actually annotated with JStache and then effectively calls 087 * {@link #getTemplate(Class)} first and if that fails possibly tries 088 * {@link #getInfoByReflection(Class)} based on config. 089 * @apiNote Callers can do an <code>instanceof Template t</code> to see if a generated 090 * template was returned instead of the fallback. 091 * @param modelType the models class (<em>the one annotated with {@link JStache} and 092 * not the Templates class</em>) 093 * @param config config used to determine whether or not to fallback 094 * @return the template info which might be a {@link Template} if the generated 095 * template was found. 096 * @throws Exception if any reflection error happes or the template is not found 097 */ 098 public static TemplateInfo findTemplate(Class<?> modelType, JStachioConfig config) throws Exception { 099 Logger logger = config.getLogger(Templates.class.getName()); 100 return findTemplate(modelType, config, logger); 101 } 102 103 /** 104 * Finds a {@link Template} if possible otherwise falling back to a 105 * {@link TemplateInfo} based on annotation metadata. A call first resolves the type 106 * that is actually annotated with JStache and then effectively calls 107 * {@link #getTemplate(Class)} first and if that fails possibly tries 108 * {@link #getInfoByReflection(Class)} based on config. Unlike 109 * {@link #findTemplate(Class, JStachioConfig)} this call will not produce any logging 110 * and will not throw an exception if it fails. 111 * @apiNote Callers can do an <code>instanceof Template t</code> to see if a generated 112 * template was returned instead of the fallback. 113 * @param modelType the models class (<em>the one annotated with {@link JStache} and 114 * not the Templates class</em>) 115 * @param config config used to determine whether or not to fallback 116 * @return the template info which might be a {@link Template} if the generated 117 * template was found or <code>null</code> if not found. 118 */ 119 public static @Nullable TemplateInfo findTemplateOrNull(Class<?> modelType, JStachioConfig config) { 120 if (isIgnoredType(modelType)) { 121 return null; 122 } 123 try { 124 return findTemplate(modelType, config, JStachioConfig.noopLogger()); 125 } 126 catch (Exception e) { 127 return null; 128 } 129 } 130 131 /** 132 * Checks to see if the model type is a type that is always ignored for faster 133 * {@link JStachioTemplateFinder#supportsType(Class)} checking. 134 * 135 * TODO possible candidate for public on minor release 136 * @param modelType the jstache annotated type 137 * @return <code>true</code> if the type should be ignored. 138 */ 139 static boolean isIgnoredType(Class<?> modelType) { 140 /* 141 * TODO JMH as this method will be called quite frequently 142 */ 143 if (modelType == String.class || modelType == Map.class || modelType == Object.class 144 || modelType.isPrimitive()) { 145 return true; 146 } 147 if (modelType.getName().startsWith("java.")) { 148 return true; 149 } 150 return false; 151 } 152 153 /** 154 * Finds the closest JStache annotation on this class or parent classes (super and 155 * interfaces). 156 * 157 * TODO possible candidate to make public on minor release 158 * @param modelType the model type to search on. 159 * @return a tuple of found class annotated and the jstache annotation. 160 */ 161 static Entry<Class<?>, JStache> findJStache(final Class<?> modelType) { 162 var jstache = findJStacheOrNull(modelType); 163 if (jstache == null) { 164 throw new TemplateNotFoundException("JStache annotation was not found on type or parents.", modelType); 165 } 166 return jstache; 167 } 168 169 /** 170 * Finds the closest JStache annotation on this class or parent classes (super and 171 * interfaces). 172 * 173 * TODO possible candidate to make public on minor release 174 * @param modelType the model type to search on. 175 * @return a tuple of found class annotated and the jstache annotation. 176 */ 177 static @Nullable Entry<Class<?>, JStache> findJStacheOrNull(final Class<?> modelType) { 178 if (isIgnoredType(modelType)) 179 return null; 180 return possibleJStacheTypes(modelType) // 181 .flatMap(_c -> Stream.ofNullable(_c.getDeclaredAnnotation(JStache.class)) // 182 .map(a -> Map.<Class<?>, JStache>entry(_c, a))) // 183 .findFirst() // 184 .orElse(null); 185 } 186 187 private static Stream<Class<?>> possibleJStacheTypes(Class<?> modelType) { 188 if (isIgnoredType(modelType)) { 189 return Stream.empty(); 190 } 191 return Stream.concat(parents(modelType), interfaces(modelType)) // 192 .filter(_c -> !isIgnoredType(_c)); 193 } 194 195 static TemplateInfo findTemplate(Class<?> modelType, JStachioConfig config, Logger logger) throws Exception { 196 197 EnumSet<TemplateLoadStrategy> strategies = EnumSet.noneOf(TemplateLoadStrategy.class); 198 199 for (var s : ALL_STRATEGIES) { 200 if (s.isEnabled(config)) { 201 strategies.add(s); 202 } 203 } 204 205 var classLoaders = collectClassLoaders(modelType.getClassLoader()); 206 Exception error; 207 if (config.getBoolean(JStachioConfig.REFLECTION_TEMPLATE_DISABLE)) { 208 /* 209 * Here we walk up the ancestor tree but do not call getDeclaredAnnotation as 210 * that is borderline reflection. 211 * 212 * It is not so much that it is reflection but calling get*Annotation on any 213 * class will instantiate most of the annotations and if someone is looking to 214 * disable reflection they probably want that as well. 215 * 216 * This is probably more expensive computational wise as the ServiceLoader is 217 * repeatedly instantiated up the hieararchy but more often the class passed 218 * is the one annotated. TODO cache serviceloader. 219 */ 220 var resolvedTypes = possibleJStacheTypes(modelType).toList(); 221 @Nullable 222 Exception firstError = null; 223 for (var resolvedType : resolvedTypes) { 224 try { 225 return Templates.getTemplate(resolvedType, strategies, classLoaders, logger); 226 } 227 catch (Exception e) { 228 if (firstError == null) { 229 firstError = e; 230 } 231 } 232 } 233 error = firstError != null ? firstError : new TemplateNotFoundException(modelType, strategies); 234 } 235 else { 236 /* 237 * Since reflection is allowed we go looking for the annotation to find the 238 * JStache model. 239 */ 240 var resolvedType = findJStache(modelType).getKey(); 241 try { 242 return Templates.getTemplate(resolvedType, strategies, classLoaders, logger); 243 } 244 catch (Exception e) { 245 error = e; 246 } 247 if (logger.isLoggable(Level.WARNING)) { 248 String message = String 249 .format("Could not find generated template and will try reflection for model type:" 250 + "'%s', annotated type: '%s'", modelType, resolvedType); 251 logger.log(Level.WARNING, message, error); 252 } 253 return getInfoByReflection(resolvedType); 254 } 255 throw error; 256 } 257 258 /** 259 * Finds template info by accessing JStache annotations through reflective lookup. 260 * <p> 261 * This allows you to lookup template meta data <strong>regardless of whether or not 262 * the annotation processor has generated code</strong>. This method is mainly used 263 * for fallback mechanisms and extensions. 264 * <p> 265 * Why might you need the reflective data instead of the static generated meta data? 266 * Well often times the annotation processor in a hot reload environment such as 267 * JRebel, JBoss modules, or Spring Reload has not generated the code from a JStache 268 * model and or it is not desired. This allows reflection based engines like JMustache 269 * to keep working even if code is not generated. 270 * @param modelType the class that is annotated with {@link JStache} 271 * @return template info meta data 272 * @throws Exception if any reflection error happes or the template is not found 273 */ 274 public static TemplateInfo getInfoByReflection(Class<?> modelType) throws Exception { 275 return TemplateInfos.templateOf(modelType); 276 } 277 278 /** 279 * Finds a template by reflection or an exception is thrown. 280 * @param <T> the model type 281 * @param clazz the model type 282 * @return the template never <code>null</code>. 283 * @throws NoSuchElementException if the template is not found 284 * @throws Exception if the template is not found or any reflective access errors 285 */ 286 public static <T> Template<T> getTemplate(Class<T> clazz) throws Exception { 287 List<ClassLoader> classLoaders = collectClassLoaders(clazz.getClassLoader()); 288 return getTemplate(clazz, ALL_STRATEGIES, classLoaders, JStachioConfig.noopLogger()); 289 } 290 291 /** 292 * Finds a template by reflection or an exception is thrown. <em>Because the return 293 * template is parameterized a template matching the exact type is returned and 294 * inheritance either via interfaces or super class is not checked!</em> 295 * @param <T> the model type 296 * @param modelType the model type 297 * @param strategies load strategies 298 * @param classLoaders class loaders to try loading 299 * @param logger used to log attempted strategies. If you do not want to log use 300 * {@link JStachioConfig#noopLogger()}. 301 * @return the template never <code>null</code>. 302 * @throws NoSuchElementException if the template is not found 303 * @throws Exception if the template is not found or any reflective access errors 304 * 305 */ 306 public static <T> Template<T> getTemplate(Class<T> modelType, Iterable<TemplateLoadStrategy> strategies, 307 Iterable<ClassLoader> classLoaders, System.Logger logger) throws Exception { 308 for (TemplateLoadStrategy s : strategies) { 309 if (logger.isLoggable(Level.DEBUG)) { 310 logger.log(Level.DEBUG, "For modelType: \"" + modelType + "\" trying strategy: \"" + s + "\""); 311 } 312 for (ClassLoader classLoader : classLoaders) { 313 try { 314 Template<T> template = s.load(modelType, classLoader, logger); 315 if (template != null) { 316 return template; 317 } 318 } 319 catch (ClassNotFoundException | TemplateNotFoundException e) { 320 continue; 321 } 322 } 323 } 324 throw new TemplateNotFoundException(modelType, StreamSupport.stream(strategies.spliterator(), false).toList()); 325 } 326 327 static boolean isReflectionTemplate(TemplateInfo template) { 328 if (template instanceof SimpleTemplateInfo) { 329 return true; 330 } 331 return false; 332 } 333 334 private static final Set<TemplateLoadStrategy> ALL_STRATEGIES = EnumSet.allOf(TemplateLoadStrategy.class); 335 336 /** 337 * Strategy to load templates dynamically. <em>These strategies expect the exact type 338 * and not a super type!</em> 339 * 340 * @author agentgt 341 * 342 */ 343 public enum TemplateLoadStrategy { 344 345 /** 346 * Strategy that will try the {@link ServiceLoader} with the SPI of 347 * {@link TemplateProvider}. 348 */ 349 SERVICE_LOADER() { 350 @SuppressWarnings("unchecked") 351 @Override 352 protected <T> @Nullable Template<T> load(Class<T> clazz, ClassLoader classLoader, System.Logger logger) 353 throws Exception { 354 return (Template<T>) templateByServiceLoader(clazz, classLoader, logger); 355 } 356 357 @Override 358 protected final boolean isEnabled(JStachioConfig config) { 359 return !config.getBoolean(JStachioConfig.SERVICELOADER_TEMPLATE_DISABLE); 360 } 361 }, 362 /** 363 * Strategy that will try no-arg constructor 364 */ 365 CONSTRUCTOR() { 366 @Override 367 protected <T> @Nullable Template<T> load(Class<T> clazz, ClassLoader classLoader, System.Logger logger) 368 throws Exception { 369 return templateByConstructor(clazz, classLoader); 370 } 371 372 @Override 373 protected final boolean isEnabled(JStachioConfig config) { 374 return !config.getBoolean(JStachioConfig.REFLECTION_TEMPLATE_DISABLE); 375 } 376 377 }; 378 379 /** 380 * Load the template by this strategy. 381 * @param <T> model type. 382 * @param clazz model type. 383 * @param classLoader classload which may more may not be used. 384 * @param logger used to log reflection warnings or other errors. 385 * @return loaded template 386 * @throws Exception if an error happens while trying to load template. 387 */ 388 protected abstract <T> @Nullable Template<T> load(Class<T> clazz, ClassLoader classLoader, System.Logger logger) 389 throws Exception; 390 391 /** 392 * Determine if the strategy is enabled. 393 * @param config key value config 394 * @return true if not disabled. 395 */ 396 protected abstract boolean isEnabled(JStachioConfig config); 397 398 } 399 400 @SuppressWarnings("unchecked") 401 private static <T> @Nullable Template<T> templateByConstructor(Class<T> clazz, ClassLoader classLoader) 402 throws Exception { 403 Class<?> implementation = classLoader.loadClass(generatedClassName(clazz)); 404 Constructor<?> constructor = implementation.getDeclaredConstructor(); 405 constructor.setAccessible(true); 406 return (Template<T>) constructor.newInstance(); 407 } 408 409 /** 410 * Gets the canonical class name of the generated template code regardless of whether 411 * or not code has actually been generated. Because the class may not have been 412 * generated the return is a String. 413 * @param modelClass the exact model class that contains the {@link JStache} 414 * annotation. 415 * @return the FQN class name of the would be generated template code 416 * @throws NoSuchElementException if the model class is not annotated with 417 * {@link JStache}. 418 */ 419 public static String generatedClassName(Class<?> modelClass) { 420 // TODO perhaps this information should be on TemplateInfo? 421 var a = modelClass.getAnnotation(JStache.class); 422 if (a == null) { 423 throw new TemplateNotFoundException(modelClass); 424 } 425 String cname; 426 if (a.name().isBlank()) { 427 428 // @SuppressWarnings("null") // Eclipse bug with annotation arrays 429 JStacheName name = findAnnotations(modelClass, JStacheConfig.class) // 430 .flatMap(config -> Arrays.stream(config.naming())).findFirst().orElse(null); 431 432 String prefix = name == null ? JStacheName.UNSPECIFIED : name.prefix(); 433 434 String suffix = name == null ? JStacheName.UNSPECIFIED : name.suffix(); 435 436 prefix = prefix.equals(JStacheName.UNSPECIFIED) ? JStacheName.DEFAULT_PREFIX : prefix; 437 suffix = suffix.equals(JStacheName.UNSPECIFIED) ? JStacheName.DEFAULT_SUFFIX : suffix; 438 439 cname = prefix + modelClass.getSimpleName() + suffix; 440 } 441 else { 442 cname = a.name(); 443 } 444 String packageName = modelClass.getPackageName(); 445 String fqn = packageName + (packageName.isEmpty() ? "" : ".") + cname; 446 return fqn; 447 } 448 449 private static Charset resolveCharset(Class<?> c) { 450 String charset = findAnnotations(c, JStacheConfig.class).map(config -> config.charset()) 451 .filter(cs -> !cs.isEmpty()).findFirst().orElse(null); 452 if (charset == null) { 453 return StandardCharsets.UTF_8; 454 } 455 return Charset.forName(charset); 456 } 457 458 static <A extends Annotation> Stream<A> findAnnotations(Class<?> c, Class<A> annotationClass) { 459 var s = annotationElements(c); 460 return s.filter(p -> p != null).flatMap(p -> Stream.ofNullable(p.getAnnotation(annotationClass))); 461 } 462 463 private static Stream<? extends AnnotatedElement> annotationElements(Class<?> c) { 464 Stream<? extends AnnotatedElement> enclosing = enclosing(c).flatMap(Templates::expandUsing); 465 return Stream.<Stream<? extends AnnotatedElement>>builder() // 466 .add(enclosing) // 467 .add(Stream.ofNullable(c.getPackage())) // 468 .add(Stream.ofNullable(c.getModule())) // 469 .build() // 470 .flatMap(Function.identity()); 471 } 472 473 /* 474 * This is to get referenced config of JStacheConfig.using 475 */ 476 private static Stream<Class<?>> expandUsing(Class<?> e) { 477 478 JStacheConfig config = e.getAnnotation(JStacheConfig.class); 479 if (config == null) { 480 return Stream.of(e); 481 } 482 var using = config.using(); 483 if (!using.equals(void.class)) { 484 return Stream.of(e, using); 485 } 486 return Stream.of(e); 487 } 488 489 private static Stream<Class<?>> enclosing(Class<?> e) { 490 return findClasses(e, Class::getEnclosingClass); 491 } 492 493 private static Stream<Class<?>> parents(Class<?> e) { 494 return findClasses(e, Class::getSuperclass); 495 } 496 497 private static Stream<Class<?>> interfaces(Class<?> clazz) { 498 return Stream.concat(Stream.of(clazz.getInterfaces()), 499 Stream.of(clazz.getInterfaces()).flatMap(interfaceClass -> interfaces(interfaceClass))); 500 } 501 502 private static Stream<Class<?>> findClasses(Class<?> e, Function<Class<?>, @Nullable Class<?>> f) { 503 AbstractSpliterator<Class<?>> split = new AbstractSpliterator<Class<?>>(Long.MAX_VALUE, 0) { 504 @Nullable 505 Class<?> current = e; 506 507 @Override 508 public boolean tryAdvance(Consumer<? super Class<?>> action) { 509 Class<?> c; 510 if ((c = current) == null) { 511 return false; 512 } 513 current = f.apply(c); 514 action.accept(c); 515 return true; 516 } 517 }; 518 return StreamSupport.stream(split, false); 519 } 520 521 private static <T> @Nullable Template<?> templateByServiceLoader(Class<T> clazz, ClassLoader classLoader, 522 System.Logger logger) { 523 ServiceLoader<TemplateProvider> loader = ServiceLoader.load(TemplateProvider.class, classLoader); 524 return findTemplates(loader, TemplateConfig.empty(), e -> { 525 logger.log(Level.ERROR, "Template provider failed to load. Skipping it.", e); 526 }).filter(t -> clazz.equals(t.modelClass())).findFirst().orElse(null); 527 } 528 529 /** 530 * Find templates by the given service loader. 531 * @param serviceLoader a prepared service loader 532 * @param templateConfig template config to use for instantiating templates 533 * @param errorHandler handle {@link ServiceConfigurationError} errors. 534 * @return lazy stream of templates 535 */ 536 public static Stream<Template<?>> findTemplates( // 537 ServiceLoader<TemplateProvider> serviceLoader, // 538 TemplateConfig templateConfig, // 539 Consumer<ServiceConfigurationError> errorHandler) { 540 return serviceLoader.stream().flatMap(p -> { 541 try { 542 return p.get().provideTemplates().stream(); 543 } 544 catch (ServiceConfigurationError e) { 545 errorHandler.accept(e); 546 } 547 return Stream.empty(); 548 }); 549 } 550 551 private static List<ClassLoader> collectClassLoaders(@Nullable ClassLoader classLoader) { 552 return Stream.<@Nullable ClassLoader>builder().add(classLoader) 553 .add(Thread.currentThread().getContextClassLoader()).add(Template.class.getClassLoader()).build() 554 .<ClassLoader>flatMap(s -> Stream.ofNullable(s)).toList(); 555 } 556 557 @SuppressWarnings("unchecked") 558 static <E extends Throwable> void sneakyThrow(final Throwable x) throws E { 559 throw (E) x; 560 } 561 562 /** 563 * Resolve path lookup information reflectively from a model class by doing config 564 * resolution at runtime. 565 * @param model a class annotated with JStache 566 * @return the resolved path annotation 567 * @apiNote This method is an implementation detail for reflection rendering engines 568 * such as JMustache and JStachio's future reflection based engine. It is recommended 569 * you do not rely on it as it is subject to change in the future. 570 * @deprecated use {@link #getInfoByReflection(Class)} or {@link #getPathInfo(Class)}. 571 */ 572 @Deprecated 573 public static @Nullable JStachePath resolvePath(Class<?> model) { 574 return _resolvePath(model); 575 } 576 577 /** 578 * <strong>INTERNAL (use at your own risk):</strong> Resolved {@link JStachePath} 579 * config by replacing {@link JStachePath#UNSPECIFIED} with default values. The passed 580 * in model class does not need be directly annotated with {@link JStache} and can be 581 * subclass. 582 * @param modelClass the model's class 583 * @return never <code>null</code> path information. 584 * @apiNote <strong>INTERNAL (use at your own risk):</strong> This method largely 585 * exists because {@link TemplateInfo} does not expose the original prefix and suffix 586 * used. The prefix and suffix are needed for reloading the templates for extensions. 587 * If you use this method please let the developers know and for what. 588 */ 589 public static PathInfo getPathInfo(Class<?> modelClass) { 590 return DefaultPathInfo.of(modelClass, _resolvePath(modelClass)); 591 } 592 593 private static @Nullable JStachePath _resolvePath(Class<?> model) { 594 // TODO perhaps this information should be on TemplateInfo? 595 return annotationElements(model) // 596 .flatMap(e -> TemplateInfos.resolvePathOnElement(e).stream()) // 597 .findFirst() // 598 .orElse(null); 599 } 600 601 /** 602 * Resolved {@link JStachePath} config by replacing {@link JStachePath#UNSPECIFIED} 603 * with default values. 604 * 605 * @apiNote This interface largely exists because {@link TemplateInfo} does not expose 606 * the original prefix and suffix used. 607 * @author agentgt 608 */ 609 public sealed interface PathInfo { 610 611 /** 612 * Resolved prefix. 613 * @return prefix maybe empty but will never be {@link JStachePath#UNSPECIFIED}. 614 */ 615 String prefix(); 616 617 /** 618 * Resolved suffix. 619 * @return suffix maybe empty but will never be {@link JStachePath#UNSPECIFIED}. 620 */ 621 String suffix(); 622 623 /** 624 * Calculates the expanded path and if the supplied path is empty then the path 625 * will be expanded based on the class name where package name are separated with 626 * a slash ("{@code /}") instead of a "<code>.</code>" and is suffixed with 627 * {@value JStachePath#AUTO_SUFFIX} if suffix is {@link JStachePath#UNSPECIFIED}. 628 * @param path if the path is empty it will be calculated based on the modelClass 629 * package name and class name. 630 * @return fully resolved path with prefix and suffix. 631 */ 632 public String resolveFullPath(String path); 633 634 } 635 636 record DefaultPathInfo(Class<?> modelClass, String prefix, String suffix, // 637 boolean prefixUnspecified, boolean suffixUnspecified // 638 ) implements PathInfo { 639 640 static DefaultPathInfo of(Class<?> modelClass, @Nullable JStachePath path) { 641 String prefix, suffix; 642 boolean prefixUnspecified, suffixUnspecified; 643 if (path == null) { 644 prefix = JStachePath.DEFAULT_PREFIX; 645 suffix = JStachePath.DEFAULT_SUFFIX; 646 prefixUnspecified = suffixUnspecified = true; 647 } 648 else { 649 prefix = path.prefix(); 650 suffix = path.suffix(); 651 prefixUnspecified = JStachePath.UNSPECIFIED.equals(prefix); 652 suffixUnspecified = JStachePath.UNSPECIFIED.equals(suffix); 653 prefix = prefixUnspecified ? JStachePath.DEFAULT_PREFIX : prefix; 654 suffix = suffixUnspecified ? JStachePath.DEFAULT_SUFFIX : suffix; 655 } 656 return new DefaultPathInfo(modelClass, requireNonNull(prefix), requireNonNull(suffix), prefixUnspecified, 657 suffixUnspecified); 658 } 659 660 @Override 661 public String resolveFullPath(String path) { 662 String resolvePath; 663 String prefix = prefix(); 664 String suffix = suffix(); 665 if (path.isEmpty()) { 666 resolvePath = resolveDefaultPath(modelClass); 667 if (suffixUnspecified()) { 668 suffix = JStachePath.AUTO_SUFFIX; 669 } 670 } 671 else { 672 resolvePath = path; 673 } 674 return prefix + resolvePath + suffix; 675 676 } 677 678 private static String resolveDefaultPath(Class<?> model) { 679 String resolvedPath; 680 String folder = model.getPackageName().replace('.', '/'); 681 folder = folder.isEmpty() ? folder : folder + "/"; 682 resolvedPath = folder + model.getSimpleName(); 683 return resolvedPath; 684 } 685 } 686 687 static class TemplateInfos { 688 689 public static TemplateInfo templateOf(Class<?> model) throws Exception { 690 JStache stache = model.getAnnotation(JStache.class); 691 if (stache == null) { 692 throw new IllegalArgumentException( 693 "Model class is not annotated with " + JStache.class.getSimpleName() + ". class: " + model); 694 } 695 696 String templateString = stache.template(); 697 698 final String templateName = generatedClassName(model); 699 String templatePath; 700 if (!templateString.isEmpty()) { 701 templatePath = ""; 702 } 703 else { 704 PathInfo pathInfo = getPathInfo(model); 705 templatePath = pathInfo.resolveFullPath(requireNonNull(stache.path())); 706 } 707 708 var ee = EscaperProvider.INSTANCE.providesFromModelType(model, stache); 709 Function<String, String> templateEscaper = ee.getValue(); 710 Class<?> templateContentType = ee.getKey(); 711 String templateMediaType = ""; 712 var jstacheContentType = templateContentType.getAnnotation(JStacheContentType.class); 713 if (jstacheContentType != null) { 714 templateMediaType = Objects.requireNonNull(jstacheContentType.mediaType()); 715 } 716 Function<@Nullable Object, String> templateFormatter = FormatterProvider.INSTANCE 717 .providesFromModelType(model, stache).getValue(); 718 719 Charset templateCharset = resolveCharset(model); 720 721 long lastLoaded = System.currentTimeMillis(); 722 return new SimpleTemplateInfo( // 723 templateName, // 724 templatePath, // 725 templateCharset, // 726 templateMediaType, // 727 templateString, // 728 templateContentType, // 729 templateEscaper, // 730 templateFormatter, // 731 lastLoaded, // 732 model); 733 734 } 735 736 private static Optional<JStachePath> resolvePathOnElement(AnnotatedElement a) { 737 var path = a.getAnnotation(JStachePath.class); 738 if (path != null) { 739 return Optional.of(path); 740 } 741 var config = a.getAnnotation(JStacheConfig.class); 742 if (config != null && config.pathing().length > 0) { 743 return Optional.ofNullable(config.pathing()[0]); 744 } 745 return Optional.empty(); 746 } 747 748 sealed interface StaticProvider<P> { 749 750 private @Nullable Class<?> autoToNull(@Nullable Class<?> type) { 751 if ((type == null) || type.equals(autoProvider())) { 752 return null; 753 } 754 return type; 755 } 756 757 default Class<?> nullToDefault(@Nullable Class<?> type) { 758 Class<?> c = autoToNull(type); 759 if (c == null) { 760 return defaultProvider(); 761 } 762 return c; 763 } 764 765 String providesMethod(Class<?> type); 766 767 Class<?> autoProvider(); 768 769 Class<?> defaultProvider(); 770 771 @Nullable 772 Class<?> providerFromJStache(JStache jstache); 773 774 Class<?> providerFromConfig(JStacheConfig config); 775 776 default Class<?> findProvider(Class<?> modelType, JStache jstache) { 777 @Nullable 778 Class<?> provider = autoToNull(providerFromJStache(jstache)); 779 if (provider != null) { 780 return provider; 781 } 782 provider = findAnnotations(modelType, JStacheConfig.class) 783 .flatMap(config -> Optional.ofNullable(autoToNull(providerFromConfig(config))).stream()) 784 .findFirst().orElse(null); 785 return nullToDefault(provider); 786 } 787 788 default Entry<Class<?>, P> providesFromModelType(Class<?> modelType, JStache jstache) throws Exception { 789 var t = findProvider(modelType, jstache); 790 return Map.entry(t, Objects.requireNonNull(provides(t))); 791 } 792 793 @SuppressWarnings("unchecked") 794 default P provides(Class<?> type) throws Exception { 795 String provides = providesMethod(type); 796 var method = type.getMethod(provides); 797 Object r = method.invoke(provides); 798 return (P) r; 799 } 800 801 } 802 803 enum EscaperProvider implements StaticProvider<Function<String, String>> { 804 805 INSTANCE; 806 807 @Override 808 public Class<?> autoProvider() { 809 return UnspecifiedContentType.class; 810 } 811 812 @Override 813 public Class<?> defaultProvider() { 814 return Html.class; 815 } 816 817 @Override 818 public @Nullable Class<?> providerFromJStache(JStache jstache) { 819 return null; 820 } 821 822 @Override 823 public Class<?> providerFromConfig(JStacheConfig config) { 824 return Objects.requireNonNull(config.contentType()); 825 } 826 827 @Override 828 public String providesMethod(Class<?> type) { 829 JStacheContentType a = type.getAnnotation(JStacheContentType.class); 830 if (a == null) { 831 throw new IllegalArgumentException("Specified content type class is not annotated with @" 832 + JStacheContentType.class.getSimpleName()); 833 } 834 return Objects.requireNonNull(a.providesMethod()); 835 } 836 837 @Override 838 public Function<String, String> provides(@Nullable Class<?> contentType) throws Exception { 839 contentType = nullToDefault(contentType); 840 if (contentType.equals(Html.class)) { 841 return Html.provider(); 842 } 843 else if (contentType.equals(PlainText.class)) { 844 return PlainText.provider(); 845 } 846 return StaticProvider.super.provides(contentType); 847 } 848 849 } 850 851 enum FormatterProvider implements StaticProvider<Function<@Nullable Object, String>> { 852 853 INSTANCE; 854 855 @Override 856 public Class<?> autoProvider() { 857 return UnspecifiedFormatter.class; 858 } 859 860 @Override 861 public Class<?> defaultProvider() { 862 return DefaultFormatter.class; 863 } 864 865 @Override 866 public @Nullable Class<?> providerFromJStache(JStache jstache) { 867 return null; 868 } 869 870 @Override 871 public Class<?> providerFromConfig(JStacheConfig config) { 872 return Objects.requireNonNull(config.formatter()); 873 } 874 875 @Override 876 public String providesMethod(Class<?> type) { 877 JStacheFormatter a = type.getAnnotation(JStacheFormatter.class); 878 if (a == null) { 879 throw new IllegalArgumentException("Specified formatter provider is not annotated with @" 880 + JStacheFormatter.class.getSimpleName()); 881 } 882 return Objects.requireNonNull(a.providesMethod()); 883 } 884 885 @Override 886 public Function<@Nullable Object, String> provides(@Nullable Class<?> formatterType) throws Exception { 887 formatterType = nullToDefault(formatterType); 888 if (formatterType.equals(DefaultFormatter.class)) { 889 return DefaultFormatter.provider(); 890 } 891 else if (formatterType.equals(SpecFormatter.class)) { 892 return SpecFormatter.provider(); 893 } 894 return StaticProvider.super.provides(formatterType); 895 } 896 897 } 898 899 record SimpleTemplateInfo( // 900 String templateName, // 901 String templatePath, // 902 Charset templateCharset, // 903 String templateMediaType, // 904 String templateString, // 905 Class<?> templateContentType, // 906 Function<String, String> templateEscaper, // 907 Function<@Nullable Object, String> templateFormatter, // 908 long lastLoaded, // 909 Class<?> modelClass) implements TemplateInfo { 910 911 @Override 912 public boolean supportsType(Class<?> type) { 913 return modelClass().isAssignableFrom(type); 914 } 915 916 @Override 917 public Function<String, String> templateEscaper() { 918 return this.templateEscaper; 919 } 920 921 @Override 922 public Function<@Nullable Object, String> templateFormatter() { 923 return this.templateFormatter; 924 } 925 926 } 927 928 } 929 930}