diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java index 483afeaff..a6745cfe4 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformation.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.config; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -40,6 +41,10 @@ public interface HypermediaMappingInformation { */ List getMediaTypes(); + default Collection getRegisterableMediaTypes() { + return getMediaTypes(); + } + /** * Configure an {@link ObjectMapper} and register custom serializers and deserializers for the supported media types. * If all you want to do is register a Jackson {@link Module}, prefer implementing {@link #getJacksonModule()}. diff --git a/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java index 7910c709c..e388a2a48 100644 --- a/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java @@ -54,22 +54,27 @@ public ExchangeStrategies hypermediaExchangeStrategies() { List> encoders = new ArrayList<>(); List> decoders = new ArrayList<>(); - + this.hypermediaTypes.forEach(hypermedia -> { ObjectMapper objectMapper = hypermedia.configureObjectMapper(this.mapper.copy()); - MimeType[] mimeTypes = hypermedia.getMediaTypes().toArray(new MimeType[0]); + + MimeType[] mimeTypes = hypermedia.getRegisterableMediaTypes().toArray(new MimeType[0]); encoders.add(new Jackson2JsonEncoder(objectMapper, mimeTypes)); decoders.add(new Jackson2JsonDecoder(objectMapper, mimeTypes)); }); - return ExchangeStrategies.builder().codecs(clientCodecConfigurer -> { + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder().codecs(clientCodecConfigurer -> { encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder)); decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder)); + clientCodecConfigurer.registerDefaults(true); + }).build(); + + return exchangeStrategies; } /** diff --git a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java index 75d18a7c9..a793d50ac 100644 --- a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java @@ -17,7 +17,10 @@ import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Optional; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectProvider; @@ -27,6 +30,10 @@ import org.springframework.context.annotation.Lazy; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.mediatype.hal.HalMediaTypeConfiguration; +import org.springframework.hateoas.mediatype.hal.forms.HalFormsMediaTypeConfiguration; +import org.springframework.http.MediaType; import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonDecoder; @@ -125,10 +132,10 @@ static class HypermediaWebFluxConfigurer implements WebFluxConfigurer { public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs(); - + this.hypermediaTypes.forEach(hypermedia -> { - MimeType[] mimeTypes = hypermedia.getMediaTypes().toArray(new MimeType[0]); + MimeType[] mimeTypes = hypermedia.getRegisterableMediaTypes().toArray(new MimeType[0]); ObjectMapper objectMapper = hypermedia.configureObjectMapper(this.mapper.copy()); customCodecs.encoder(new Jackson2JsonEncoder(objectMapper, mimeTypes)); diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java index 7accce1f9..412f504bc 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java @@ -15,6 +15,8 @@ */ package org.springframework.hateoas.mediatype.hal; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.springframework.beans.factory.ObjectProvider; @@ -74,6 +76,17 @@ public List getMediaTypes() { return HypermediaType.HAL.getMediaTypes(); } + /** + * {@code HAL} media type must also register as {@link MediaType#APPLICATION_JSON}. + */ + @Override + public Collection getRegisterableMediaTypes() { + + List mediaTypes = new ArrayList<>(getMediaTypes()); + mediaTypes.add(MediaType.APPLICATION_JSON); + return mediaTypes; + } + /* * (non-Javadoc) * @see org.springframework.hateoas.config.HypermediaMappingInformation#configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper) diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java index 7aa41bc64..669d0e12e 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java @@ -17,6 +17,8 @@ import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.springframework.beans.factory.ObjectProvider; @@ -42,7 +44,7 @@ */ @Configuration @RequiredArgsConstructor -class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { +public class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { private final DelegatingLinkRelationProvider relProvider; private final ObjectProvider curieProvider; @@ -64,6 +66,17 @@ public List getMediaTypes() { return HypermediaType.HAL_FORMS.getMediaTypes(); } + /** + * {@code HAL-FORMS} media type must also register as {@link MediaType#APPLICATION_JSON}. + */ + @Override + public Collection getRegisterableMediaTypes() { + + List mediaTypes = new ArrayList<>(getMediaTypes()); + mediaTypes.add(MediaType.APPLICATION_JSON); + return mediaTypes; + } + /* * (non-Javadoc) * @see org.springframework.hateoas.config.HypermediaMappingInformation#configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper) diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java index f41efd7e0..916f4d4e7 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java @@ -24,6 +24,7 @@ import reactor.test.StepVerifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -41,6 +42,7 @@ import org.springframework.hateoas.server.core.TypeReferences.CollectionModelType; import org.springframework.hateoas.server.core.TypeReferences.EntityModelType; import org.springframework.hateoas.support.Employee; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; @@ -262,10 +264,135 @@ void callingForUnregisteredMediaTypeShouldFail() { setUp(HalWebFluxConfig.class); - this.testClient.get().uri("/").accept(MediaTypes.UBER_JSON).exchange().expectStatus().is4xxClientError() - .returnResult(String.class).getResponseBody().as(StepVerifier::create).verifyComplete(); + this.testClient.get().uri("/").accept(MediaTypes.UBER_JSON).exchange() // + .expectStatus().isEqualTo(HttpStatus.NOT_ACCEPTABLE) // + .returnResult(RepresentationModel.class).getResponseBody() // + .as(StepVerifier::create) // + .verifyComplete(); } + @Test + void callingForUnregisteredMediaTypeShouldFail2() { + + setUp(HalFormsWebFluxConfig.class); + + this.testClient.get().uri("/").accept(MediaTypes.UBER_JSON).exchange() // + .expectStatus().isEqualTo(HttpStatus.NOT_ACCEPTABLE) // + .returnResult(RepresentationModel.class).getResponseBody() // + .as(StepVerifier::create) // + .verifyComplete(); + } + + @Test + void callingForApplicationJsonWhenHalIsRegisteredShouldResultInHalAsApplicationJson() { + + setUp(HalWebFluxConfig.class); + + this.testClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .returnResult(RepresentationModel.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNextMatches(s -> { + assertThat(s).isEqualTo( + new RepresentationModel<>(Arrays.asList(new Link("/", "self"), new Link("/employees", "employees")))); + return true; + }) // + .verifyComplete(); + + this.testClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + } + + @Test + void callingForDefaultWhenHalIsRegisteredShouldResultInHal() { + + setUp(HalWebFluxConfig.class); + + this.testClient.get().uri("/").exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + } + + @Test + void callingForApplicationJsonWhenHalFormsIsRegisteredShouldResultInHalFormsAsApplicationJson() { + + setUp(HalFormsWebFluxConfig.class); + + this.testClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .returnResult(RepresentationModel.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNextMatches(s -> { + assertThat(s).isEqualTo( + new RepresentationModel<>(Arrays.asList(new Link("/", "self"), new Link("/employees", "employees")))); + return true; + }) // + .verifyComplete(); + + this.testClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + } + + @Test + void callingForDefaultWhenHalFormsIsRegisteredShouldResultInHalForms() { + + setUp(HalFormsWebFluxConfig.class); + + this.testClient.get().uri("/").exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_FORMS_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + } + + @Test + void callingForApplicationJsonWhenHalAndHalFormsAreRegisteredProducesHalAsApplicationJson() { + + setUp(AllHalWebFluxConfig.class); + + this.testClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + + } + + @Test + void callingForDefaultWhenHalAndHalFormsIsRegisteredShouldResultInHal() { + + setUp(AllHalWebFluxConfig.class); + + this.testClient.get().uri("/").exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_JSON) // + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // + .expectNext("{\"_links\":{\"self\":{\"href\":\"/\"},\"employees\":{\"href\":\"/employees\"}}}") // + .verifyComplete(); + } + + /** * @see #728 */