001package io.jstach.jstachio.spi; 002 003import java.lang.System.Logger; 004import java.lang.System.Logger.Level; 005import java.lang.annotation.Annotation; 006import java.lang.reflect.AnnotatedElement; 007import java.lang.reflect.Constructor; 008import java.util.List; 009import java.util.Map; 010import java.util.Map.Entry; 011import java.util.ServiceLoader; 012import java.util.Spliterators.AbstractSpliterator; 013import java.util.function.Consumer; 014import java.util.function.Function; 015import java.util.stream.Stream; 016import java.util.stream.StreamSupport; 017 018import org.eclipse.jdt.annotation.NonNull; 019import org.eclipse.jdt.annotation.Nullable; 020 021import io.jstach.jstache.JStache; 022import io.jstach.jstache.JStacheConfig; 023import io.jstach.jstache.JStacheContentType; 024import io.jstach.jstache.JStacheContentType.UnspecifiedContentType; 025import io.jstach.jstache.JStacheFormatter; 026import io.jstach.jstache.JStacheFormatter.UnspecifiedFormatter; 027import io.jstach.jstache.JStacheName; 028import io.jstach.jstache.JStachePath; 029import io.jstach.jstachio.JStachio; 030import io.jstach.jstachio.Template; 031import io.jstach.jstachio.TemplateInfo; 032import io.jstach.jstachio.escapers.Html; 033import io.jstach.jstachio.escapers.PlainText; 034import io.jstach.jstachio.formatters.DefaultFormatter; 035import io.jstach.jstachio.formatters.SpecFormatter; 036 037/** 038 * 039 * Locates generated templates by their model via reflection. 040 * <p> 041 * This utility class is useful if you plan on implementing your own {@link JStachio} and 042 * or other integrations. 043 * 044 * @apiNote In order to use reflection in a modular setup one must <code>open</code> 045 * packages to the {@link io.jstach.jstachio/ } module. 046 * @author agentgt 047 * 048 */ 049public final class Templates { 050 051 private Templates() { 052 } 053 054 /** 055 * Finds a {@link Template} if possible otherwise falling back to a 056 * {@link TemplateInfo} based on annotation metadata. This method is effectively calls 057 * {@link #getTemplate(Class)} first and if that fails possibly tries 058 * {@link #getInfoByReflection(Class)} based on config. 059 * @apiNote Callers can do an <code>instanceof Template t</code> to see if a generated 060 * template was returned instead of the fallback. 061 * @param modelType the models class (<em>the one annotated with {@link JStache} and 062 * not the Templates class</em>) 063 * @param config config used to determine whether or not to fallback 064 * @return the template info which might be a {@link Template} if the generated 065 * template was found. 066 * @throws Exception if any reflection error happes or the template is not found 067 */ 068 public static TemplateInfo findTemplate(Class<?> modelType, JStachioConfig config) throws Exception { 069 Exception error; 070 try { 071 Template<?> r = Templates.getTemplate(modelType); 072 return r; 073 } 074 catch (Exception e) { 075 error = e; 076 } 077 if (!config.getBoolean(JStachioConfig.REFLECTION_TEMPLATE_DISABLE)) { 078 Logger logger = config.getLogger(JStachioExtension.class.getCanonicalName()); 079 if (logger.isLoggable(Level.WARNING)) { 080 logger.log(Level.WARNING, 081 "Could not find generated template and will try reflection for model type: " + modelType, 082 error); 083 } 084 return getInfoByReflection(modelType); 085 086 } 087 throw error; 088 089 } 090 091 /** 092 * Finds template info by accessing JStache annotations through reflective lookup. 093 * <p> 094 * This allows you to lookup template meta data <strong>regardless of whether or not 095 * the annotation processor has generated code</strong>. This method is mainly used 096 * for fallback mechanisms and extensions. 097 * <p> 098 * Why might you need the reflective data instead of the static generated meta data? 099 * Well often times the annotation processor in a hot reload environment such as 100 * JRebel, JBoss modules, or Spring Reload has not generated the code from a JStache 101 * model and or it is not desired. This allows reflection based engines like JMustache 102 * to keep working even if code is not generated. 103 * @param modelType the class that is annotated with {@link JStache} 104 * @return template info meta data 105 * @throws Exception if any reflection error happes or the template is not found 106 */ 107 public static TemplateInfo getInfoByReflection(Class<?> modelType) throws Exception { 108 return TemplateInfos.templateOf(modelType); 109 } 110 111 /** 112 * Finds a template by reflection or an exception is thrown. 113 * @param <T> the model type 114 * @param clazz the model type 115 * @return the template never <code>null</code>. 116 * @throws ClassNotFoundException if the template is not found 117 * @throws Exception if the template is not found or any reflective access errors 118 */ 119 public static <T> Template<T> getTemplate(Class<T> clazz) throws Exception { 120 List<ClassLoader> classLoaders = collectClassLoaders(clazz.getClassLoader()); 121 return (Template<T>) getTemplate(clazz, classLoaders); 122 } 123 124 private static <T> Template<T> getTemplate(Class<T> templateType, Iterable<ClassLoader> classLoaders) 125 throws Exception { 126 127 for (ClassLoader classLoader : classLoaders) { 128 Template<T> template = doGetTemplate(templateType, classLoader); 129 if (template != null) { 130 return template; 131 } 132 } 133 134 throw new ClassNotFoundException("Cannot find implementation for " + templateType.getName()); 135 } 136 137 @SuppressWarnings("unchecked") 138 private static <T> @Nullable Template<T> doGetTemplate(Class<T> clazz, ClassLoader classLoader) throws Exception { 139 try { 140 Class<?> implementation = (Class<?>) classLoader.loadClass(resolveName(clazz)); 141 Constructor<?> constructor = implementation.getDeclaredConstructor(); 142 constructor.setAccessible(true); 143 144 return (Template<T>) constructor.newInstance(); 145 } 146 catch (ClassNotFoundException e) { 147 return (Template<T>) getTemplateFromServiceLoader(clazz, classLoader); 148 } 149 } 150 151 private static String resolveName(Class<?> c) { 152 var a = c.getAnnotation(JStache.class); 153 String cname; 154 if (a == null || a.name().isBlank()) { 155 156 JStacheName name = findAnnotations(c, JStacheConfig.class).flatMap(config -> Stream.of(config.naming())) 157 .findFirst().orElse(null); 158 159 String prefix = name == null ? JStacheName.UNSPECIFIED : name.prefix(); 160 161 String suffix = name == null ? JStacheName.UNSPECIFIED : name.suffix(); 162 163 prefix = prefix.equals(JStacheName.UNSPECIFIED) ? JStacheName.DEFAULT_PREFIX : prefix; 164 suffix = suffix.equals(JStacheName.UNSPECIFIED) ? JStacheName.DEFAULT_SUFFIX : suffix; 165 166 cname = prefix + c.getSimpleName() + suffix; 167 } 168 else { 169 cname = a.name(); 170 } 171 String packageName = c.getPackageName(); 172 String fqn = packageName + (packageName.isEmpty() ? "" : ".") + cname; 173 return fqn; 174 } 175 176 private static <A extends Annotation> Stream<A> findAnnotations(Class<?> c, Class<A> annotationClass) { 177 var s = annotationElements(c); 178 return s.filter(p -> p != null).map(p -> p.getAnnotation(annotationClass)).filter(a -> a != null); 179 } 180 181 private static @NonNull Stream<AnnotatedElement> annotationElements(Class<?> c) { 182 Stream<? extends AnnotatedElement> enclosing = enclosing(c).flatMap(Templates::expandUsing); 183 var s = Stream.concat(enclosing, Stream.of(c.getPackage(), c.getModule())); 184 return s; 185 } 186 187 /* 188 * This is to get referenced config of JStacheConfig.using 189 */ 190 private static Stream<Class<?>> expandUsing(Class<?> e) { 191 192 JStacheConfig config = e.getAnnotation(JStacheConfig.class); 193 if (config == null) { 194 return Stream.of(e); 195 } 196 var using = config.using(); 197 if (!using.equals(void.class)) { 198 return Stream.of(e, using); 199 } 200 return Stream.of(e); 201 } 202 203 private static Stream<Class<?>> enclosing(Class<?> e) { 204 AbstractSpliterator<Class<?>> split = new AbstractSpliterator<Class<?>>(Long.MAX_VALUE, 0) { 205 @Nullable 206 Class<?> current = e; 207 208 @Override 209 public boolean tryAdvance(Consumer<? super Class<?>> action) { 210 if (current == null) { 211 return false; 212 } 213 var c = current; 214 current = current.getEnclosingClass(); 215 action.accept(c); 216 return true; 217 } 218 }; 219 return StreamSupport.stream(split, false); 220 } 221 222 private static <T> @Nullable Template<?> getTemplateFromServiceLoader(Class<T> clazz, ClassLoader classLoader) { 223 ServiceLoader<TemplateProvider> loader = ServiceLoader.load(TemplateProvider.class, classLoader); 224 for (TemplateProvider rp : loader) { 225 for (var t : rp.provideTemplates()) { 226 if (t.supportsType(clazz)) { 227 return t; 228 } 229 } 230 } 231 return null; 232 } 233 234 private static List<ClassLoader> collectClassLoaders(@Nullable ClassLoader classLoader) { 235 return Stream.of(classLoader, Thread.currentThread().getContextClassLoader(), Template.class.getClassLoader()) 236 .filter(cl -> cl != null).toList(); 237 } 238 239 @SuppressWarnings("unchecked") 240 static <E extends Throwable> void sneakyThrow(final Throwable x) throws E { 241 throw (E) x; 242 } 243 244 static class TemplateInfos { 245 246 public static TemplateInfo templateOf(Class<?> model) throws Exception { 247 JStache stache = model.getAnnotation(JStache.class); 248 if (stache == null) { 249 throw new IllegalArgumentException( 250 "Model class is not annotated with " + JStache.class.getSimpleName() + ". class: " + model); 251 } 252 @Nullable 253 JStachePath pathConfig = resolvePath(model); 254 String templateString = stache.template(); 255 256 final String templateName = resolveName(model); 257 String path = stache.path(); 258 String templatePath; 259 if (templateString.isEmpty() && path.isEmpty()) { 260 String folder = model.getPackageName().replace('.', '/'); 261 folder = folder.isEmpty() ? folder : folder + "/"; 262 templatePath = folder + model.getSimpleName(); 263 } 264 else if (!path.isEmpty()) { 265 templatePath = path; 266 } 267 else { 268 templatePath = ""; 269 } 270 if (pathConfig != null && !templatePath.isBlank()) { 271 templatePath = pathConfig.prefix() + templatePath + pathConfig.suffix(); 272 } 273 274 // Class<?> templateContentType = 275 // EscaperProvider.INSTANCE.nullToDefault(stache.contentType()); 276 277 var ee = EscaperProvider.INSTANCE.providesFromModelType(model, stache); 278 Function<String, String> templateEscaper = ee.getValue(); 279 Class<?> templateContentType = ee.getKey(); 280 281 Function<@Nullable Object, String> templateFormatter = FormatterProvider.INSTANCE 282 .providesFromModelType(model, stache).getValue(); 283 284 long lastLoaded = System.currentTimeMillis(); 285 return new SimpleTemplateInfo( // 286 templateName, // 287 templatePath, // 288 templateString, // 289 templateContentType, // 290 templateEscaper, // 291 templateFormatter, // 292 lastLoaded, // 293 model); 294 295 } 296 297 private static JStachePath resolvePath(Class<?> model) { 298 return annotationElements(model).map(TemplateInfos::resolvePathOnElement).filter(p -> p != null).findFirst() 299 .orElse(null); 300 } 301 302 private static @Nullable JStachePath resolvePathOnElement(AnnotatedElement a) { 303 var path = a.getAnnotation(JStachePath.class); 304 if (path != null) { 305 return path; 306 } 307 var config = a.getAnnotation(JStacheConfig.class); 308 if (config != null && config.pathing().length > 0) { 309 return config.pathing()[0]; 310 } 311 return null; 312 } 313 314 sealed interface StaticProvider<P> { 315 316 default @Nullable Class<?> autoToNull(@Nullable Class<?> type) { 317 if (type == null) { 318 return null; 319 } 320 if (type.equals(autoProvider())) { 321 return null; 322 } 323 return type; 324 } 325 326 default Class<?> nullToDefault(@Nullable Class<?> type) { 327 Class<?> c = autoToNull(type); 328 if (c == null) { 329 return defaultProvider(); 330 } 331 return c; 332 } 333 334 String providesMethod(Class<?> type); 335 336 Class<?> autoProvider(); 337 338 Class<?> defaultProvider(); 339 340 @Nullable 341 Class<?> providerFromJStache(JStache jstache); 342 343 Class<?> providerFromConfig(JStacheConfig config); 344 345 default Class<?> findProvider(Class<?> modelType, JStache jstache) { 346 @Nullable 347 Class<?> provider = autoToNull(providerFromJStache(jstache)); 348 if (provider != null) { 349 return provider; 350 } 351 provider = findAnnotations(modelType, JStacheConfig.class) 352 .map(config -> autoToNull(providerFromConfig(config))).filter(p -> p != null).findFirst() 353 .orElse(null); 354 return nullToDefault(provider); 355 } 356 357 default Entry<Class<?>, P> providesFromModelType(Class<?> modelType, JStache jstache) throws Exception { 358 var t = findProvider(modelType, jstache); 359 return Map.entry(t, provides(t)); 360 } 361 362 @SuppressWarnings("unchecked") 363 default P provides(Class<?> type) throws Exception { 364 String provides = providesMethod(type); 365 var method = type.getMethod(provides); 366 Object r = method.invoke(provides); 367 return (P) r; 368 } 369 370 } 371 372 enum EscaperProvider implements StaticProvider<Function<String, String>> { 373 374 INSTANCE; 375 376 @Override 377 public Class<?> autoProvider() { 378 return UnspecifiedContentType.class; 379 } 380 381 @Override 382 public Class<?> defaultProvider() { 383 return Html.class; 384 } 385 386 @Override 387 public @Nullable Class<?> providerFromJStache(JStache jstache) { 388 return null; 389 } 390 391 @Override 392 public Class<?> providerFromConfig(JStacheConfig config) { 393 return config.contentType(); 394 } 395 396 @Override 397 public String providesMethod(Class<?> type) { 398 JStacheContentType a = type.getAnnotation(JStacheContentType.class); 399 return a.providesMethod(); 400 } 401 402 public Function<String, String> provides(@Nullable Class<?> contentType) throws Exception { 403 contentType = nullToDefault(contentType); 404 if (contentType.equals(Html.class)) { 405 return Html.provider(); 406 } 407 else if (contentType.equals(PlainText.class)) { 408 return PlainText.provider(); 409 } 410 return StaticProvider.super.provides(contentType); 411 } 412 413 } 414 415 enum FormatterProvider implements StaticProvider<Function<@Nullable Object, String>> { 416 417 INSTANCE; 418 419 @Override 420 public Class<?> autoProvider() { 421 return UnspecifiedFormatter.class; 422 } 423 424 @Override 425 public Class<?> defaultProvider() { 426 return DefaultFormatter.class; 427 } 428 429 @Override 430 public @Nullable Class<?> providerFromJStache(JStache jstache) { 431 return null; 432 } 433 434 @Override 435 public Class<?> providerFromConfig(JStacheConfig config) { 436 return config.formatter(); 437 } 438 439 @Override 440 public String providesMethod(Class<?> type) { 441 JStacheFormatter a = type.getAnnotation(JStacheFormatter.class); 442 return a.providesMethod(); 443 } 444 445 public Function<@Nullable Object, String> provides(@Nullable Class<?> formatterType) throws Exception { 446 formatterType = nullToDefault(formatterType); 447 if (formatterType.equals(DefaultFormatter.class)) { 448 return DefaultFormatter.provider(); 449 } 450 else if (formatterType.equals(SpecFormatter.class)) { 451 return SpecFormatter.provider(); 452 } 453 return StaticProvider.super.provides(formatterType); 454 } 455 456 } 457 458 record SimpleTemplateInfo( // 459 String templateName, // 460 String templatePath, // 461 String templateString, // 462 Class<?> templateContentType, // 463 Function<String, String> templateEscaper, // 464 Function<@Nullable Object, String> templateFormatter, // 465 long lastLoaded, // 466 Class<?> modelClass) implements TemplateInfo { 467 468 @Override 469 public boolean supportsType(Class<?> type) { 470 return modelClass().isAssignableFrom(type); 471 } 472 473 } 474 475 } 476 477}