From ec25e51f1ff830527c09415c16c64b99e082198c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 19 Apr 2017 17:08:13 +0200 Subject: [PATCH] Refactor Mustache views support in Spring MVC This commit simplifies the Mustache support for Spring MVC and removes the included (view-based) i18n support in favor of more idiomatic constructs like Mustache lambdas. Fixes gh-8941 --- .../mustache/MustacheAutoConfiguration.java | 3 +- .../mustache/servlet/MustacheView.java | 68 +++++++++---- .../servlet/MustacheViewResolver.java | 98 +++++-------------- .../servlet/MustacheViewResolverTests.java | 61 ++---------- .../mustache/servlet/MustacheViewTests.java | 12 +-- .../servlet/MustacheWebIntegrationTests.java | 8 +- 6 files changed, 88 insertions(+), 162 deletions(-) diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java index e84c8f4c27..d7bc956638 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java @@ -114,10 +114,9 @@ public class MustacheAutoConfiguration { @Bean @ConditionalOnMissingBean(MustacheViewResolver.class) public MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler) { - MustacheViewResolver resolver = new MustacheViewResolver(); + MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler); this.mustache.applyToViewResolver(resolver); resolver.setCharset(this.mustache.getCharsetName()); - resolver.setCompiler(mustacheCompiler); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheView.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheView.java index f877b2bbb6..7fbe995aca 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheView.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheView.java @@ -16,58 +16,84 @@ package org.springframework.boot.autoconfigure.mustache.servlet; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Locale; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; +import org.springframework.core.io.Resource; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.AbstractTemplateView; /** * Spring MVC {@link View} using the Mustache template engine. * + * @author Brian Clozel * @author Dave Syer * @author Phillip Webb - * @since 1.2.2 + * @since 2.0.0 */ public class MustacheView extends AbstractTemplateView { - private Template template; + private Mustache.Compiler compiler; + + private String charset; /** - * Create a new {@link MustacheView} instance. - * @since 1.2.5 - * @see #setTemplate(Template) + * Set the Mustache compiler to be used by this view. + *

Typically this property is not set directly. Instead a single + * {@link Mustache.Compiler} is expected in the Spring application context + * which is used to compile Mustache templates. + * @param compiler the Mustache compiler */ - public MustacheView() { + public void setCompiler(Mustache.Compiler compiler) { + this.compiler = compiler; } /** - * Create a new {@link MustacheView} with the specified template. - * @param template the source template + * Set the charset used for reading Mustache template files. + * @param charset the charset to use for reading template files */ - public MustacheView(Template template) { - this.template = template; + public void setCharset(String charset) { + this.charset = charset; } - /** - * Set the Mustache template that should actually be rendered. - * @param template the mustache template - * @since 1.2.5 - */ - public void setTemplate(Template template) { - this.template = template; + @Override + public boolean checkResource(Locale locale) throws Exception { + Resource resource = getApplicationContext().getResource(this.getUrl()); + return (resource != null && resource.exists()); } @Override - protected void renderMergedTemplateModel(Map model, - HttpServletRequest request, HttpServletResponse response) throws Exception { - if (this.template != null) { - this.template.execute(model, response.getWriter()); + protected void renderMergedTemplateModel(Map model, HttpServletRequest request, + HttpServletResponse response) throws Exception { + Template template = createTemplate(getApplicationContext().getResource(this.getUrl())); + if (template != null) { + template.execute(model, response.getWriter()); + } + } + + private Template createTemplate(Resource resource) throws IOException { + Reader reader = getReader(resource); + try { + return this.compiler.compile(reader); + } + finally { + reader.close(); } } + private Reader getReader(Resource resource) throws IOException { + if (this.charset != null) { + return new InputStreamReader(resource.getInputStream(), this.charset); + } + return new InputStreamReader(resource.getInputStream()); + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolver.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolver.java index d513c65554..1c66279268 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolver.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolver.java @@ -16,50 +16,46 @@ package org.springframework.boot.autoconfigure.mustache.servlet; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.Locale; - import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.Mustache.Compiler; -import com.samskivert.mustache.Template; -import org.springframework.beans.propertyeditors.LocaleEditor; -import org.springframework.core.io.Resource; -import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.view.AbstractTemplateViewResolver; +import org.springframework.web.servlet.view.AbstractUrlBasedView; /** * Spring MVC {@link ViewResolver} for Mustache. * - * @author Dave Syer - * @author Andy Wilkinson - * @author Phillip Webb - * @since 1.2.2 + * @author Brian Clozel + * @since 2.0.0 */ public class MustacheViewResolver extends AbstractTemplateViewResolver { - private Compiler compiler = Mustache.compiler(); + private final Mustache.Compiler compiler; private String charset; + /** + * Create a {@code MustacheViewResolver} backed by a default + * instance of a {@link Mustache.Compiler}. + */ public MustacheViewResolver() { + this.compiler = Mustache.compiler(); setViewClass(requiredViewClass()); } - @Override - protected Class requiredViewClass() { - return MustacheView.class; - } - /** - * Set the compiler. - * @param compiler the compiler + * Create a {@code MustacheViewResolver} backed by a custom + * instance of a {@link Mustache.Compiler}. + * @param compiler the Mustache compiler used to compile templates */ - public void setCompiler(Compiler compiler) { + public MustacheViewResolver(Mustache.Compiler compiler) { this.compiler = compiler; + setViewClass(requiredViewClass()); + } + + @Override + protected Class requiredViewClass() { + return MustacheView.class; } /** @@ -71,57 +67,11 @@ public class MustacheViewResolver extends AbstractTemplateViewResolver { } @Override - protected View loadView(String viewName, Locale locale) throws Exception { - Resource resource = resolveResource(viewName, locale); - if (resource == null) { - return null; - } - MustacheView mustacheView = (MustacheView) super.loadView(viewName, locale); - mustacheView.setTemplate(createTemplate(resource)); - return mustacheView; - } - - private Resource resolveResource(String viewName, Locale locale) { - return resolveFromLocale(viewName, getLocale(locale)); - } - - private Resource resolveFromLocale(String viewName, String locale) { - Resource resource = getApplicationContext() - .getResource(getPrefix() + viewName + locale + getSuffix()); - if (resource == null || !resource.exists()) { - if (locale.isEmpty()) { - return null; - } - int index = locale.lastIndexOf("_"); - return resolveFromLocale(viewName, locale.substring(0, index)); - } - return resource; - } - - private String getLocale(Locale locale) { - if (locale == null) { - return ""; - } - LocaleEditor localeEditor = new LocaleEditor(); - localeEditor.setValue(locale); - return "_" + localeEditor.getAsText(); - } - - private Template createTemplate(Resource resource) throws IOException { - Reader reader = getReader(resource); - try { - return this.compiler.compile(reader); - } - finally { - reader.close(); - } - } - - private Reader getReader(Resource resource) throws IOException { - if (this.charset != null) { - return new InputStreamReader(resource.getInputStream(), this.charset); - } - return new InputStreamReader(resource.getInputStream()); + protected AbstractUrlBasedView buildView(String viewName) throws Exception { + MustacheView view = (MustacheView) super.buildView(viewName); + view.setCompiler(this.compiler); + view.setCharset(this.charset); + return view; } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolverTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolverTests.java index b15931873f..36a4ad24e9 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolverTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,14 @@ package org.springframework.boot.autoconfigure.mustache.servlet; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.Locale; - import org.junit.Before; import org.junit.Test; -import org.springframework.core.io.Resource; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.mock.web.MockServletContext; -import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.servlet.View; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link MustacheViewResolver}. @@ -46,7 +37,9 @@ public class MustacheViewResolverTests { @Before public void init() { - this.resolver.setApplicationContext(new StaticWebApplicationContext()); + GenericApplicationContext applicationContext = new GenericApplicationContext(); + applicationContext.refresh(); + this.resolver.setApplicationContext(applicationContext); this.resolver.setServletContext(new MockServletContext()); this.resolver.setPrefix("classpath:/mustache-templates/"); this.resolver.setSuffix(".html"); @@ -58,32 +51,10 @@ public class MustacheViewResolverTests { } @Test - public void resolveNullLocale() throws Exception { + public void resolveExisting() throws Exception { assertThat(this.resolver.resolveViewName("foo", null)).isNotNull(); } - @Test - public void resolveDefaultLocale() throws Exception { - assertThat(this.resolver.resolveViewName("foo", Locale.US)).isNotNull(); - } - - @Test - public void resolveDoubleLocale() throws Exception { - assertThat(this.resolver.resolveViewName("foo", Locale.CANADA_FRENCH)) - .isNotNull(); - } - - @Test - public void resolveTripleLocale() throws Exception { - assertThat(this.resolver.resolveViewName("foo", new Locale("en", "GB", "cy"))) - .isNotNull(); - } - - @Test - public void resolveSpecificLocale() throws Exception { - assertThat(this.resolver.resolveViewName("foo", new Locale("de"))).isNotNull(); - } - @Test public void setsContentType() throws Exception { this.resolver.setContentType("application/octet-stream"); @@ -92,24 +63,4 @@ public class MustacheViewResolverTests { } - @Test - public void templateResourceInputStreamIsClosed() throws Exception { - final Resource resource = mock(Resource.class); - given(resource.exists()).willReturn(true); - InputStream inputStream = new ByteArrayInputStream(new byte[0]); - InputStream spyInputStream = spy(inputStream); - given(resource.getInputStream()).willReturn(spyInputStream); - this.resolver = new MustacheViewResolver(); - this.resolver.setApplicationContext(new StaticWebApplicationContext() { - - @Override - public Resource getResource(String location) { - return resource; - } - - }); - this.resolver.loadView("foo", null); - verify(spyInputStream).close(); - } - } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewTests.java index c1e1d7f4b5..a1801eb2cd 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheViewTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,12 +55,12 @@ public class MustacheViewTests { @Test public void viewResolvesHandlebars() throws Exception { - MustacheView view = new MustacheView( - Mustache.compiler().compile("Hello {{msg}}")); + MustacheView view = new MustacheView(); + view.setCompiler(Mustache.compiler()); + view.setUrl("classpath:/mustache-templates/foo.html"); view.setApplicationContext(this.context); - view.render(Collections.singletonMap("msg", "World"), this.request, - this.response); - assertThat(this.response.getContentAsString()).isEqualTo("Hello World"); + view.render(Collections.singletonMap("World", "Spring"), this.request, this.response); + assertThat(this.response.getContentAsString()).isEqualTo("Hello Spring"); } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheWebIntegrationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheWebIntegrationTests.java index cf8a5d28b9..7b0a45c425 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheWebIntegrationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/servlet/MustacheWebIntegrationTests.java @@ -120,12 +120,12 @@ public class MustacheWebIntegrationTests { @Bean public MustacheViewResolver viewResolver() { - MustacheViewResolver resolver = new MustacheViewResolver(); + Mustache.Compiler compiler = Mustache.compiler() + .withLoader(new MustacheResourceTemplateLoader("classpath:/mustache-templates/", + ".html")); + MustacheViewResolver resolver = new MustacheViewResolver(compiler); resolver.setPrefix("classpath:/mustache-templates/"); resolver.setSuffix(".html"); - resolver.setCompiler( - Mustache.compiler().withLoader(new MustacheResourceTemplateLoader( - "classpath:/mustache-templates/", ".html"))); return resolver; }