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}