#202 - Added example for JSONPath and XPath based payload binding to projection interfaces.

See the readme for details.
This commit is contained in:
Oliver Gierke
2016-07-01 12:11:52 +02:00
parent ecafda00ca
commit d2e5c4b28f
9 changed files with 418 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Oliver Gierke
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.web.JsonPath;
import org.springframework.data.web.ProjectedPayload;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.xmlbeam.annotation.XBRead;
/**
* Controller to handle web requests for {@link UserPayload}s.
*
* @author Oliver Gierke
*/
@RestController
class UserController {
private static String XML_PAYLOAD = "<firstname>Dave</firstname><lastname>Matthews</lastname>";
/**
* Receiving POST requests supporting both JSON and XML.
*
* @param user
* @return
*/
@PostMapping(value = "/")
HttpEntity<String> post(@RequestBody UserPayload user) {
return ResponseEntity
.ok(String.format("Received firstname: %s, lastname: %s", user.getFirstname(), user.getLastname()));
}
/**
* Returns a simple JSON payload.
*
* @return
*/
@GetMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
Map<String, Object> getJson() {
Map<String, Object> result = new HashMap<>();
result.put("firstname", "Dave");
result.put("lastname", "Matthews");
return result;
}
/**
* Returns the payload of {@link #getJson()} wrapped into another element to simulate a change in the representation.
*
* @return
*/
@GetMapping(path = "/changed", produces = MediaType.APPLICATION_JSON_VALUE)
Map<String, Object> getChangedJson() {
return Collections.singletonMap("user", getJson());
}
/**
* Returns a simple XML payload.
*
* @return
*/
@GetMapping(path = "/", produces = MediaType.APPLICATION_XML_VALUE)
String getXml() {
return "<user>".concat(XML_PAYLOAD).concat("</user>");
}
/**
* Returns the payload of {@link #getXml()} wrapped into another XML element to simulate a change in the
* representation structure.
*
* @return
*/
@GetMapping(path = "/changed", produces = MediaType.APPLICATION_XML_VALUE)
String getChangedXml() {
return "<user><username>".concat(XML_PAYLOAD).concat("</username></user>");
}
/**
* The projection interface using XPath and JSON Path expression to selectively pick elements from the payload.
*
* @author Oliver Gierke
*/
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("//lastname")
@JsonPath("$..lastname")
String getLastname();
}
}

View File

@@ -0,0 +1,2 @@
logging.level.org.springframework.web=DEBUG
logging.level.org.springframework.boot=DEBUG

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import example.users.UserController.UserPayload;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.web.ProjectingJackson2HttpMessageConverter;
import org.springframework.data.web.XmlBeamHttpMessageConverter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Integration tests for {@link UserController} to demonstrate client-side resilience of the payload type against
* changes in the representation.
*
* @author Oliver Gierke
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerClientTests {
@Autowired TestRestTemplate template;
/**
* Custom configuration for the test to enrich the {@link TestRestTemplate} with the {@link HttpMessageConverter}s for
* XML and JSON projections.
*
* @author Oliver Gierke
*/
@Configuration
@Import(Application.class)
static class Config {
@Bean
RestTemplateBuilder builder() {
return new RestTemplateBuilder()//
.additionalMessageConverters(new ProjectingJackson2HttpMessageConverter())//
.additionalMessageConverters(new XmlBeamHttpMessageConverter());
}
}
@Test
public void accessJsonFieldsOnSimplePayload() {
assertDave(issueGet("/", MediaType.APPLICATION_JSON));
}
@Test
public void accessJsonFieldsOnNestedPayload() {
assertDave(issueGet("/changed", MediaType.APPLICATION_JSON));
}
@Test
public void accessXmlElementsOnSimplePayload() {
assertDave(issueGet("/", MediaType.APPLICATION_XML));
}
@Test
public void accessXmlElementsOnNestedPayload() {
assertDave(issueGet("/changed", MediaType.APPLICATION_XML));
}
private UserPayload issueGet(String path, MediaType mediaType) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT, mediaType.toString());
return template.exchange(path, HttpMethod.GET, new HttpEntity<Void>(headers), UserPayload.class).getBody();
}
private static void assertDave(UserPayload payload) {
assertThat(payload.getFirstname(), is("Dave"));
assertThat(payload.getLastname(), is("Matthews"));
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.users;
import static org.hamcrest.CoreMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
/**
* Integration tests for {@link UserController}.
*
* @author Oliver Gierke
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc(alwaysPrint = false)
public class UserControllerIntegrationTests {
@Autowired MockMvc mvc;
@Test
public void handlesJsonPayloadWithExactProperties() throws Exception {
postAndExpect("{ \"firstname\" : \"Dave\", \"lastname\" : \"Matthews\" }", MediaType.APPLICATION_JSON);
}
@Test
public void handlesJsonPayloadWithNestedProperties() throws Exception {
postAndExpect("{ \"user\" : { \"firstname\" : \"Dave\", \"lastname\" : \"Matthews\" } }",
MediaType.APPLICATION_JSON);
}
@Test
public void handlesXmlPayLoadWithExactProperties() throws Exception {
postAndExpect("<user><firstname>Dave</firstname><lastname>Matthews</lastname></user>", MediaType.APPLICATION_XML);
}
private void postAndExpect(String payload, MediaType mediaType) throws Exception {
ResultActions actions = mvc
.perform(post("/")//
.content(payload)//
.contentType(mediaType))//
.andExpect(status().isOk());
actions.andExpect(content().string(containsString("Dave")));
actions.andExpect(content().string(containsString("Matthews")));
}
}