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
pull/9002/head
Brian Clozel 8 years ago
parent 7e77e648bf
commit ec25e51f1f

@ -114,10 +114,9 @@ public class MustacheAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(MustacheViewResolver.class) @ConditionalOnMissingBean(MustacheViewResolver.class)
public MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler) { public MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler) {
MustacheViewResolver resolver = new MustacheViewResolver(); MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler);
this.mustache.applyToViewResolver(resolver); this.mustache.applyToViewResolver(resolver);
resolver.setCharset(this.mustache.getCharsetName()); resolver.setCharset(this.mustache.getCharsetName());
resolver.setCompiler(mustacheCompiler);
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver; return resolver;
} }

@ -16,58 +16,84 @@
package org.springframework.boot.autoconfigure.mustache.servlet; 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 java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template; import com.samskivert.mustache.Template;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.View; import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractTemplateView; import org.springframework.web.servlet.view.AbstractTemplateView;
/** /**
* Spring MVC {@link View} using the Mustache template engine. * Spring MVC {@link View} using the Mustache template engine.
* *
* @author Brian Clozel
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
* @since 1.2.2 * @since 2.0.0
*/ */
public class MustacheView extends AbstractTemplateView { public class MustacheView extends AbstractTemplateView {
private Template template; private Mustache.Compiler compiler;
private String charset;
/** /**
* Create a new {@link MustacheView} instance. * Set the Mustache compiler to be used by this view.
* @since 1.2.5 * <p>Typically this property is not set directly. Instead a single
* @see #setTemplate(Template) * {@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. * Set the charset used for reading Mustache template files.
* @param template the source template * @param charset the charset to use for reading template files
*/ */
public MustacheView(Template template) { public void setCharset(String charset) {
this.template = template; this.charset = charset;
} }
/** @Override
* Set the Mustache template that should actually be rendered. public boolean checkResource(Locale locale) throws Exception {
* @param template the mustache template Resource resource = getApplicationContext().getResource(this.getUrl());
* @since 1.2.5 return (resource != null && resource.exists());
*/
public void setTemplate(Template template) {
this.template = template;
} }
@Override @Override
protected void renderMergedTemplateModel(Map<String, Object> model, protected void renderMergedTemplateModel(Map<String, Object> model, HttpServletRequest request,
HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
if (this.template != null) { Template template = createTemplate(getApplicationContext().getResource(this.getUrl()));
this.template.execute(model, response.getWriter()); 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());
}
} }

@ -16,50 +16,46 @@
package org.springframework.boot.autoconfigure.mustache.servlet; 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;
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.ViewResolver;
import org.springframework.web.servlet.view.AbstractTemplateViewResolver; import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
/** /**
* Spring MVC {@link ViewResolver} for Mustache. * Spring MVC {@link ViewResolver} for Mustache.
* *
* @author Dave Syer * @author Brian Clozel
* @author Andy Wilkinson * @since 2.0.0
* @author Phillip Webb
* @since 1.2.2
*/ */
public class MustacheViewResolver extends AbstractTemplateViewResolver { public class MustacheViewResolver extends AbstractTemplateViewResolver {
private Compiler compiler = Mustache.compiler(); private final Mustache.Compiler compiler;
private String charset; private String charset;
/**
* Create a {@code MustacheViewResolver} backed by a default
* instance of a {@link Mustache.Compiler}.
*/
public MustacheViewResolver() { public MustacheViewResolver() {
this.compiler = Mustache.compiler();
setViewClass(requiredViewClass()); setViewClass(requiredViewClass());
} }
@Override
protected Class<?> requiredViewClass() {
return MustacheView.class;
}
/** /**
* Set the compiler. * Create a {@code MustacheViewResolver} backed by a custom
* @param compiler the compiler * 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; this.compiler = compiler;
setViewClass(requiredViewClass());
}
@Override
protected Class<?> requiredViewClass() {
return MustacheView.class;
} }
/** /**
@ -71,57 +67,11 @@ public class MustacheViewResolver extends AbstractTemplateViewResolver {
} }
@Override @Override
protected View loadView(String viewName, Locale locale) throws Exception { protected AbstractUrlBasedView buildView(String viewName) throws Exception {
Resource resource = resolveResource(viewName, locale); MustacheView view = (MustacheView) super.buildView(viewName);
if (resource == null) { view.setCompiler(this.compiler);
return null; view.setCharset(this.charset);
} return view;
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());
} }
} }

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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.Before;
import org.junit.Test; 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.mock.web.MockServletContext;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.View; import org.springframework.web.servlet.View;
import static org.assertj.core.api.Assertions.assertThat; 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}. * Tests for {@link MustacheViewResolver}.
@ -46,7 +37,9 @@ public class MustacheViewResolverTests {
@Before @Before
public void init() { 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.setServletContext(new MockServletContext());
this.resolver.setPrefix("classpath:/mustache-templates/"); this.resolver.setPrefix("classpath:/mustache-templates/");
this.resolver.setSuffix(".html"); this.resolver.setSuffix(".html");
@ -58,32 +51,10 @@ public class MustacheViewResolverTests {
} }
@Test @Test
public void resolveNullLocale() throws Exception { public void resolveExisting() throws Exception {
assertThat(this.resolver.resolveViewName("foo", null)).isNotNull(); 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 @Test
public void setsContentType() throws Exception { public void setsContentType() throws Exception {
this.resolver.setContentType("application/octet-stream"); 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();
}
} }

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -55,12 +55,12 @@ public class MustacheViewTests {
@Test @Test
public void viewResolvesHandlebars() throws Exception { public void viewResolvesHandlebars() throws Exception {
MustacheView view = new MustacheView( MustacheView view = new MustacheView();
Mustache.compiler().compile("Hello {{msg}}")); view.setCompiler(Mustache.compiler());
view.setUrl("classpath:/mustache-templates/foo.html");
view.setApplicationContext(this.context); view.setApplicationContext(this.context);
view.render(Collections.singletonMap("msg", "World"), this.request, view.render(Collections.singletonMap("World", "Spring"), this.request, this.response);
this.response); assertThat(this.response.getContentAsString()).isEqualTo("Hello Spring");
assertThat(this.response.getContentAsString()).isEqualTo("Hello World");
} }
} }

@ -120,12 +120,12 @@ public class MustacheWebIntegrationTests {
@Bean @Bean
public MustacheViewResolver viewResolver() { 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.setPrefix("classpath:/mustache-templates/");
resolver.setSuffix(".html"); resolver.setSuffix(".html");
resolver.setCompiler(
Mustache.compiler().withLoader(new MustacheResourceTemplateLoader(
"classpath:/mustache-templates/", ".html")));
return resolver; return resolver;
} }

Loading…
Cancel
Save