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() {