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