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