From 1f0b9bd6649c090cd9260d7aabbf4563ea1c55e7 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 14 Jan 2014 21:50:52 -0600 Subject: [PATCH] DATAREST-219 - Make editing collection-based properties more efficient This PR is inspired by #86, but had to be written from scratch because it was over 10 months old when this was undertaken. It was unmergeable. Essentially, by casting existing collections and then iterating over them and making adjustments should render more efficient back end database operations. Only for a PUT should a new collection be created. This PR not only updates these operations, but provides test cases to back up these edits, i.e. a test case for POSTing to a collection, PUTing to a collection, and DELETE-ing from a collection. Adds ability to return an empty response with only the Location header populated by looking at the property update's incoming URL. Original pull request: #128, #86. --- .../data/rest/webmvc/ControllerUtils.java | 31 ++- ...RepositoryPropertyReferenceController.java | 63 ++++-- .../convert/UriListHttpMessageConverter.java | 29 ++- .../webmvc/AbstractWebIntegrationTests.java | 72 +++++- .../data/rest/webmvc/jpa/JpaWebTests.java | 210 +++++++++++++++++- 5 files changed, 373 insertions(+), 32 deletions(-) diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java index 2183d96be..d1c479ecc 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -27,15 +27,23 @@ import org.springframework.http.ResponseEntity; /** * @author Oliver Gierke + * @author Greg Turnquist */ public class ControllerUtils { - public static final Resource EMPTY_RESOURCE = new Resource(new Object()); public static final Resources> EMPTY_RESOURCES = new Resources>( Collections.> emptyList()); public static final Iterable> EMPTY_RESOURCE_LIST = Collections.emptyList(); public static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); + /** + * Wrap a resource as a {@link ResourceEntity} and attach given headers and status. + * @param headers + * @param resource + * @param status + * @param + * @return + */ public static ResponseEntity toResponseEntity(HttpHeaders headers, R resource, HttpStatus status) { @@ -46,4 +54,23 @@ public class ControllerUtils { return new ResponseEntity(resource, hdrs, status); } + + /** + * Return an empty response that is only comprised of a status + * @param status + * @return + */ + public static ResponseEntity toEmptyResponse(HttpStatus status) { + return toResponseEntity(null, EMPTY_RESOURCES, status); + } + + /** + * Return an empty response that is only comprised of headers and a status + * @param headers + * @param status + * @return + */ + public static ResponseEntity toEmptyResponse(HttpHeaders headers, HttpStatus status) { + return toResponseEntity(headers, EMPTY_RESOURCES, status); + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index 88381f3ac..299a616e6 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2014 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. @@ -18,9 +18,11 @@ package org.springframework.data.rest.webmvc; import static org.springframework.data.rest.webmvc.ControllerUtils.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; +import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -58,10 +60,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.HandlerMapping; /** * @author Jon Brisbin * @author Oliver Gierke + * @author Greg Turnquist */ @RepositoryRestController @SuppressWarnings({ "unchecked" }) @@ -188,7 +192,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } } - return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.GET, produces = { "application/json", @@ -291,11 +295,13 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return ControllerUtils.toResponseEntity(null, new Resource(EMPTY_RESOURCE_LIST, links), HttpStatus.OK); } - @RequestMapping(value = BASE_MAPPING, method = { RequestMethod.POST, RequestMethod.PUT }, consumes = { - "application/json", "application/x-spring-data-compact+json", "text/uri-list" }) + @RequestMapping(value = BASE_MAPPING, // + method = { RequestMethod.POST, RequestMethod.PUT }, // + consumes = { "application/json", "application/x-spring-data-compact+json", "text/uri-list" }) @ResponseBody public ResponseEntity createPropertyReference(final RepositoryRestRequest repoRequest, - final @RequestBody Resource incoming, @PathVariable String id, @PathVariable String property) + final @RequestBody Resources incoming, @PathVariable String id, @PathVariable String property, + final HttpServletRequest request) throws NoSuchMethodException { final RepositoryInvoker invoker = repoRequest.getRepositoryInvoker(); @@ -310,12 +316,16 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes if (prop.property.isCollectionLike()) { - Collection coll = new ArrayList(); + Collection coll; + // Either load the exist collection to add to it (POST) if (HttpMethod.POST.equals(repoRequest.getRequestMethod())) { - coll.addAll((Collection) prop.propertyValue); + coll = (Collection) prop.propertyValue; + } else { // Or start from an empty collection to replace it (PUT) + coll = new ArrayList(); } + // Add to the existing collection for (Link l : incoming.getLinks()) { Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); coll.add(propVal); @@ -325,12 +335,16 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } else if (prop.property.isMap()) { - Map m = new HashMap(); + Map m; + // Either load the exist collection to add to it (POST) if (HttpMethod.POST.equals(repoRequest.getRequestMethod())) { - m.putAll((Map) prop.propertyValue); + m = (Map) prop.propertyValue; + } else { // Or start from an empty collection to replace it (PUT) + m = new HashMap(); } + // Add to the existing collection for (Link l : incoming.getLinks()) { Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); m.put(l.getRel(), propVal); @@ -364,7 +378,10 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes doWithReferencedProperty(repoRequest, id, property, handler); - return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.CREATED); + final HttpHeaders headers = new HttpHeaders(); + headers.set("Location", String.valueOf(request.getRequestURL())); + + return ControllerUtils.toEmptyResponse(headers, HttpStatus.CREATED); } @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.DELETE) @@ -389,25 +406,27 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } if (prop.property.isCollectionLike()) { - Collection coll = new ArrayList(); - for (Object obj : (Collection) prop.propertyValue) { + Collection coll = (Collection) prop.propertyValue; + Iterator itr = coll.iterator(); + while (itr.hasNext()) { + Object obj = itr.next(); BeanWrapper propValWrapper = BeanWrapper.create(obj, null); String s = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); - if (!propertyId.equals(s)) { - coll.add(obj); + if (propertyId.equals(s)) { + itr.remove(); } } - prop.wrapper.setProperty(prop.property, coll); } else if (prop.property.isMap()) { - Map m = new HashMap(); - for (Map.Entry entry : ((Map) prop.propertyValue).entrySet()) { - BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), null); + Map m = (Map) prop.propertyValue; + Iterator itr = m.keySet().iterator(); + while (itr.hasNext()) { + Object key = itr.next(); + BeanWrapper propValWrapper = BeanWrapper.create(m.get(key), null); String s = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); - if (!propertyId.equals(s)) { - m.put(entry.getKey(), entry.getValue()); + if (propertyId.equals(s)) { + itr.remove(); } } - prop.wrapper.setProperty(prop.property, m); } else { prop.wrapper.setProperty(prop.property, null); } @@ -422,7 +441,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes doWithReferencedProperty(repoRequest, id, property, handler); - return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } private Object loadPropertyValue(Class type, String href) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java index 3b46199fc..36272d535 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/convert/UriListHttpMessageConverter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2014 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.rest.webmvc.convert; import java.io.BufferedReader; @@ -10,7 +25,8 @@ import java.util.Collections; import java.util.List; import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.Resources; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -20,8 +36,9 @@ import org.springframework.http.converter.HttpMessageNotWritableException; /** * @author Jon Brisbin + * @author Greg Turnquist */ -public class UriListHttpMessageConverter implements HttpMessageConverter> { +public class UriListHttpMessageConverter implements HttpMessageConverter { private static final List MEDIA_TYPES = new ArrayList(); @@ -34,7 +51,7 @@ public class UriListHttpMessageConverter implements HttpMessageConverter read(Class> clazz, HttpInputMessage inputMessage) throws IOException, + public ResourceSupport read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { List links = new ArrayList(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody())); @@ -56,11 +73,11 @@ public class UriListHttpMessageConverter implements HttpMessageConverter(Collections.emptyList(), links); + return new Resources(Collections.emptyList(), links); } @Override - public void write(Resource resource, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, + public void write(ResourceSupport resource, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputMessage.getBody())); for (Link link : resource.getLinks()) { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index f340d83a2..8173ae5cc 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -50,6 +50,7 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; import com.jayway.jsonpath.InvalidPathException; @@ -57,6 +58,7 @@ import com.jayway.jsonpath.JsonPath; /** * @author Oliver Gierke + * @author Greg Turnquist */ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @@ -134,6 +136,74 @@ public abstract class AbstractWebIntegrationTests { return assertHasLinkWithRel(rel, response); } + protected MockHttpServletResponse postAndGet(Link link, Object payload, MediaType mediaType) throws Exception { + + String href; + if (link.isTemplated()) { + href = link.expand().getHref(); + } else { + href = link.getHref(); + } + + MockHttpServletResponse response = mvc.perform(post(href).content(payload.toString()).contentType(mediaType)).// + andExpect(status().isCreated()).// + andExpect(header().string("Location", is(notNullValue()))).// + andReturn().getResponse(); + + String content = response.getContentAsString(); + + if (StringUtils.hasText(content)) { + return response; + } + + return request(response.getHeader("Location")); + } + + protected MockHttpServletResponse putAndGet(Link link, Object payload, MediaType mediaType) throws Exception { + + String href; + if (link.isTemplated()) { + href = link.expand().getHref(); + } else { + href = link.getHref(); + } + + MockHttpServletResponse response = mvc.perform(put(href).content(payload.toString()).contentType(mediaType)).// + andExpect(status().isCreated()).// + andExpect(header().string("Location", is(notNullValue()))).// + andReturn().getResponse(); + + String content = response.getContentAsString(); + + if (StringUtils.hasText(content)) { + return response; + } + + return request(response.getHeader("Location")); + } + + protected MockHttpServletResponse deleteAndGet(Link link, MediaType mediaType) throws Exception { + + String href; + if (link.isTemplated()) { + href = link.expand().getHref(); + } else { + href = link.getHref(); + } + + MockHttpServletResponse response = mvc.perform(delete(href).contentType(mediaType)).// + andExpect(status().isNoContent()).// + andReturn().getResponse(); + + String content = response.getContentAsString(); + + if (StringUtils.hasText(content)) { + return response; + } + + return request(response.getHeader("Location")); + } + protected Link assertHasLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { String content = response.getContentAsString(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index fd42ef761..f1e5650be 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -20,11 +20,14 @@ import static org.junit.Assert.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Scanner; +import net.minidev.json.JSONArray; + import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -39,15 +42,23 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriTemplate; + /** * Web integration tests specific to JPA. * * @author Oliver Gierke + * @author Greg Turnquist */ @Transactional @ContextConfiguration(classes = JpaRepositoryConfig.class) public class JpaWebTests extends AbstractWebIntegrationTests { + private static final MediaType TEXT_URI_LIST = MediaType.valueOf("text/uri-list"); static final String LINK_TO_SIBLINGS_OF = "$._embedded..[?(@.firstName == '%s')]._links.siblings.href[0]"; @Autowired TestDataPopulator loader; @@ -183,6 +194,203 @@ public class JpaWebTests extends AbstractWebIntegrationTests { mvc.perform(get(href)).andExpect(status().isOk()); } + + /** + * @see DATAREST-219 + */ + @Test + public void manipulatePropertyCollectionRestfullyWithMultiplePosts() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String bilbo = mapper.writeValueAsString(new Person("Bilbo", "Baggins")); + String frodo = mapper.writeValueAsString(new Person("Frodo", "Baggins")); + String merry = mapper.writeValueAsString(new Person("Merry", "Baggins")); + String pippin = mapper.writeValueAsString(new Person("Pippin", "Baggins")); + + Link peopleLink = discoverUnique("people"); + + MockHttpServletResponse bilboResponse = postAndGet(peopleLink, bilbo, MediaType.APPLICATION_JSON); + MockHttpServletResponse frodoResponse = postAndGet(peopleLink, frodo, MediaType.APPLICATION_JSON); + MockHttpServletResponse merryResponse = postAndGet(peopleLink, merry, MediaType.APPLICATION_JSON); + MockHttpServletResponse pippinResponse = postAndGet(peopleLink, pippin, MediaType.APPLICATION_JSON); + + Link bilboSelfLink = assertHasLinkWithRel(Link.REL_SELF, bilboResponse); + Link merrySelfLink = assertHasLinkWithRel(Link.REL_SELF, merryResponse); + Link pippinSelfLink = assertHasLinkWithRel(Link.REL_SELF, pippinResponse); + Link frodosSiblingsLink = assertHasLinkWithRel("siblings", frodoResponse); + + postAndGet(frodosSiblingsLink, bilboSelfLink.getHref(), TEXT_URI_LIST); + postAndGet(frodosSiblingsLink, merrySelfLink.getHref(), TEXT_URI_LIST); + postAndGet(frodosSiblingsLink, pippinSelfLink.getHref(), TEXT_URI_LIST); + + MockHttpServletResponse frodosLatestSiblings = request(frodosSiblingsLink); + String[] persons = ((JSONArray) JsonPath.read(frodosLatestSiblings.getContentAsString(), "$._embedded.persons[*].firstName")).toArray(new String[]{}); + assertThat(persons.length, equalTo(3)); + assertThat(Arrays.asList(persons), hasItems("Bilbo", "Merry", "Pippin")); + } + + /** + * @see DATAREST-219 + */ + @Test + public void manipulatePropertyCollectionRestfullyWithSinglePost() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String bilbo = mapper.writeValueAsString(new Person("Bilbo", "Baggins")); + String frodo = mapper.writeValueAsString(new Person("Frodo", "Baggins")); + String merry = mapper.writeValueAsString(new Person("Merry", "Baggins")); + String pippin = mapper.writeValueAsString(new Person("Pippin", "Baggins")); + + Link peopleLink = discoverUnique("people"); + + MockHttpServletResponse bilboResponse = postAndGet(peopleLink, bilbo, MediaType.APPLICATION_JSON); + MockHttpServletResponse frodoResponse = postAndGet(peopleLink, frodo, MediaType.APPLICATION_JSON); + MockHttpServletResponse merryResponse = postAndGet(peopleLink, merry, MediaType.APPLICATION_JSON); + MockHttpServletResponse pippinResponse = postAndGet(peopleLink, pippin, MediaType.APPLICATION_JSON); + + final Link bilboSelfLink = assertHasLinkWithRel(Link.REL_SELF, bilboResponse); + final Link merrySelfLink = assertHasLinkWithRel(Link.REL_SELF, merryResponse); + final Link pippinSelfLink = assertHasLinkWithRel(Link.REL_SELF, pippinResponse); + Link frodosSiblingsLink = assertHasLinkWithRel("siblings", frodoResponse); + + postAndGet(frodosSiblingsLink, + StringUtils.arrayToDelimitedString(new Object[]{bilboSelfLink.getHref(), + merrySelfLink.getHref(), pippinSelfLink.getHref()}, "\n"), + TEXT_URI_LIST); + + MockHttpServletResponse frodosLatestSiblings = request(frodosSiblingsLink); + String[] persons = ((JSONArray) JsonPath.read(frodosLatestSiblings.getContentAsString(), "$._embedded.persons[*].firstName")).toArray(new String[]{}); + assertThat(persons.length, equalTo(3)); + assertThat(Arrays.asList(persons), hasItems("Bilbo", "Merry", "Pippin")); + } + + /** + * @see DATAREST-219 + */ + @Test + public void manipulatePropertyCollectionRestfullyWithMultiplePuts() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String bilbo = mapper.writeValueAsString(new Person("Bilbo", "Baggins")); + String frodo = mapper.writeValueAsString(new Person("Frodo", "Baggins")); + String merry = mapper.writeValueAsString(new Person("Merry", "Baggins")); + String pippin = mapper.writeValueAsString(new Person("Pippin", "Baggins")); + + Link peopleLink = discoverUnique("people"); + + MockHttpServletResponse bilboResponse = postAndGet(peopleLink, bilbo, MediaType.APPLICATION_JSON); + MockHttpServletResponse frodoResponse = postAndGet(peopleLink, frodo, MediaType.APPLICATION_JSON); + MockHttpServletResponse merryResponse = postAndGet(peopleLink, merry, MediaType.APPLICATION_JSON); + MockHttpServletResponse pippinResponse = postAndGet(peopleLink, pippin, MediaType.APPLICATION_JSON); + + Link bilboSelfLink = assertHasLinkWithRel(Link.REL_SELF, bilboResponse); + Link merrySelfLink = assertHasLinkWithRel(Link.REL_SELF, merryResponse); + Link pippinSelfLink = assertHasLinkWithRel(Link.REL_SELF, pippinResponse); + Link frodosSiblingsLink = assertHasLinkWithRel("siblings", frodoResponse); + + putAndGet(frodosSiblingsLink, bilboSelfLink.getHref(), TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, merrySelfLink.getHref(), TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, pippinSelfLink.getHref(), TEXT_URI_LIST); + + MockHttpServletResponse frodosLatestSiblings = request(frodosSiblingsLink); + String firstName = JsonPath.read(frodosLatestSiblings.getContentAsString(), "$._embedded.person.firstName"); + assertThat(firstName, equalTo("Pippin")); + + postAndGet(frodosSiblingsLink, merrySelfLink.getHref(), TEXT_URI_LIST); + + frodosLatestSiblings = request(frodosSiblingsLink); + String[] persons = ((JSONArray) JsonPath.read(frodosLatestSiblings.getContentAsString(), + "$._embedded.persons[*].firstName")).toArray(new String[]{}); + assertThat(persons.length, equalTo(2)); + assertThat(Arrays.asList(persons), hasItems("Merry", "Pippin")); + } + + /** + * @see DATAREST-219 + */ + @Test + public void manipulatePropertyCollectionRestfullyWithSinglePut() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String bilbo = mapper.writeValueAsString(new Person("Bilbo", "Baggins")); + String frodo = mapper.writeValueAsString(new Person("Frodo", "Baggins")); + String merry = mapper.writeValueAsString(new Person("Merry", "Baggins")); + String pippin = mapper.writeValueAsString(new Person("Pippin", "Baggins")); + + Link peopleLink = discoverUnique("people"); + + MockHttpServletResponse bilboResponse = postAndGet(peopleLink, bilbo, MediaType.APPLICATION_JSON); + MockHttpServletResponse frodoResponse = postAndGet(peopleLink, frodo, MediaType.APPLICATION_JSON); + MockHttpServletResponse merryResponse = postAndGet(peopleLink, merry, MediaType.APPLICATION_JSON); + MockHttpServletResponse pippinResponse = postAndGet(peopleLink, pippin, MediaType.APPLICATION_JSON); + + Link bilboSelfLink = assertHasLinkWithRel(Link.REL_SELF, bilboResponse); + Link merrySelfLink = assertHasLinkWithRel(Link.REL_SELF, merryResponse); + Link pippinSelfLink = assertHasLinkWithRel(Link.REL_SELF, pippinResponse); + Link frodosSiblingsLink = assertHasLinkWithRel("siblings", frodoResponse); + + putAndGet(frodosSiblingsLink, + StringUtils.arrayToDelimitedString(new Object[]{bilboSelfLink.getHref(), + merrySelfLink.getHref()}, "\n"), + TEXT_URI_LIST); + putAndGet(frodosSiblingsLink, pippinSelfLink.getHref(), TEXT_URI_LIST); + + MockHttpServletResponse frodosLatestSiblings = request(frodosSiblingsLink); + String firstName = JsonPath.read(frodosLatestSiblings.getContentAsString(), "$._embedded.person.firstName"); + assertThat(firstName, equalTo("Pippin")); + + postAndGet(frodosSiblingsLink, merrySelfLink.getHref(), TEXT_URI_LIST); + + frodosLatestSiblings = request(frodosSiblingsLink); + String[] persons = ((JSONArray) JsonPath.read(frodosLatestSiblings.getContentAsString(), + "$._embedded.persons[*].firstName")).toArray(new String[]{}); + assertThat(persons.length, equalTo(2)); + assertThat(Arrays.asList(persons), hasItems("Merry", "Pippin")); + } + + /** + * @see DATAREST-219 + */ + @Test + public void manipulatePropertyCollectionRestfullyWithDelete() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String bilbo = mapper.writeValueAsString(new Person("Bilbo", "Baggins")); + String frodo = mapper.writeValueAsString(new Person("Frodo", "Baggins")); + String merry = mapper.writeValueAsString(new Person("Merry", "Baggins")); + String pippin = mapper.writeValueAsString(new Person("Pippin", "Baggins")); + + Link peopleLink = discoverUnique("people"); + + MockHttpServletResponse bilboResponse = postAndGet(peopleLink, bilbo, MediaType.APPLICATION_JSON); + MockHttpServletResponse frodoResponse = postAndGet(peopleLink, frodo, MediaType.APPLICATION_JSON); + MockHttpServletResponse merryResponse = postAndGet(peopleLink, merry, MediaType.APPLICATION_JSON); + MockHttpServletResponse pippinResponse = postAndGet(peopleLink, pippin, MediaType.APPLICATION_JSON); + + Link bilboSelfLink = assertHasLinkWithRel(Link.REL_SELF, bilboResponse); + Link merrySelfLink = assertHasLinkWithRel(Link.REL_SELF, merryResponse); + Link pippinSelfLink = assertHasLinkWithRel(Link.REL_SELF, pippinResponse); + Link frodosSiblingsLink = assertHasLinkWithRel("siblings", frodoResponse); + + postAndGet(frodosSiblingsLink, bilboSelfLink.getHref(), TEXT_URI_LIST); + postAndGet(frodosSiblingsLink, merrySelfLink.getHref(), TEXT_URI_LIST); + postAndGet(frodosSiblingsLink, pippinSelfLink.getHref(), TEXT_URI_LIST); + + String pippinId = new UriTemplate("/people/{id}").match(pippinSelfLink.getHref()).get("id"); + deleteAndGet(new Link(frodosSiblingsLink.getHref() + "/" + pippinId), TEXT_URI_LIST); + + MockHttpServletResponse frodosLatestSiblings = request(frodosSiblingsLink); + String[] persons = ((JSONArray) JsonPath.read(frodosLatestSiblings.getContentAsString(), "$._embedded.persons[*].firstName")).toArray(new String[]{}); + assertThat(persons.length, equalTo(2)); + assertThat(Arrays.asList(persons), hasItems("Bilbo", "Merry")); + } + + private String readFile(String name) throws Exception { ClassPathResource file = new ClassPathResource(name, getClass());