001package io.jstach.opt.spring.boot.webmvc;
002
003import java.util.ArrayList;
004import java.util.HashSet;
005import java.util.List;
006import java.util.ServiceLoader;
007import java.util.Set;
008import java.util.stream.Collectors;
009import java.util.stream.Stream;
010
011import org.apache.commons.logging.Log;
012import org.apache.commons.logging.LogFactory;
013import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
014import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
015import org.springframework.boot.context.properties.EnableConfigurationProperties;
016import org.springframework.context.annotation.Bean;
017import org.springframework.context.annotation.Configuration;
018import org.springframework.core.env.Environment;
019
020import io.jstach.jstachio.JStachio;
021import io.jstach.jstachio.Template;
022import io.jstach.jstachio.TemplateConfig;
023import io.jstach.jstachio.spi.JStachioConfig;
024import io.jstach.jstachio.spi.JStachioExtension;
025import io.jstach.jstachio.spi.JStachioTemplateFinder;
026import io.jstach.jstachio.spi.TemplateProvider;
027import io.jstach.jstachio.spi.Templates;
028import io.jstach.opt.spring.SpringJStachio;
029import io.jstach.opt.spring.SpringJStachioExtension;
030import io.jstach.opt.spring.web.JStachioHttpMessageConverter;
031import io.jstach.opt.spring.webmvc.ServletJStachioHttpMessageConverter;
032
033/**
034 * Configures JStachio Spring style.
035 * <p>
036 * Templates are loaded from the ServiceLoader and are then registered in the
037 * ApplicationContext. Extensions that are wired by Spring will also be discovered as well
038 * as ServiceLoader based extensions that are not already wired as beans.
039 *
040 * @author agentgt
041 * @author dsyer
042 * @apiNote while this class and methods on this class are public for Spring reflection it
043 * is not intended to be true public API.
044 */
045@Configuration
046@EnableConfigurationProperties(value = JStachioProperties.class)
047public class JStachioConfiguration {
048
049        private static final Log logger = LogFactory.getLog(JStachioConfiguration.class);
050
051        private final ConfigurableListableBeanFactory beanFactory;
052
053        /**
054         * Do nothing constructor to placate jdk 18 javadoc
055         * @param beanFactory used to register the serviceloader found templates
056         */
057        public JStachioConfiguration(@SuppressWarnings("exports") ConfigurableListableBeanFactory beanFactory) {
058                this.beanFactory = beanFactory;
059        }
060
061        /**
062         * Templates found with the service loader
063         * @param templateConfig used to create singleton templates
064         * @return templates
065         * @see #templateConfig()
066         */
067        @Bean
068        public List<Template<?>> templatesByServiceLoader(TemplateConfig templateConfig) {
069                /*
070                 * TODO check config.getBoolean(JStachioConfig.SERVICELOADER_TEMPLATE_DISABLE)
071                 * Cannot do it yet because that will change the method contract and require a
072                 * minor version change.
073                 */
074                var serviceLoader = serviceLoader(TemplateProvider.class);
075                var templates = Templates.findTemplates(serviceLoader, templateConfig, e -> {
076                        logger.error("Failed to load template provider. Skipping it.", e);
077                }).toList();
078                for (var t : templates) {
079                        this.beanFactory.registerSingleton(t.getClass().getName(), t);
080                }
081                return templates;
082        }
083
084        /**
085         * Resolve config from spring environment
086         * @param environment for properties
087         * @return config
088         */
089        @Bean
090        @ConditionalOnMissingBean(JStachioConfig.class)
091        public JStachioConfig config(@SuppressWarnings("exports") Environment environment) {
092                return SpringJStachioExtension.config(environment);
093        }
094
095        /**
096         * Resolve template finder configs
097         * @param config jstachio config
098         * @param templateConfig the template config
099         * @return spring powered template finder
100         */
101        @Bean
102        @ConditionalOnMissingBean(JStachioTemplateFinder.class)
103        public JStachioTemplateFinder templateFinder(JStachioConfig config, TemplateConfig templateConfig) {
104                List<JStachioTemplateFinder> finders = new ArrayList<>();
105                if (!config.getBoolean(JStachioConfig.SERVICELOADER_TEMPLATE_DISABLE)) {
106                        var templates = templatesByServiceLoader(templateConfig);
107                        if (!templates.isEmpty()) {
108                                finders.add(JStachioTemplateFinder.of(templates, 0));
109                        }
110                        else {
111                                logger.info("Failed to find any templates with ServiceLoader. "
112                                                + "Using reflection based fallback! https://jstach.io/jstachio/#faq_template_not_found ");
113                        }
114                }
115                finders.add(JStachioTemplateFinder.defaultTemplateFinder(config));
116                var unCached = JStachioTemplateFinder.of(finders);
117                /*
118                 * TODO we should add a flag to disable cache for developer mode with the
119                 * assumption that some form of hot reloading might be happening.
120                 */
121                return JStachioTemplateFinder.cachedTemplateFinder(unCached);
122        }
123
124        /**
125         * The default template config is empty and will let each template resolve its own
126         * config. The template config contains an optional formatter (nullable) and optional
127         * escaper (nullable). If a template config is provided as a bean somewhere else it
128         * will replace this default. The only time this could be of use is if you needed a
129         * formatter or escaper with custom wiring.
130         * @return empty template config.
131         * @see TemplateConfig#empty()
132         */
133        @Bean
134        @ConditionalOnMissingBean(TemplateConfig.class)
135        public TemplateConfig templateConfig() {
136                return TemplateConfig.empty();
137        }
138
139        /**
140         * Creates a services based on spring objects.
141         * @param config used for config
142         * @param templateFinder used to find templates
143         * @return spring powered jstatchio extension provider
144         * @see TemplateConfig#empty()
145         */
146        @Bean
147        public SpringJStachioExtension springJStachioExtension(JStachioConfig config,
148                        JStachioTemplateFinder templateFinder) {
149                return new SpringJStachioExtension(config, templateFinder);
150        }
151
152        /**
153         * Creates jstachio from found plugins
154         * @param extensions plugins
155         * @return spring version fo jstachio
156         */
157        @Bean
158        @ConditionalOnMissingBean(JStachio.class)
159        public SpringJStachio jstachio(List<JStachioExtension> extensions) {
160                Set<Class<?>> extensionClasses = extensions.stream().map(e -> e.getClass())
161                                .collect(Collectors.toCollection(HashSet::new));
162                /*
163                 * We attempt to filter already loaded extensions via the service loader.
164                 *
165                 * We should probably make this configurable.
166                 */
167                List<JStachioExtension> serviceLoaderExtensions = serviceLoader(JStachioExtension.class) //
168                                .stream() //
169                                .filter(p -> !extensionClasses.contains(p.type())) //
170                                .map(p -> p.get()) //
171                                .toList();
172
173                for (var s : serviceLoaderExtensions) {
174                        logger.info("JStachio found extension by ServiceLoader: " + s.getClass());
175                }
176
177                extensions = Stream.concat(extensions.stream(), serviceLoaderExtensions.stream()).toList();
178
179                var js = new SpringJStachio(extensions);
180                // We need this for the view mixins.
181                JStachio.setStatic(() -> js);
182                return js;
183        }
184
185        private <T> ServiceLoader<T> serviceLoader(Class<T> spiClass) {
186                ClassLoader classLoader = beanFactory.getBeanClassLoader();
187                return classLoader == null ? ServiceLoader.load(spiClass) : ServiceLoader.load(spiClass, classLoader);
188        }
189
190        /**
191         * Creates a message converter from Spring JStachio
192         * @param jstachio jstachio instance
193         * @param properties spring boot powered properties
194         * @return jstachio message converter
195         */
196        @Bean
197        @ConditionalOnMissingBean(value = JStachioHttpMessageConverter.class)
198        public JStachioHttpMessageConverter messageConverter(JStachio jstachio, JStachioProperties properties) {
199                return new ServletJStachioHttpMessageConverter(jstachio, properties.getMediaType(),
200                                properties.getBufferLimit());
201        }
202
203}