diff --git a/pom.xml b/pom.xml index 4d619c5fb4..9c4eed6b9f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,4 @@ - + 4.0.0 @@ -13,7 +13,7 @@ org.springframework.data.build spring-data-parent - 1.6.0.RC1 + 1.6.0.BUILD-SNAPSHOT ../spring-data-build/parent/pom.xml @@ -27,7 +27,7 @@ multi spring-data-rest - 1.10.0.RC1 + 1.10.0.BUILD-SNAPSHOT 1.8.0.RC1 1.7.0.RC1 3.3.0.RC1 @@ -154,8 +154,8 @@ - spring-libs-milestone - http://repo.spring.io/libs-milestone + spring-libs-snapshot + http://repo.spring.io/libs-snapshot diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index d8a9f3c764..0005a4a01a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -46,8 +46,8 @@ public class RepositoryRestConfiguration { private String sortParamName = "sort"; private MediaType defaultMediaType = MediaTypes.HAL_JSON; private boolean useHalAsDefaultJsonMediaType = true; - private boolean returnBodyOnCreate = false; - private boolean returnBodyOnUpdate = false; + private Boolean returnBodyOnCreate = Boolean.FALSE; + private Boolean returnBodyOnUpdate = Boolean.FALSE; private List> exposeIdsFor = new ArrayList>(); private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration(); private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration(); @@ -296,19 +296,21 @@ public RepositoryRestConfiguration useHalAsDefaultJsonMediaType(boolean useHalAs /** * Whether to return a response body after creating an entity. * - * @return {@literal true} to return a body on create, {@literal false} otherwise. + * @return {@link java.lang.Boolean#TRUE} to return a body on create, {@link java.lang.Boolean#FALSE} otherwise. + * If {@literal null}, defer to HTTP Accept header */ - public boolean isReturnBodyOnCreate() { + public Boolean isReturnBodyOnCreate() { return returnBodyOnCreate; } /** * Set whether to return a response body after creating an entity. * - * @param returnBodyOnCreate {@literal true} to return a body on create, {@literal false} otherwise. + * @param returnBodyOnCreate {@link java.lang.Boolean#TRUE} to return a body on create, {@link java.lang.Boolean#FALSE} otherwise. + * If {@literal null}, defer to HTTP Accept header * @return {@literal this} */ - public RepositoryRestConfiguration setReturnBodyOnCreate(boolean returnBodyOnCreate) { + public RepositoryRestConfiguration setReturnBodyOnCreate(Boolean returnBodyOnCreate) { this.returnBodyOnCreate = returnBodyOnCreate; return this; } @@ -316,19 +318,21 @@ public RepositoryRestConfiguration setReturnBodyOnCreate(boolean returnBodyOnCre /** * Whether to return a response body after updating an entity. * - * @return {@literal true} to return a body on update, {@literal false} otherwise. + * @return {@link java.lang.Boolean#TRUE} to return a body on update, {@link java.lang.Boolean#FALSE} otherwise. + * If {@literal null}, defer to HTTP Accept header */ - public boolean isReturnBodyOnUpdate() { + public Boolean isReturnBodyOnUpdate() { return returnBodyOnUpdate; } /** - * Sets whether to return a response body after updating an entity. - * - * @param returnBodyOnUpdate - * @return + * Set whether to return a response body after updating an entity. + * + * @param returnBodyOnUpdate {@link java.lang.Boolean#TRUE} to return a body on update, {@link java.lang.Boolean#FALSE} otherwise. + * If {@literal null}, defer to HTTP Accept header + * @return {@literal this} */ - public RepositoryRestConfiguration setReturnBodyOnUpdate(boolean returnBodyOnUpdate) { + public RepositoryRestConfiguration setReturnBodyOnUpdate(Boolean returnBodyOnUpdate) { this.returnBodyOnUpdate = returnBodyOnUpdate; return this; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 7801779f9e..ace77375de 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -63,6 +63,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @@ -81,6 +82,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem RestMediaTypes.JSON_PATCH_JSON.toString(), // MediaType.APPLICATION_JSON_VALUE); + private static final String ACCEPT_HEADER = "Accept"; + private final RepositoryEntityLinks entityLinks; private final RepositoryRestConfiguration config; private final ConversionService conversionService; @@ -227,18 +230,23 @@ public Resources getCollectionResourceCompact(RootResourceInformation repoReq * * @param resourceInformation * @param payload + * @param assembler + * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException */ @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST) public ResponseEntity postCollectionResource(RootResourceInformation resourceInformation, - PersistentEntityResource payload, PersistentEntityResourceAssembler assembler) + PersistentEntityResource payload, PersistentEntityResourceAssembler assembler, + @RequestHeader(value= ACCEPT_HEADER, required = false) String acceptHeader) throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.POST, ResourceType.COLLECTION); - return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler); + boolean acceptHeaderPresent = acceptHeader != null; + + return createAndReturn(payload.getContent(), resourceInformation.getInvoker(), assembler, acceptHeaderPresent); } /** @@ -312,17 +320,21 @@ public ResponseEntity> getItemResource(RootResourceInformation resou /** * PUT /{repository}/{id} - Updates an existing entity or creates one at exactly that place. * - * @param eTagMatch + * @param resourceInformation * @param payload * @param id + * @param assembler + * @param eTag + * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT) public ResponseEntity putItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, - ETag eTag) throws HttpRequestMethodNotSupportedException { + ETag eTag, @RequestHeader(value=ACCEPT_HEADER, required = false) String acceptHeader) + throws HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); @@ -338,8 +350,10 @@ public ResponseEntity putItemResource(RootResourceInf eTag.verify(resourceInformation.getPersistentEntity(), domainObject); - return domainObject == null ? createAndReturn(objectToSave, invoker, assembler) : saveAndReturn(objectToSave, - invoker, PUT, assembler); + boolean acceptHeaderPresent = acceptHeader != null; + + return domainObject == null ? createAndReturn(objectToSave, invoker, assembler, acceptHeaderPresent) + : saveAndReturn(objectToSave, invoker, PUT, assembler, acceptHeaderPresent); } /** @@ -349,7 +363,8 @@ public ResponseEntity putItemResource(RootResourceInf * @param payload * @param id * @param assembler - * @param eTag + * @param eTag, + * @param acceptHeader * @return * @throws HttpRequestMethodNotSupportedException * @throws ResourceNotFoundException @@ -358,7 +373,8 @@ public ResponseEntity putItemResource(RootResourceInf @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH) public ResponseEntity patchItemResource(RootResourceInformation resourceInformation, PersistentEntityResource payload, @BackendId Serializable id, PersistentEntityResourceAssembler assembler, - ETag eTag) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { + ETag eTag,@RequestHeader(value=ACCEPT_HEADER, required = false) String acceptHeader ) + throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM); @@ -370,7 +386,9 @@ public ResponseEntity patchItemResource(RootResourceInformation eTag.verify(resourceInformation.getPersistentEntity(), domainObject); - return saveAndReturn(payload.getContent(), resourceInformation.getInvoker(), PATCH, assembler); + boolean acceptHeaderPresent = acceptHeader != null; + + return saveAndReturn(payload.getContent(), resourceInformation.getInvoker(), PATCH, assembler, acceptHeaderPresent); } /** @@ -415,7 +433,7 @@ public ResponseEntity deleteItemResource(RootResourceInformation resourceInfo * @return */ private ResponseEntity saveAndReturn(Object domainObject, RepositoryInvoker invoker, - HttpMethod httpMethod, PersistentEntityResourceAssembler assembler) { + HttpMethod httpMethod, PersistentEntityResourceAssembler assembler, boolean acceptHeaderPresent) { publisher.publishEvent(new BeforeSaveEvent(domainObject)); Object obj = invoker.invokeSave(domainObject); @@ -428,7 +446,10 @@ private ResponseEntity saveAndReturn(Object domainObject, Repos addLocationHeader(headers, assembler, obj); } - if (config.isReturnBodyOnUpdate()) { + boolean returnBodyOnUpdate = (config.isReturnBodyOnUpdate() == null && acceptHeaderPresent) + || Boolean.TRUE.equals(config.isReturnBodyOnUpdate()); + + if (returnBodyOnUpdate) { return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, resource); } else { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT, headers); @@ -443,13 +464,17 @@ private ResponseEntity saveAndReturn(Object domainObject, Repos * @return */ private ResponseEntity createAndReturn(Object domainObject, RepositoryInvoker invoker, - PersistentEntityResourceAssembler assembler) { + PersistentEntityResourceAssembler assembler, boolean acceptHeaderPresent) { publisher.publishEvent(new BeforeCreateEvent(domainObject)); Object savedObject = invoker.invokeSave(domainObject); publisher.publishEvent(new AfterCreateEvent(savedObject)); - PersistentEntityResource resource = config.isReturnBodyOnCreate() ? assembler.toFullResource(savedObject) : null; + + boolean returnBodyOnCreate = (config.isReturnBodyOnCreate() == null && acceptHeaderPresent) + || Boolean.TRUE.equals(config.isReturnBodyOnCreate()); + + PersistentEntityResource resource = returnBodyOnCreate ? assembler.toFullResource(savedObject) : null; HttpHeaders headers = prepareHeaders(resource); addLocationHeader(headers, assembler, savedObject); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 21de8f0e6a..1262a12cd9 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.List; +import org.junit.After; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mapping.context.PersistentEntities; @@ -33,6 +34,7 @@ import org.springframework.data.rest.webmvc.jpa.Order; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.support.ETag; +import org.springframework.hateoas.ResourceSupport; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -46,6 +48,7 @@ * * @author Oliver Gierke */ +@SuppressWarnings("ALL") @ContextConfiguration(classes = JpaRepositoryConfig.class) @Transactional public class RepositoryEntityControllerIntegrationTests extends AbstractControllerIntegrationTests { @@ -76,7 +79,7 @@ public void rejectsEntityCreationIfSaveIsNotExported() throws Exception { RootResourceInformation request = getResourceInformation(Address.class); - controller.postCollectionResource(request, null, null); + controller.postCollectionResource(request, null, null, MediaType.APPLICATION_JSON_VALUE); } /** @@ -91,7 +94,7 @@ public void setsExpandedSelfUriInLocationHeader() throws Exception { entities.getPersistentEntity(Order.class)).build(); ResponseEntity entity = controller.putItemResource(information, persistentEntityResource, 1L, assembler, - ETag.NO_ETAG); + ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE); assertThat(entity.getHeaders().getLocation().toString(), not(endsWith("{?projection}"))); } @@ -181,4 +184,99 @@ public void optionsForItermResourceSetsAllowPatchHeader() { RestMediaTypes.MERGE_PATCH_JSON.toString(), // MediaType.APPLICATION_JSON_VALUE)); } + + /** + * @see DATAREST-34 + */ + @Test + public void verifyAcceptHeaderCanControlBodyReturnOnPutItemResource() throws HttpRequestMethodNotSupportedException { + RootResourceInformation request = getResourceInformation(Order.class); + + PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), + entities.getPersistentEntity(Order.class)).build(); + + configuration.setReturnBodyOnCreate(Boolean.FALSE); + configuration.setReturnBodyOnUpdate(Boolean.FALSE); + + ResponseEntity response = controller.putItemResource(request, persistentEntityResource, 1L, assembler, + ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE); + + assert(!response.hasBody()); + + + configuration.setReturnBodyOnCreate(Boolean.TRUE); + configuration.setReturnBodyOnUpdate(Boolean.TRUE); + + response = controller.putItemResource(request, persistentEntityResource, 1L, assembler, + ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE); + + configuration.setReturnBodyOnCreate(Boolean.FALSE); + configuration.setReturnBodyOnUpdate(Boolean.FALSE); + + response = controller.putItemResource(request, persistentEntityResource, 1L, assembler, + ETag.NO_ETAG, null); + + assert(!response.hasBody()); + + configuration.setReturnBodyOnCreate(null); + configuration.setReturnBodyOnUpdate(null); + + response = controller.putItemResource(request, persistentEntityResource, 1L, assembler, + ETag.NO_ETAG, null); + + assert(!response.hasBody()); + + configuration.setReturnBodyOnCreate(null); + configuration.setReturnBodyOnUpdate(null); + + response = controller.putItemResource(request, persistentEntityResource, 1L, assembler, + ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE); + + assert(response.hasBody()); + } + + /** + * @see DATAREST-34 + */ + @Test + public void verifyAcceptHeaderCanControlBodyReturnPostCollectionResource() throws HttpRequestMethodNotSupportedException { + RootResourceInformation request = getResourceInformation(Order.class); + + PersistentEntityResource persistentEntityResource = PersistentEntityResource.build(new Order(new Person()), + entities.getPersistentEntity(Order.class)).build(); + + configuration.setReturnBodyOnCreate(null); + + ResponseEntity response = + controller.postCollectionResource(request, persistentEntityResource, assembler, MediaType.APPLICATION_JSON_VALUE); + + + assert(response.hasBody()); + + + response = + controller.postCollectionResource(request, persistentEntityResource, assembler, null); + + + assert(!response.hasBody()); + + + configuration.setReturnBodyOnCreate(Boolean.FALSE); + response = + controller.postCollectionResource(request, persistentEntityResource, assembler, MediaType.APPLICATION_JSON_VALUE); + + assert(!response.hasBody()); + + configuration.setReturnBodyOnCreate(Boolean.TRUE); + response = + controller.postCollectionResource(request, persistentEntityResource, assembler, null); + + assert(response.hasBody()); + } + + @After + public void cleanUp() { + configuration.setReturnBodyOnCreate(Boolean.FALSE); + configuration.setReturnBodyOnUpdate(Boolean.FALSE); + } } diff --git a/src/main/asciidoc/repository-resources.adoc b/src/main/asciidoc/repository-resources.adoc index 2d6ad00eab..55077f4e76 100644 --- a/src/main/asciidoc/repository-resources.adoc +++ b/src/main/asciidoc/repository-resources.adoc @@ -24,6 +24,8 @@ For the resources exposed, we use a set of default status codes: * `201 Created` - for `POST` and `PUT` requests that create new resources. * `204 No Content` - for `PUT`, `PATCH`, and `DELETE` requests if the configuration is set to not return response bodies for resource updates (`RepositoryRestConfiguration.returnBodyOnUpdate`). If the configuration value is set to include responses for `PUT`, `200 OK` will be returned for updates, `201 Created` will be returned for resource created through `PUT`. +If the configuration values (`RepositoryRestConfiguration.returnBodyOnUpdate` and `RepositoryRestConfiguration.returnBodyCreate)` are explicitly set to null, the presence of the HTTP Accept header will be used to determine the response code. + [[repository-resources.resource-discoverability]] === Resource discoverability