Skip to content

Commit 1338d46

Browse files
committed
Add JSONP support for MappingJackson2MessageConverter
Issue: SPR-9899
1 parent 05e96ee commit 1338d46

File tree

8 files changed

+311
-40
lines changed

8 files changed

+311
-40
lines changed

spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,27 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage)
249249
if (this.jsonPrefix != null) {
250250
jsonGenerator.writeRaw(this.jsonPrefix);
251251
}
252+
Class<?> serializationView = null;
253+
String jsonpFunction = null;
252254
if (object instanceof MappingJacksonValue) {
253-
MappingJacksonValue valueHolder = (MappingJacksonValue) object;
254-
object = valueHolder.getValue();
255-
Class<?> serializationView = valueHolder.getSerializationView();
255+
MappingJacksonValue container = (MappingJacksonValue) object;
256+
object = container.getValue();
257+
serializationView = container.getSerializationView();
258+
jsonpFunction = container.getJsonpFunction();
259+
}
260+
if (jsonpFunction != null) {
261+
jsonGenerator.writeRaw(jsonpFunction + "(" );
262+
}
263+
if (serializationView != null) {
256264
this.objectMapper.writerWithView(serializationView).writeValue(jsonGenerator, object);
257265
}
258266
else {
259267
this.objectMapper.writeValue(jsonGenerator, object);
260268
}
269+
if (jsonpFunction != null) {
270+
jsonGenerator.writeRaw(");");
271+
jsonGenerator.flush();
272+
}
261273
}
262274
catch (JsonProcessingException ex) {
263275
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);

spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,83 @@
1717
package org.springframework.http.converter.json;
1818

1919
/**
20-
* Holds an Object to be serialized via Jackson together with a serialization
21-
* view to be applied.
20+
* A simple holder for the POJO to serialize via
21+
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
22+
* MappingJackson2HttpMessageConverter} along with further serialization
23+
* instructions to be passed in to the converter.
24+
*
25+
* <p>On the server side this wrapper is added with a
26+
* {@code ResponseBodyInterceptor} after content negotiation selects the
27+
* converter to use but before the write.
28+
*
29+
* <p>On the client side, simply wrap the POJO and pass it in to the
30+
* {@code RestTemplate}.
2231
*
2332
* @author Rossen Stoyanchev
2433
* @since 4.1
25-
*
26-
* @see com.fasterxml.jackson.annotation.JsonView
2734
*/
2835
public class MappingJacksonValue {
2936

30-
private final Object value;
37+
private Object value;
3138

32-
private final Class<?> serializationView;
39+
private Class<?> serializationView;
40+
41+
private String jsonpFunction;
3342

3443

3544
/**
36-
* Create a new instance.
45+
* Create a new instance wrapping the given POJO to be serialized.
3746
* @param value the Object to be serialized
38-
* @param serializationView the view to be applied
3947
*/
40-
public MappingJacksonValue(Object value, Class<?> serializationView) {
48+
public MappingJacksonValue(Object value) {
4149
this.value = value;
42-
this.serializationView = serializationView;
4350
}
4451

4552

4653
/**
47-
* Return the value to be serialized.
54+
* Modify the POJO to serialize.
55+
*/
56+
public void setValue(Object value) {
57+
this.value = value;
58+
}
59+
60+
/**
61+
* Return the POJO that needs to be serialized.
4862
*/
4963
public Object getValue() {
5064
return this.value;
5165
}
5266

67+
/**
68+
* Set the serialization view to serialize the POJO with.
69+
* @see com.fasterxml.jackson.databind.ObjectMapper#writerWithView(Class)
70+
* @see com.fasterxml.jackson.annotation.JsonView
71+
*/
72+
public void setSerializationView(Class<?> serializationView) {
73+
this.serializationView = serializationView;
74+
}
75+
5376
/**
5477
* Return the serialization view to use.
78+
* @see com.fasterxml.jackson.databind.ObjectMapper#writerWithView(Class)
79+
* @see com.fasterxml.jackson.annotation.JsonView
5580
*/
5681
public Class<?> getSerializationView() {
5782
return this.serializationView;
5883
}
5984

85+
/**
86+
* Set the name of the JSONP function name.
87+
*/
88+
public void setJsonpFunction(String functionName) {
89+
this.jsonpFunction = functionName;
90+
}
91+
92+
/**
93+
* Return the configured JSONP function name.
94+
*/
95+
public String getJsonpFunction() {
96+
return this.jsonpFunction;
97+
}
98+
6099
}

spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.fasterxml.jackson.annotation.JsonView;
2828
import com.fasterxml.jackson.databind.JavaType;
2929
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import org.hamcrest.CoreMatchers;
3031
import org.junit.Test;
3132

3233
import org.springframework.core.ParameterizedTypeReference;
@@ -35,6 +36,7 @@
3536
import org.springframework.http.MockHttpOutputMessage;
3637
import org.springframework.http.converter.HttpMessageNotReadableException;
3738

39+
import static org.hamcrest.CoreMatchers.*;
3840
import static org.junit.Assert.*;
3941

4042
/**
@@ -245,13 +247,48 @@ public void jsonView() throws Exception {
245247
bean.setWithView1("with");
246248
bean.setWithView2("with");
247249
bean.setWithoutView("without");
248-
MappingJacksonValue jsv = new MappingJacksonValue(bean, MyJacksonView1.class);
249-
this.converter.writeInternal(jsv, outputMessage);
250+
251+
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
252+
jacksonValue.setSerializationView(MyJacksonView1.class);
253+
this.converter.writeInternal(jacksonValue, outputMessage);
254+
255+
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
256+
assertThat(result, containsString("\"withView1\":\"with\""));
257+
assertThat(result, containsString("\"withoutView\":\"without\""));
258+
assertThat(result, not(containsString("\"withView2\":\"with\"")));
259+
}
260+
261+
@Test
262+
public void jsonp() throws Exception {
263+
MappingJacksonValue jacksonValue = new MappingJacksonValue("foo");
264+
jacksonValue.setSerializationView(MyJacksonView1.class);
265+
jacksonValue.setJsonpFunction("callback");
266+
267+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
268+
this.converter.writeInternal(jacksonValue, outputMessage);
269+
270+
assertEquals("callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
271+
}
272+
273+
@Test
274+
public void jsonpAndJsonView() throws Exception {
275+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
276+
JacksonViewBean bean = new JacksonViewBean();
277+
bean.setWithView1("with");
278+
bean.setWithView2("with");
279+
bean.setWithoutView("without");
280+
281+
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
282+
jacksonValue.setSerializationView(MyJacksonView1.class);
283+
jacksonValue.setJsonpFunction("callback");
284+
this.converter.writeInternal(jacksonValue, outputMessage);
250285

251286
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
252-
assertTrue(result.contains("\"withView1\":\"with\""));
253-
assertFalse(result.contains("\"withView2\":\"with\""));
254-
assertTrue(result.contains("\"withoutView\":\"without\""));
287+
assertThat(result, startsWith("callback("));
288+
assertThat(result, endsWith(");"));
289+
assertThat(result, containsString("\"withView1\":\"with\""));
290+
assertThat(result, containsString("\"withoutView\":\"without\""));
291+
assertThat(result, not(containsString("\"withView2\":\"with\"")));
255292
}
256293

257294

spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,9 @@ public void jsonPostForObjectWithJacksonView() throws URISyntaxException {
220220
HttpHeaders entityHeaders = new HttpHeaders();
221221
entityHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
222222
MySampleBean bean = new MySampleBean("with", "with", "without");
223-
MappingJacksonValue jsv = new MappingJacksonValue(bean, MyJacksonView1.class);
224-
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jsv);
223+
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
224+
jacksonValue.setSerializationView(MyJacksonView1.class);
225+
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jacksonValue);
225226
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
226227
assertTrue(s.contains("\"with1\":\"with\""));
227228
assertFalse(s.contains("\"with2\":\"with\""));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import org.springframework.core.MethodParameter;
20+
import org.springframework.http.MediaType;
21+
import org.springframework.http.converter.json.MappingJacksonValue;
22+
import org.springframework.http.server.ServerHttpRequest;
23+
import org.springframework.http.server.ServerHttpResponse;
24+
import org.springframework.http.server.ServletServerHttpRequest;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.CollectionUtils;
27+
28+
import javax.servlet.http.HttpServletRequest;
29+
import java.util.Collection;
30+
31+
/**
32+
* A convenient base class for a {@code ResponseBodyInterceptor} to instruct the
33+
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
34+
* MappingJackson2HttpMessageConverter} to serialize with JSONP formatting.
35+
*
36+
* <p>Sub-classes must specify the query parameter name(s) to check for the name
37+
* of the JSONP callback function.
38+
*
39+
* <p>Sub-classes are likely to be annotated with the {@code @ControllerAdvice}
40+
* annotation and auto-detected or otherwise must be registered directly with the
41+
* {@code RequestMappingHandlerAdapter} and {@code ExceptionHandlerExceptionResolver}.
42+
*
43+
* @author Rossen Stoyanchev
44+
* @since 4.1
45+
*/
46+
public abstract class AbstractJsonpResponseBodyInterceptor extends AbstractMappingJacksonResponseBodyInterceptor {
47+
48+
private final String[] jsonpQueryParamNames;
49+
50+
51+
protected AbstractJsonpResponseBodyInterceptor(Collection<String> queryParamNames) {
52+
Assert.isTrue(!CollectionUtils.isEmpty(queryParamNames), "At least one query param name is required");
53+
this.jsonpQueryParamNames = queryParamNames.toArray(new String[queryParamNames.size()]);
54+
}
55+
56+
57+
@Override
58+
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
59+
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
60+
61+
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
62+
63+
for (String name : this.jsonpQueryParamNames) {
64+
String value = servletRequest.getParameter(name);
65+
if (value != null) {
66+
MediaType contentTypeToUse = getContentType(contentType, request, response);
67+
response.getHeaders().setContentType(contentTypeToUse);
68+
bodyContainer.setJsonpFunction(value);
69+
return;
70+
}
71+
}
72+
}
73+
74+
/**
75+
* Return the content type to set the response to.
76+
* This implementation always returns "application/javascript".
77+
* @param contentType the content type selected through content negotiation
78+
* @param request the current request
79+
* @param response the current response
80+
* @return the content type to set the response to
81+
*/
82+
protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) {
83+
return new MediaType("application", "javascript");
84+
}
85+
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import org.springframework.core.MethodParameter;
20+
import org.springframework.http.MediaType;
21+
import org.springframework.http.converter.HttpMessageConverter;
22+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
23+
import org.springframework.http.converter.json.MappingJacksonValue;
24+
import org.springframework.http.server.ServerHttpRequest;
25+
import org.springframework.http.server.ServerHttpResponse;
26+
27+
/**
28+
* A convenient base class for {@code ResponseBodyInterceptor} implementations
29+
* that customize the response before JSON serialization with
30+
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
31+
* MappingJackson2HttpMessageConverter}.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 4.1
35+
*/
36+
public abstract class AbstractMappingJacksonResponseBodyInterceptor implements ResponseBodyInterceptor {
37+
38+
39+
protected AbstractMappingJacksonResponseBodyInterceptor() {
40+
}
41+
42+
43+
@SuppressWarnings("unchecked")
44+
@Override
45+
public final <T> T beforeBodyWrite(T body, MediaType contentType,
46+
Class<? extends HttpMessageConverter<T>> converterType,
47+
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
48+
49+
if (!MappingJackson2HttpMessageConverter.class.equals(converterType)) {
50+
return body;
51+
}
52+
MappingJacksonValue container = getOrCreateContainer(body);
53+
beforeBodyWriteInternal(container, contentType, returnType, request, response);
54+
return (T) container;
55+
}
56+
57+
/**
58+
* Wrap the body in a {@link MappingJacksonValue} value container (for providing
59+
* additional serialization instructions) or simply cast it if already wrapped.
60+
*/
61+
protected MappingJacksonValue getOrCreateContainer(Object body) {
62+
return (body instanceof MappingJacksonValue) ? (MappingJacksonValue) body : new MappingJacksonValue(body);
63+
}
64+
65+
/**
66+
* Invoked only if the converter type is {@code MappingJackson2HttpMessageConverter}.
67+
*/
68+
protected abstract void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
69+
MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response);
70+
71+
72+
}

0 commit comments

Comments
 (0)