From 5dc27ee134d28c7b25d0f6d3e9059f80c95d4402 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 27 May 2014 12:07:40 +0200 Subject: [PATCH] Add JSONP support to MappingJackson2JsonView Enable JSONP support by wrapping the JSON output into a callback when a JSONP query parameter specifying the function name to use as callback is detected. Default query parameter names recognized as JSONP ones are "jsonp" and "callback". This list can be customized if needed. This commit also fixes JSONView support by removing the view name specified in the model from the output. Issue: SPR-8346 --- .../view/json/MappingJackson2JsonView.java | 91 +++++++++++++++---- .../json/MappingJackson2JsonViewTests.java | 62 ++++++++++--- 2 files changed, 124 insertions(+), 29 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java index 44a54108cb..0a52556ca4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java @@ -19,10 +19,7 @@ package org.springframework.web.servlet.view.json; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -32,6 +29,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.validation.BindingResult; @@ -50,6 +48,7 @@ import org.springframework.web.servlet.view.AbstractView; * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 3.1.2 */ public class MappingJackson2JsonView extends AbstractView { @@ -60,6 +59,10 @@ public class MappingJackson2JsonView extends AbstractView { */ public static final String DEFAULT_CONTENT_TYPE = "application/json"; + public static final String DEFAULT_JSONP_CONTENT_TYPE = "application/javascript"; + + public static final String[] DEFAULT_JSONP_PARAMETER_NAMES = {"jsonp", "callback"}; + private ObjectMapper objectMapper = new ObjectMapper(); @@ -77,6 +80,8 @@ public class MappingJackson2JsonView extends AbstractView { private boolean updateContentLength = false; + private String[] jsonpParameterNames; + /** * Construct a new {@code MappingJackson2JsonView}, setting the content type to {@code application/json}. @@ -84,6 +89,7 @@ public class MappingJackson2JsonView extends AbstractView { public MappingJackson2JsonView() { setContentType(DEFAULT_CONTENT_TYPE); setExposePathVariables(false); + this.jsonpParameterNames = DEFAULT_JSONP_PARAMETER_NAMES; } @@ -236,6 +242,20 @@ public class MappingJackson2JsonView extends AbstractView { this.updateContentLength = updateContentLength; } + /** + * Set the names of the request parameters recognized as JSONP ones. + * Each time a request has one of those parameters, the resulting JSON will + * be wrapped into a function named as specified by the JSONP parameter value. + * + * Default JSONP parameter names are "jsonp" and "callback". + * + * @since 4.1 + * @see JSONP Wikipedia article + */ + public void setJsonpParameterNames(Collection jsonpParameterNames) { + Assert.isTrue(!CollectionUtils.isEmpty(jsonpParameterNames), "At least one JSONP query parameter name is required"); + this.jsonpParameterNames = jsonpParameterNames.toArray(new String[jsonpParameterNames.size()]); + } @Override protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { @@ -248,23 +268,49 @@ public class MappingJackson2JsonView extends AbstractView { } } + @Override + protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) { + if (getJsonpParameterValue(request) != null) { + response.setContentType(DEFAULT_JSONP_CONTENT_TYPE); + } + else { + super.setResponseContentType(request, response); + } + } + @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); + + Class serializationView = (Class)model.get(JsonView.class.getName()); + String jsonpParameterValue = getJsonpParameterValue(request); Object value = filterModel(model); - if (model.containsKey(JsonView.class.getName())) { - writeContent(stream, value, this.jsonPrefix, model); - } - else { - writeContent(stream, value, this.jsonPrefix); + if(serializationView != null || jsonpParameterValue != null) { + MappingJacksonValue container = new MappingJacksonValue(value); + container.setSerializationView(serializationView); + container.setJsonpFunction(jsonpParameterValue); + value = container; } + + writeContent(stream, value, this.jsonPrefix); if (this.updateContentLength) { writeToResponse(response, (ByteArrayOutputStream) stream); } } + private String getJsonpParameterValue(HttpServletRequest request) { + String jsonpParameterValue = null; + for(String jsonpParameterName : this.jsonpParameterNames) { + jsonpParameterValue = request.getParameter(jsonpParameterName); + if(jsonpParameterValue != null) { + break; + } + } + return jsonpParameterValue; + } + /** * Filter out undesired attributes from the given model. * The return value can be either another {@link Map} or a single value object. @@ -277,7 +323,9 @@ public class MappingJackson2JsonView extends AbstractView { Map result = new HashMap(model.size()); Set renderedAttributes = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); for (Map.Entry entry : model.entrySet()) { - if (!(entry.getValue() instanceof BindingResult) && renderedAttributes.contains(entry.getKey())) { + if (!(entry.getValue() instanceof BindingResult) + && renderedAttributes.contains(entry.getKey()) + && !entry.getKey().equals(JsonView.class.getName())) { result.put(entry.getKey(), entry.getValue()); } } @@ -292,11 +340,7 @@ public class MappingJackson2JsonView extends AbstractView { * (as indicated through {@link #setJsonPrefix}/{@link #setPrefixJson}) * @throws IOException if writing failed */ - protected void writeContent(OutputStream stream, Object value, String jsonPrefix) throws IOException { - writeContent(stream, value, jsonPrefix, Collections.emptyMap()); - } - - protected void writeContent(OutputStream stream, Object value, String jsonPrefix, Map model) + protected void writeContent(OutputStream stream, Object value, String jsonPrefix) throws IOException { // The following has been deprecated as late as Jackson 2.2 (April 2013); @@ -313,14 +357,27 @@ public class MappingJackson2JsonView extends AbstractView { if (jsonPrefix != null) { generator.writeRaw(jsonPrefix); } - - Class serializationView = (Class) model.get(JsonView.class.getName()); + Class serializationView = null; + String jsonpFunction = null; + if (value instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) value; + value = container.getValue(); + serializationView = container.getSerializationView(); + jsonpFunction = container.getJsonpFunction(); + } + if (jsonpFunction != null) { + generator.writeRaw(jsonpFunction + "(" ); + } if (serializationView != null) { this.objectMapper.writerWithView(serializationView).writeValue(generator, value); } else { this.objectMapper.writeValue(generator, value); } + if (jsonpFunction != null) { + generator.writeRaw(");"); + generator.flush(); + } } } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java index c0742bfd36..165af2c55f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java @@ -17,11 +17,7 @@ package org.springframework.web.servlet.view.json; import java.io.IOException; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; @@ -54,6 +50,7 @@ import static org.mockito.Mockito.*; * @author Jeremy Grelle * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class MappingJackson2JsonViewTests { @@ -290,7 +287,48 @@ public class MappingJackson2JsonViewTests { assertTrue(content.length() > 0); assertEquals(content.length(), response.getContentLength()); assertTrue(content.contains("foo")); - assertFalse(content.contains("42")); + assertFalse(content.contains("boo")); + assertFalse(content.contains(JsonView.class.getName())); + } + + @Test + public void renderWithJsonpDefaultParameterName() throws Exception { + Map model = new HashMap(); + model.put("foo", "bar"); + request.addParameter("otherparam", "value"); + request.addParameter("jsonp", "jsonpCallback"); + + view.render(model, request, response); + + String content = response.getContentAsString(); + assertEquals("jsonpCallback({\"foo\":\"bar\"});", content); + } + + @Test + public void renderWithCallbackDefaultParameterName() throws Exception { + Map model = new HashMap(); + model.put("foo", "bar"); + request.addParameter("otherparam", "value"); + request.addParameter("callback", "jsonpCallback"); + + view.render(model, request, response); + + String content = response.getContentAsString(); + assertEquals("jsonpCallback({\"foo\":\"bar\"});", content); + } + + @Test + public void renderWithCustomJsonpParameterName() throws Exception { + Map model = new HashMap(); + model.put("foo", "bar"); + request.addParameter("otherparam", "value"); + request.addParameter("custom", "jsonpCallback"); + view.setJsonpParameterNames(Arrays.asList("jsonp", "callback", "custom")); + + view.render(model, request, response); + + String content = response.getContentAsString(); + assertEquals("jsonpCallback({\"foo\":\"bar\"});", content); } private void validateResult() throws Exception { @@ -307,25 +345,25 @@ public class MappingJackson2JsonViewTests { public static class TestBeanSimple { @JsonView(MyJacksonView1.class) - private String value = "foo"; + private String property1 = "foo"; private boolean test = false; @JsonView(MyJacksonView2.class) - private long number = 42; + private String property2 = "boo"; private TestChildBean child = new TestChildBean(); - public String getValue() { - return value; + public String getProperty1() { + return property1; } public boolean getTest() { return test; } - public long getNumber() { - return number; + public String getProperty2() { + return property2; } public Date getNow() {