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}