001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.view.groovy; 018 019import java.io.IOException; 020import java.net.URL; 021import java.net.URLClassLoader; 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Locale; 025 026import groovy.text.markup.MarkupTemplateEngine; 027import groovy.text.markup.TemplateConfiguration; 028import groovy.text.markup.TemplateResolver; 029 030import org.springframework.beans.factory.InitializingBean; 031import org.springframework.context.ApplicationContext; 032import org.springframework.context.ApplicationContextAware; 033import org.springframework.context.i18n.LocaleContextHolder; 034import org.springframework.core.io.Resource; 035import org.springframework.lang.Nullable; 036import org.springframework.util.Assert; 037import org.springframework.util.StringUtils; 038 039/** 040 * An extension of Groovy's {@link groovy.text.markup.TemplateConfiguration} and 041 * an implementation of Spring MVC's {@link GroovyMarkupConfig} for creating 042 * a {@code MarkupTemplateEngine} for use in a web application. The most basic 043 * way to configure this class is to set the "resourceLoaderPath". For example: 044 * 045 * <pre class="code"> 046 * 047 * // Add the following to an @Configuration class 048 * 049 * @Bean 050 * public GroovyMarkupConfig groovyMarkupConfigurer() { 051 * GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); 052 * configurer.setResourceLoaderPath("classpath:/WEB-INF/groovymarkup/"); 053 * return configurer; 054 * } 055 * </pre> 056 * 057 * By default this bean will create a {@link MarkupTemplateEngine} with: 058 * <ul> 059 * <li>a parent ClassLoader for loading Groovy templates with their references 060 * <li>the default configuration in the base class {@link TemplateConfiguration} 061 * <li>a {@link groovy.text.markup.TemplateResolver} for resolving template files 062 * </ul> 063 * 064 * You can provide the {@link MarkupTemplateEngine} instance directly to this bean 065 * in which case all other properties will not be effectively ignored. 066 * 067 * <p>This bean must be included in the application context of any application 068 * using the Spring MVC {@link GroovyMarkupView} for rendering. It exists purely 069 * for the purpose of configuring Groovy's Markup templates. It is not meant to be 070 * referenced by application components directly. It implements GroovyMarkupConfig 071 * to be found by GroovyMarkupView without depending on a bean name. Each 072 * DispatcherServlet can define its own GroovyMarkupConfigurer if desired. 073 * 074 * <p>Note that resource caching is enabled by default in {@link MarkupTemplateEngine}. 075 * Use the {@link #setCacheTemplates(boolean)} to configure that as necessary. 076 077 * <p>Spring's Groovy Markup template support requires Groovy 2.3.1 or higher. 078 * 079 * @author Brian Clozel 080 * @author Rossen Stoyanchev 081 * @since 4.1 082 * @see GroovyMarkupView 083 * @see <a href="http://groovy-lang.org/templating.html#_the_markuptemplateengine"> 084 * Groovy Markup Template engine documentation</a> 085 */ 086public class GroovyMarkupConfigurer extends TemplateConfiguration 087 implements GroovyMarkupConfig, ApplicationContextAware, InitializingBean { 088 089 private String resourceLoaderPath = "classpath:"; 090 091 @Nullable 092 private MarkupTemplateEngine templateEngine; 093 094 @Nullable 095 private ApplicationContext applicationContext; 096 097 098 /** 099 * Set the Groovy Markup Template resource loader path(s) via a Spring resource 100 * location. Accepts multiple locations as a comma-separated list of paths. 101 * Standard URLs like "file:" and "classpath:" and pseudo URLs are supported 102 * as understood by Spring's {@link org.springframework.core.io.ResourceLoader}. 103 * Relative paths are allowed when running in an ApplicationContext. 104 * 105 */ 106 public void setResourceLoaderPath(String resourceLoaderPath) { 107 this.resourceLoaderPath = resourceLoaderPath; 108 } 109 110 public String getResourceLoaderPath() { 111 return this.resourceLoaderPath; 112 } 113 114 /** 115 * Set a pre-configured MarkupTemplateEngine to use for the Groovy Markup 116 * Template web configuration. 117 * <p>Note that this engine instance has to be manually configured, since all 118 * other bean properties of this configurer will be ignored. 119 */ 120 public void setTemplateEngine(MarkupTemplateEngine templateEngine) { 121 this.templateEngine = templateEngine; 122 } 123 124 @Override 125 public MarkupTemplateEngine getTemplateEngine() { 126 Assert.state(this.templateEngine != null, "No MarkupTemplateEngine set"); 127 return this.templateEngine; 128 } 129 130 @Override 131 public void setApplicationContext(ApplicationContext applicationContext) { 132 this.applicationContext = applicationContext; 133 } 134 135 protected ApplicationContext getApplicationContext() { 136 Assert.state(this.applicationContext != null, "No ApplicationContext set"); 137 return this.applicationContext; 138 } 139 140 /** 141 * This method should not be used, since the considered Locale for resolving 142 * templates is the Locale for the current HTTP request. 143 */ 144 @Override 145 public void setLocale(Locale locale) { 146 super.setLocale(locale); 147 } 148 149 150 @Override 151 public void afterPropertiesSet() throws Exception { 152 if (this.templateEngine == null) { 153 this.templateEngine = createTemplateEngine(); 154 } 155 } 156 157 protected MarkupTemplateEngine createTemplateEngine() throws IOException { 158 if (this.templateEngine == null) { 159 ClassLoader templateClassLoader = createTemplateClassLoader(); 160 this.templateEngine = new MarkupTemplateEngine(templateClassLoader, this, new LocaleTemplateResolver()); 161 } 162 return this.templateEngine; 163 } 164 165 /** 166 * Create a parent ClassLoader for Groovy to use as parent ClassLoader 167 * when loading and compiling templates. 168 */ 169 protected ClassLoader createTemplateClassLoader() throws IOException { 170 String[] paths = StringUtils.commaDelimitedListToStringArray(getResourceLoaderPath()); 171 List<URL> urls = new ArrayList<>(); 172 for (String path : paths) { 173 Resource[] resources = getApplicationContext().getResources(path); 174 if (resources.length > 0) { 175 for (Resource resource : resources) { 176 if (resource.exists()) { 177 urls.add(resource.getURL()); 178 } 179 } 180 } 181 } 182 ClassLoader classLoader = getApplicationContext().getClassLoader(); 183 Assert.state(classLoader != null, "No ClassLoader"); 184 return (!urls.isEmpty() ? new URLClassLoader(urls.toArray(new URL[0]), classLoader) : classLoader); 185 } 186 187 /** 188 * Resolve a template from the given template path. 189 * <p>The default implementation uses the Locale associated with the current request, 190 * as obtained through {@link org.springframework.context.i18n.LocaleContextHolder LocaleContextHolder}, 191 * to find the template file. Effectively the locale configured at the engine level is ignored. 192 * @see LocaleContextHolder 193 * @see #setLocale 194 */ 195 protected URL resolveTemplate(ClassLoader classLoader, String templatePath) throws IOException { 196 MarkupTemplateEngine.TemplateResource resource = MarkupTemplateEngine.TemplateResource.parse(templatePath); 197 Locale locale = LocaleContextHolder.getLocale(); 198 URL url = classLoader.getResource(resource.withLocale(StringUtils.replace(locale.toString(), "-", "_")).toString()); 199 if (url == null) { 200 url = classLoader.getResource(resource.withLocale(locale.getLanguage()).toString()); 201 } 202 if (url == null) { 203 url = classLoader.getResource(resource.withLocale(null).toString()); 204 } 205 if (url == null) { 206 throw new IOException("Unable to load template:" + templatePath); 207 } 208 return url; 209 } 210 211 212 /** 213 * Custom {@link TemplateResolver template resolver} that simply delegates to 214 * {@link #resolveTemplate(ClassLoader, String)}.. 215 */ 216 private class LocaleTemplateResolver implements TemplateResolver { 217 218 @Nullable 219 private ClassLoader classLoader; 220 221 @Override 222 public void configure(ClassLoader templateClassLoader, TemplateConfiguration configuration) { 223 this.classLoader = templateClassLoader; 224 } 225 226 @Override 227 public URL resolveTemplate(String templatePath) throws IOException { 228 Assert.state(this.classLoader != null, "No template ClassLoader available"); 229 return GroovyMarkupConfigurer.this.resolveTemplate(this.classLoader, templatePath); 230 } 231 } 232 233}