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}