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}