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 44a54108cb0..0a52556ca4e 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 c0742bfd365..165af2c55f6 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() {