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}