Update ScriptTemplateView to manage content type

This commit introduces the following changes:
 - Content type can now be properly configured
 - Default content type is "text/html"
 - Content type and charset are now properly set in the response

Issue: SPR-13379
This commit is contained in:
Sebastien Deleuze 2015-08-24 14:27:17 +02:00
parent 88405be8a5
commit 04cff89eb7
9 changed files with 143 additions and 20 deletions

View File

@ -67,6 +67,9 @@ public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimple
if (element.hasAttribute("render-function")) {
builder.addPropertyValue("renderFunction", element.getAttribute("render-function"));
}
if (element.hasAttribute("content-type")) {
builder.addPropertyValue("contentType", element.getAttribute("content-type"));
}
if (element.hasAttribute("charset")) {
builder.addPropertyValue("charset", Charset.forName(element.getAttribute("charset")));
}
@ -81,7 +84,8 @@ public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimple
@Override
protected boolean isEligibleAttribute(String name) {
return (name.equals("engine-name") || name.equals("scripts") || name.equals("render-object") ||
name.equals("render-function") || name.equals("charset") || name.equals("resource-loader-path"));
name.equals("render-function") || name.equals("content-type") ||
name.equals("charset") || name.equals("resource-loader-path"));
}
}

View File

@ -60,6 +60,12 @@ public interface ScriptTemplateConfig {
*/
String getRenderFunction();
/**
* Return the content type to use for the response.
* @since 4.2.1
*/
String getContentType();
/**
* Return the charset used to read script and template files.
*/

View File

@ -59,6 +59,8 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig {
private String renderFunction;
private String contentType;
private Charset charset;
private String resourceLoaderPath;
@ -170,6 +172,24 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig {
return this.renderFunction;
}
/**
* Set the content type to use for the response.
* ({@code text/html} by default).
* @since 4.2.1
*/
public void setContentType(String contentType) {
this.contentType = contentType;
}
/**
* Return the content type to use for the response.
* @since 4.2.1
*/
@Override
public String getContentType() {
return this.contentType;
}
/**
* Set the charset used to read script and template files.
* ({@code UTF-8} by default).

View File

@ -63,6 +63,8 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView;
*/
public class ScriptTemplateView extends AbstractUrlBasedView {
public static final String DEFAULT_CONTENT_TYPE = "text/html";
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:";
@ -89,6 +91,24 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
private String resourceLoaderPath;
/**
* Constructor for use as a bean.
* @see #setUrl
*/
public ScriptTemplateView() {
setContentType(null);
}
/**
* Create a new ScriptTemplateView with the given URL.
* @since 4.2.1
*/
public ScriptTemplateView(String url) {
super(url);
setContentType(null);
}
/**
* See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation.
*/
@ -132,6 +152,15 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
this.renderFunction = functionName;
}
/**
* See {@link ScriptTemplateConfigurer#setContentType(String)}} documentation.
* @since 4.2.1
*/
@Override
public void setContentType(String contentType) {
super.setContentType(contentType);
}
/**
* See {@link ScriptTemplateConfigurer#setCharset(Charset)} documentation.
*/
@ -167,6 +196,9 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
if (this.renderFunction == null && viewConfig.getRenderFunction() != null) {
this.renderFunction = viewConfig.getRenderFunction();
}
if (this.getContentType() == null) {
setContentType(viewConfig.getContentType() != null ? viewConfig.getContentType() : DEFAULT_CONTENT_TYPE);
}
if (this.charset == null) {
this.charset = (viewConfig.getCharset() != null ? viewConfig.getCharset() : DEFAULT_CHARSET);
}
@ -276,10 +308,17 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
}
}
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
super.prepareResponse(request, response);
setResponseContentType(request, response);
response.setCharacterEncoding(this.charset.name());
}
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
ScriptEngine engine = getEngine();

View File

@ -1252,6 +1252,13 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="content-type" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Set the content type to use for the response (text/html by default).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="charset" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[

View File

@ -799,6 +799,7 @@ public class MvcNamespaceTests {
ScriptTemplateConfigurer scriptTemplateConfigurer = appContext.getBean(ScriptTemplateConfigurer.class);
assertNotNull(scriptTemplateConfigurer);
assertEquals("render", scriptTemplateConfigurer.getRenderFunction());
assertEquals(MediaType.TEXT_PLAIN_VALUE, scriptTemplateConfigurer.getContentType());
assertEquals(StandardCharsets.ISO_8859_1, scriptTemplateConfigurer.getCharset());
assertEquals("classpath:", scriptTemplateConfigurer.getResourceLoaderPath());
assertFalse(scriptTemplateConfigurer.isSharedEngine());

View File

@ -20,7 +20,9 @@ import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@ -34,6 +36,13 @@ import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@ -51,7 +60,7 @@ public class ScriptTemplateViewTests {
private ScriptTemplateConfigurer configurer;
private StaticApplicationContext applicationContext;
private StaticWebApplicationContext wac;
private static final String RESOURCE_LOADER_PATH = "classpath:org/springframework/web/servlet/view/script/";
@ -59,10 +68,10 @@ public class ScriptTemplateViewTests {
@Before
public void setup() {
this.configurer = new ScriptTemplateConfigurer();
this.applicationContext = new StaticApplicationContext();
this.applicationContext.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.wac = new StaticWebApplicationContext();
this.wac.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer);
this.view = new ScriptTemplateView();
this.view.setUrl("sampleView");
this.view.setUrl(RESOURCE_LOADER_PATH + "empty.txt");
}
@Test
@ -83,13 +92,18 @@ public class ScriptTemplateViewTests {
this.configurer.setEngine(engine);
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setContentType(MediaType.TEXT_PLAIN_VALUE);
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
assertEquals(engine, accessor.getPropertyValue("engine"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(MediaType.TEXT_PLAIN_VALUE, accessor.getPropertyValue("contentType"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
assertEquals(true, accessor.getPropertyValue("sharedEngine"));
}
@Test
@ -97,16 +111,15 @@ public class ScriptTemplateViewTests {
this.configurer.setEngineName("nashorn");
this.configurer.setRenderObject("Template");
this.configurer.setRenderFunction("render");
this.configurer.setCharset(StandardCharsets.ISO_8859_1);
this.configurer.setSharedEngine(true);
DirectFieldAccessor accessor = new DirectFieldAccessor(this.view);
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
assertEquals("nashorn", accessor.getPropertyValue("engineName"));
assertNotNull(accessor.getPropertyValue("engine"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
assertEquals(true, accessor.getPropertyValue("sharedEngine"));
assertEquals(MediaType.TEXT_HTML_VALUE, accessor.getPropertyValue("contentType"));
assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("charset"));
}
@Test
@ -115,7 +128,7 @@ public class ScriptTemplateViewTests {
given(engine.get("key")).willReturn("value");
this.view.setEngine(engine);
this.view.setRenderFunction("render");
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
engine = this.view.getEngine();
assertNotNull(engine);
assertEquals("value", engine.get("key"));
@ -131,7 +144,7 @@ public class ScriptTemplateViewTests {
this.view.setEngineName("nashorn");
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Boolean>> results = new ArrayList<>();
for(int i = 0; i < iterations; i++) {
@ -160,7 +173,7 @@ public class ScriptTemplateViewTests {
public void noRenderFunctionDefined() {
this.view.setEngine(mock(InvocableScriptEngine.class));
try {
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException ex) {
@ -174,7 +187,7 @@ public class ScriptTemplateViewTests {
this.view.setEngineName("test");
this.view.setRenderFunction("render");
try {
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException ex) {
@ -188,7 +201,7 @@ public class ScriptTemplateViewTests {
this.view.setRenderFunction("render");
this.view.setSharedEngine(false);
try {
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException ex) {
@ -203,7 +216,7 @@ public class ScriptTemplateViewTests {
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setResourceLoaderPath(RESOURCE_LOADER_PATH);
this.view.setApplicationContext(this.applicationContext);
this.view.setApplicationContext(this.wac);
ClassLoader classLoader = this.view.createClassLoader();
assertNotNull(classLoader);
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
@ -219,6 +232,38 @@ public class ScriptTemplateViewTests {
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(1).toString(), Matchers.endsWith("org/springframework/web/servlet/view/"));
}
@Test // SPR-13379
public void contentType() throws Exception {
MockServletContext servletContext = new MockServletContext();
this.wac.setServletContext(servletContext);
this.wac.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.wac);
MockHttpServletResponse response = new MockHttpServletResponse();
Map<String, Object> model = new HashMap<String, Object>();
this.view.setEngine(mock(InvocableScriptEngine.class));
this.view.setRenderFunction("render");
this.view.setResourceLoaderPath(RESOURCE_LOADER_PATH);
this.view.setApplicationContext(this.wac);
this.view.render(model, request, response);
assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=" +
StandardCharsets.UTF_8, response.getHeader(HttpHeaders.CONTENT_TYPE));
response = new MockHttpServletResponse();
this.view.setContentType(MediaType.TEXT_PLAIN_VALUE);
this.view.render(model, request, response);
assertEquals(MediaType.TEXT_PLAIN_VALUE + ";charset=" +
StandardCharsets.UTF_8, response.getHeader(HttpHeaders.CONTENT_TYPE));
response = new MockHttpServletResponse();
this.view.setCharset(StandardCharsets.ISO_8859_1);
this.view.render(model, request, response);
assertEquals(MediaType.TEXT_PLAIN_VALUE + ";charset=" +
StandardCharsets.ISO_8859_1, response.getHeader(HttpHeaders.CONTENT_TYPE));
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}

View File

@ -34,7 +34,8 @@
<mvc:groovy-configurer resource-loader-path="/test" cache-templates="false" auto-indent="true" />
<mvc:script-template-configurer engine-name="nashorn" render-function="render" charset="ISO-8859-1"
<mvc:script-template-configurer engine-name="nashorn" render-function="render"
content-type="text/plain" charset="ISO-8859-1"
resource-loader-path="classpath:" shared-engine="false">
<mvc:script location="org/springframework/web/servlet/view/script/nashorn/render.js" />
</mvc:script-template-configurer>