DATACMNS-885 - Support for binding JSON payloads to projection interfaces.

Projection types annotated with @ProjectedPayload can now be used as parameters for @RequestBody annotated Spring MVC controller method parameters.

Accessor methods will be translated into JSON path property expressions which can be customized by using the @JsonPath annotation. The methods are allowed to return simple types, nested projection interfaces or complex classes which will will be mapped using a Jackson ObjectMapper.
This commit is contained in:
Oliver Gierke
2016-07-17 12:32:59 +02:00
parent 86c1086976
commit eec2b43fd0
14 changed files with 952 additions and 78 deletions

View File

@@ -0,0 +1,227 @@
/*
* 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 org.springframework.data.web;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
/**
* Unit tests for {@link JsonProjectingMethodInterceptorFactory}.
*
* @author Oliver Gierke
* @since 1.13
* @soundtrack Richard Spaven - Assemble (Whole Other*)
*/
public class JsonProjectingMethodInterceptorFactoryUnitTests {
ProjectionFactory projectionFactory;
Customer customer;
@Before
public void setUp() {
String json = "{\"firstname\" : \"Dave\", "//
+ "\"address\" : { \"zipCode\" : \"01097\", \"city\" : \"Dresden\" }," //
+ "\"addresses\" : [ { \"zipCode\" : \"01097\", \"city\" : \"Dresden\" }]" + " }";
SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
MappingProvider mappingProvider = new JacksonMappingProvider(new ObjectMapper());
projectionFactory.registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory(mappingProvider));
this.projectionFactory = projectionFactory;
this.customer = projectionFactory.createProjection(Customer.class, new ByteArrayInputStream(json.getBytes()));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessSimpleProperty() {
assertThat(customer.getFirstname(), is("Dave"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessPropertyWithExplicitAnnotation() {
assertThat(customer.getBar(), is("Dave"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessPropertyWithComplexReturnType() {
assertThat(customer.getAddress(), is(new Address("01097", "Dresden")));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessComplexPropertyWithProjection() {
assertThat(customer.getAddressProjection().getCity(), is("Dresden"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessPropertyWithNestedJsonPath() {
assertThat(customer.getNestedZipCode(), is("01097"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessCollectionProperty() {
assertThat(customer.getAddresses().get(0), is(new Address("01097", "Dresden")));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessPropertyOnNestedProjection() {
assertThat(customer.getAddressProjections().get(0).getZipCode(), is("01097"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessPropertyThatUsesJsonPathProjectionInTurn() {
assertThat(customer.getAnotherAddressProjection().getZipCodeButNotCity(), is("01097"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessCollectionPropertyThatUsesJsonPathProjectionInTurn() {
List<AnotherAddressProjection> projections = customer.getAnotherAddressProjections();
assertThat(projections, hasSize(1));
assertThat(projections.get(0).getZipCodeButNotCity(), is("01097"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessAsCollectionPropertyThatUsesJsonPathProjectionInTurn() {
Set<AnotherAddressProjection> projections = customer.getAnotherAddressProjectionAsCollection();
assertThat(projections, hasSize(1));
assertThat(projections.iterator().next().getZipCodeButNotCity(), is("01097"));
}
/**
* @see DATCMNS-885
*/
@Test
public void accessNestedPropertyButStayOnRootLevel() {
Name name = customer.getName();
assertThat(name, is(notNullValue()));
assertThat(name.getFirstname(), is("Dave"));
}
interface Customer {
String getFirstname();
@JsonPath("$")
Name getName();
Address getAddress();
List<Address> getAddresses();
@JsonPath("$.addresses")
List<AddressProjection> getAddressProjections();
@JsonPath("$.firstname")
String getBar();
@JsonPath("$.address")
AddressProjection getAddressProjection();
@JsonPath("$.address.zipCode")
String getNestedZipCode();
@JsonPath("$.address")
AnotherAddressProjection getAnotherAddressProjection();
@JsonPath("$.addresses")
List<AnotherAddressProjection> getAnotherAddressProjections();
@JsonPath("$.address")
Set<AnotherAddressProjection> getAnotherAddressProjectionAsCollection();
}
interface AddressProjection {
String getZipCode();
String getCity();
}
interface Name {
@JsonPath("$.firstname")
String getFirstname();
@JsonPath("$.lastname")
String getLastname();
}
interface AnotherAddressProjection {
@JsonPath("$.zipCode")
String getZipCodeButNotCity();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Address {
private String zipCode, city;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 org.springframework.data.web;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.http.MediaType;
/**
* Unit tests for {@link ProjectingJackson2HttpMessageConverter}.
*
* @author Oliver Gierke
* @soundtrack Richard Spaven - Tribute (Whole Other*)
* @since 1.13
*/
public class ProjectingJackson2HttpMessageConverterUnitTests {
ProjectingJackson2HttpMessageConverter converter = new ProjectingJackson2HttpMessageConverter();
MediaType ANYTHING_JSON = MediaType.parseMediaType("application/*+json");
/**
* @see DATCMNS-885
*/
@Test
public void canReadJsonIntoAnnotatedInterface() {
assertThat(converter.canRead(SampleInterface.class, ANYTHING_JSON), is(true));
}
/**
* @see DATCMNS-885
*/
@Test
public void cannotReadUnannotatedInterface() {
assertThat(converter.canRead(UnannotatedInterface.class, ANYTHING_JSON), is(false));
}
/**
* @see DATCMNS-885
*/
@Test
public void cannotReadClass() {
assertThat(converter.canRead(SampleClass.class, ANYTHING_JSON), is(false));
}
@ProjectedPayload
interface SampleInterface {}
interface UnannotatedInterface {}
class SampleClass {}
}