Refine Mustache support

Refine Mustache support to provide a cleaner separation between the
reactive and servlet implementations. The views have now moved to the
`spring-boot` project and the auto-configuration has been split into
two distinct `@Imports` to save needing full package declarations.

See gh-8941
This commit is contained in:
Phillip Webb
2017-04-25 09:11:14 -07:00
parent 06558675bb
commit 1ad318d8a6
23 changed files with 230 additions and 150 deletions

View File

@@ -54,6 +54,11 @@
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>

View File

@@ -0,0 +1,119 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.reactive.result.view;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import com.samskivert.mustache.Mustache.Compiler;
import com.samskivert.mustache.Template;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.server.ServerWebExchange;
/**
* Spring WebFlux {@link View} using the Mustache template engine.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class MustacheView extends AbstractUrlBasedView {
private Compiler compiler;
private String charset;
/**
* Set the JMustache compiler to be used by this view. Typically this property is not
* set directly. Instead a single {@link Compiler} is expected in the Spring
* application context which is used to compile Mustache templates.
* @param compiler the Mustache compiler
*/
public void setCompiler(Compiler compiler) {
this.compiler = compiler;
}
/**
* Set the charset used for reading Mustache template files.
* @param charset the charset to use for reading template files
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
public boolean checkResourceExists(Locale locale) throws Exception {
return resolveResource() != null;
}
@Override
protected Mono<Void> renderInternal(Map<String, Object> model, MediaType contentType,
ServerWebExchange exchange) {
Resource resource = resolveResource();
if (resource == null) {
return Mono.error(new IllegalStateException(
"Could not find Mustache template with URL [" + getUrl() + "]"));
}
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
try (Reader reader = getReader(resource)) {
Template template = this.compiler.compile(reader);
Charset charset = getCharset(contentType).orElse(getDefaultCharset());
try (Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(),
charset)) {
template.execute(model, writer);
writer.flush();
}
}
catch (Throwable exc) {
return Mono.error(exc);
}
return exchange.getResponse().writeWith(Flux.just(dataBuffer));
}
private Resource resolveResource() {
Resource resource = getApplicationContext().getResource(getUrl());
if (resource == null || !resource.exists()) {
return null;
}
return resource;
}
private Reader getReader(Resource resource) throws IOException {
if (this.charset != null) {
return new InputStreamReader(resource.getInputStream(), this.charset);
}
return new InputStreamReader(resource.getInputStream());
}
private Optional<Charset> getCharset(MediaType mediaType) {
return Optional.ofNullable(mediaType != null ? mediaType.getCharset() : null);
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.reactive.result.view;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Compiler;
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
import org.springframework.web.reactive.result.view.UrlBasedViewResolver;
import org.springframework.web.reactive.result.view.ViewResolver;
/**
* Spring WebFlux {@link ViewResolver} for Mustache.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class MustacheViewResolver extends UrlBasedViewResolver {
private final Compiler compiler;
private String charset;
/**
* Create a {@code MustacheViewResolver} backed by a default instance of a
* {@link Compiler}.
*/
public MustacheViewResolver() {
this.compiler = Mustache.compiler();
setViewClass(requiredViewClass());
}
/**
* Create a {@code MustacheViewResolver} backed by a custom instance of a
* {@link Compiler}.
* @param compiler the Mustache compiler used to compile templates
*/
public MustacheViewResolver(Compiler compiler) {
this.compiler = compiler;
setViewClass(requiredViewClass());
}
/**
* Set the charset.
* @param charset the charset
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
protected Class<?> requiredViewClass() {
return MustacheView.class;
}
@Override
protected AbstractUrlBasedView createUrlBasedView(String viewName) {
MustacheView view = (MustacheView) super.createUrlBasedView(viewName);
view.setCompiler(this.compiler);
view.setCharset(this.charset);
return view;
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Additional {@link org.springframework.web.reactive.result.view.View Views} for use with
* WebFlux.
*/
package org.springframework.boot.web.reactive.result.view;

View File

@@ -0,0 +1,102 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.servlet.view;
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.Compiler;
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 2.0.0
*/
public class MustacheView extends AbstractTemplateView {
private Compiler compiler;
private String charset;
/**
* Set the Mustache compiler to be used by this view.
* <p>
* Typically this property is not set directly. Instead a single {@link Compiler} is
* expected in the Spring application context which is used to compile Mustache
* templates.
* @param compiler the Mustache compiler
*/
public void setCompiler(Compiler compiler) {
this.compiler = compiler;
}
/**
* Set the charset used for reading Mustache template files.
* @param charset the charset to use for reading template files
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
public boolean checkResource(Locale locale) throws Exception {
Resource resource = getApplicationContext().getResource(this.getUrl());
return (resource != null && resource.exists());
}
@Override
protected void renderMergedTemplateModel(Map<String, Object> 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());
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.servlet.view;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Compiler;
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 Brian Clozel
* @since 2.0.0
*/
public class MustacheViewResolver extends AbstractTemplateViewResolver {
private final Mustache.Compiler compiler;
private String charset;
/**
* Create a {@code MustacheViewResolver} backed by a default instance of a
* {@link Compiler}.
*/
public MustacheViewResolver() {
this.compiler = Mustache.compiler();
setViewClass(requiredViewClass());
}
/**
* Create a {@code MustacheViewResolver} backed by a custom instance of a
* {@link Compiler}.
* @param compiler the Mustache compiler used to compile templates
*/
public MustacheViewResolver(Compiler compiler) {
this.compiler = compiler;
setViewClass(requiredViewClass());
}
@Override
protected Class<?> requiredViewClass() {
return MustacheView.class;
}
/**
* Set the charset.
* @param charset the charset
*/
public void setCharset(String charset) {
this.charset = charset;
}
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
MustacheView view = (MustacheView) super.buildView(viewName);
view.setCompiler(this.compiler);
view.setCharset(this.charset);
return view;
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Additional {@link org.springframework.web.servlet.View Views} for use with Web MVC.
*/
package org.springframework.boot.web.servlet.view;

View File

@@ -0,0 +1,57 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.reactive.result.view;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.GenericApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MustacheViewResolver}.
*
* @author Brian Clozel
*/
public class MustacheViewResolverTests {
private final String prefix = "classpath:/"
+ getClass().getPackage().getName().replace(".", "/") + "/";
private MustacheViewResolver resolver = new MustacheViewResolver();
@Before
public void init() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
this.resolver.setApplicationContext(applicationContext);
this.resolver.setPrefix(this.prefix);
this.resolver.setSuffix(".html");
}
@Test
public void resolveNonExistent() throws Exception {
assertThat(this.resolver.resolveViewName("bar", null).block()).isNull();
}
@Test
public void resolveExisting() throws Exception {
assertThat(this.resolver.resolveViewName("template", null).block()).isNotNull();
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.reactive.result.view;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import com.samskivert.mustache.Mustache;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MustacheView}.
*
* @author Brian Clozel
*/
public class MustacheViewTests {
private final String templateUrl = "classpath:/"
+ getClass().getPackage().getName().replace(".", "/") + "/template.html";
private GenericApplicationContext context = new GenericApplicationContext();
private MockServerWebExchange exchange;
@Before
public void init() {
this.context.refresh();
}
@Test
public void viewResolvesHandlebars() throws Exception {
this.exchange = MockServerHttpRequest.get("/test").toExchange();
MustacheView view = new MustacheView();
view.setCompiler(Mustache.compiler());
view.setUrl(this.templateUrl);
view.setCharset(StandardCharsets.UTF_8.displayName());
view.setApplicationContext(this.context);
view.render(Collections.singletonMap("World", "Spring"), MediaType.TEXT_HTML,
this.exchange).block();
assertThat(this.exchange.getResponse().getBodyAsString().block())
.isEqualTo("Hello Spring");
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.servlet.view;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.servlet.View;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MustacheViewResolver}.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
public class MustacheViewResolverTests {
private final String prefix = "classpath:/"
+ getClass().getPackage().getName().replace(".", "/") + "/";
private MustacheViewResolver resolver = new MustacheViewResolver();
@Before
public void init() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.refresh();
this.resolver.setApplicationContext(applicationContext);
this.resolver.setServletContext(new MockServletContext());
this.resolver.setPrefix(this.prefix);
this.resolver.setSuffix(".html");
}
@Test
public void resolveNonExistent() throws Exception {
assertThat(this.resolver.resolveViewName("bar", null)).isNull();
}
@Test
public void resolveExisting() throws Exception {
assertThat(this.resolver.resolveViewName("template", null)).isNotNull();
}
@Test
public void setsContentType() throws Exception {
this.resolver.setContentType("application/octet-stream");
View view = this.resolver.resolveViewName("template", null);
assertThat(view.getContentType()).isEqualTo("application/octet-stream");
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.servlet.view;
import java.util.Collections;
import com.samskivert.mustache.Mustache;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MustacheView}.
*
* @author Dave Syer
*/
public class MustacheViewTests {
private final String templateUrl = "classpath:/"
+ getClass().getPackage().getName().replace(".", "/") + "/template.html";
private MockHttpServletRequest request = new MockHttpServletRequest();
private MockHttpServletResponse response = new MockHttpServletResponse();
private AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
@Before
public void init() {
this.context.refresh();
MockServletContext servletContext = new MockServletContext();
this.context.setServletContext(servletContext);
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
this.context);
}
@Test
public void viewResolvesHandlebars() throws Exception {
MustacheView view = new MustacheView();
view.setCompiler(Mustache.compiler());
view.setUrl(this.templateUrl);
view.setApplicationContext(this.context);
view.render(Collections.singletonMap("World", "Spring"), this.request,
this.response);
assertThat(this.response.getContentAsString()).isEqualTo("Hello Spring");
}
}