From d2e5c4b28fc8bc5dd003094323b23940a08cf9c3 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 1 Jul 2016 12:11:52 +0200 Subject: [PATCH] #202 - Added example for JSONPath and XPath based payload binding to projection interfaces. See the readme for details. --- README.md | 1 + web/pom.xml | 1 + web/projection/README.md | 34 +++++ web/projection/pom.xml | 55 ++++++++ .../main/java/example/users/Application.java | 30 +++++ .../java/example/users/UserController.java | 118 ++++++++++++++++++ .../src/main/resources/application.properties | 2 + .../users/UserControllerClientTests.java | 105 ++++++++++++++++ .../users/UserControllerIntegrationTests.java | 72 +++++++++++ 9 files changed, 418 insertions(+) create mode 100644 web/projection/README.md create mode 100644 web/projection/pom.xml create mode 100644 web/projection/src/main/java/example/users/Application.java create mode 100644 web/projection/src/main/java/example/users/UserController.java create mode 100644 web/projection/src/main/resources/application.properties create mode 100644 web/projection/src/test/java/example/users/UserControllerClientTests.java create mode 100644 web/projection/src/test/java/example/users/UserControllerIntegrationTests.java diff --git a/README.md b/README.md index 19ba9a1e..a9226a21 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ We have separate folders for the samples of individual modules: * `web` - Example for Spring Data web integration (binding `Pageable` instances to Spring MVC controller methods, using interfaces to bind Spring MVCrequest payloads). * `querydsl` - Example for Spring Data Querydsl web integration (creating a `Predicate` from web requests). +* `projections` - Example for Spring Data web support for JSONPath and XPath expressions on projection interfaces. ## Miscellaneous diff --git a/web/pom.xml b/web/pom.xml index 68c19af4..b34feaf2 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -18,6 +18,7 @@ example querydsl + projection diff --git a/web/projection/README.md b/web/projection/README.md new file mode 100644 index 00000000..a128376c --- /dev/null +++ b/web/projection/README.md @@ -0,0 +1,34 @@ +# Spring Data Web - Projection example + +This example shows how to use projection interfaces in combination with JSON Path or XPath expressions to bind payload data to an object instance. + +The most interesting bit is the following projection interface declared in `UserController`: + +```java +@ProjectedPayload +public interface UserPayload { + + @XBRead("//firstname") + @JsonPath("$..firstname") + String getFirstname(); + + @XBRead("//lastname") + @JsonPath("$..lastname") + String getLastname(); +} +``` + +This type is used in `UserController.index(…)` and basically combines two modes of operation. + +## Binding request data via JSON Path expression + +The `@JsonPath` annotations bind the values obtained by evaluating the expressions from the request. The sample is using a recursive property lookup for `firstname` and `lastname`. Using those expressions allows the payload thats received to slightly changed and the code dealing with not having to be changed. + +As an example using that on the server side can be found in `UserControllerIntegrationTests`. The tests sends two different flavors of JSON to simulate a change in behavior of the client and the server can handle both representation formats without the need for a change. + +This is also very useful on the client side which is simulated in `UserControllerClientTests` setting up a `RestTemplate` with the newly introduced `HttpMessageConverters` so that the projection interface can be used to access the payload. See how the test case accesses different HTTP resources, that simulate a change in the representation on the server side. + +## Support for XPath expressions + +The JSON Path support is automatically activated on the server side if the Jayway JSON Path library is on the classpath. Support for XPath is activated if XMLBeam is on the classpath, generally works the same and actually offers quite some more sophisticated features. + diff --git a/web/projection/pom.xml b/web/projection/pom.xml new file mode 100644 index 00000000..232a4231 --- /dev/null +++ b/web/projection/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + spring-data-web-projection + + Spring Data - JSON and XML projection web example + + + org.springframework.data.examples + spring-data-web-examples + 1.0.0.BUILD-SNAPSHOT + + + + Ingalls-BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.data + spring-data-commons + + + + com.jayway.jsonpath + json-path + + + + org.xmlbeam + xmlprojector + 1.4.8 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/web/projection/src/main/java/example/users/Application.java b/web/projection/src/main/java/example/users/Application.java new file mode 100644 index 00000000..4a528f70 --- /dev/null +++ b/web/projection/src/main/java/example/users/Application.java @@ -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); + } +} diff --git a/web/projection/src/main/java/example/users/UserController.java b/web/projection/src/main/java/example/users/UserController.java new file mode 100644 index 00000000..08b67b6e --- /dev/null +++ b/web/projection/src/main/java/example/users/UserController.java @@ -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 = "DaveMatthews"; + + /** + * Receiving POST requests supporting both JSON and XML. + * + * @param user + * @return + */ + @PostMapping(value = "/") + HttpEntity 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 getJson() { + + Map 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 getChangedJson() { + return Collections.singletonMap("user", getJson()); + } + + /** + * Returns a simple XML payload. + * + * @return + */ + @GetMapping(path = "/", produces = MediaType.APPLICATION_XML_VALUE) + String getXml() { + return "".concat(XML_PAYLOAD).concat(""); + } + + /** + * 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 "".concat(XML_PAYLOAD).concat(""); + } + + /** + * 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(); + } +} diff --git a/web/projection/src/main/resources/application.properties b/web/projection/src/main/resources/application.properties new file mode 100644 index 00000000..7ea71449 --- /dev/null +++ b/web/projection/src/main/resources/application.properties @@ -0,0 +1,2 @@ +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.boot=DEBUG diff --git a/web/projection/src/test/java/example/users/UserControllerClientTests.java b/web/projection/src/test/java/example/users/UserControllerClientTests.java new file mode 100644 index 00000000..94e51064 --- /dev/null +++ b/web/projection/src/test/java/example/users/UserControllerClientTests.java @@ -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(headers), UserPayload.class).getBody(); + } + + private static void assertDave(UserPayload payload) { + + assertThat(payload.getFirstname(), is("Dave")); + assertThat(payload.getLastname(), is("Matthews")); + } +} diff --git a/web/projection/src/test/java/example/users/UserControllerIntegrationTests.java b/web/projection/src/test/java/example/users/UserControllerIntegrationTests.java new file mode 100644 index 00000000..10d435dd --- /dev/null +++ b/web/projection/src/test/java/example/users/UserControllerIntegrationTests.java @@ -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("DaveMatthews", 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"))); + } +}