diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java index 621ddc8a7..6c1e0a0bf 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -20,12 +20,15 @@ import lombok.Getter; import lombok.experimental.Wither; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; +import org.springframework.http.MediaType; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; @@ -47,6 +50,7 @@ public class HalConfiguration { */ private final @Wither @Getter RenderSingleLinks renderSingleLinks; private final @Wither(AccessLevel.PRIVATE) Map singleLinksPerPattern; + private final @Getter List additionalMediaTypes = new ArrayList<>(); /** * Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document. @@ -122,4 +126,16 @@ public enum RenderSingleLinks { */ AS_ARRAY } + + /** + * Register other {@link MediaType}s this one should response to. + * + * @param mediatype + * @return HalFormsConfiguration with new {@link MediaType} added + */ + public HalConfiguration withAdditionalMediatype(MediaType mediatype) { + + this.additionalMediaTypes.add(mediatype); + return this; + } } 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..8a91a4df6 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,7 @@ */ package org.springframework.hateoas.mediatype.hal; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.ObjectProvider; @@ -71,7 +72,10 @@ LinkDiscoverer halLinkDisocoverer() { */ @Override public List getMediaTypes() { - return HypermediaType.HAL.getMediaTypes(); + + List mediaTypes = new ArrayList<>(HypermediaType.HAL.getMediaTypes()); + this.halConfiguration.ifAvailable(halConfig -> mediaTypes.addAll(halConfig.getAdditionalMediaTypes())); + return mediaTypes; } /* diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java index 2756ca8c5..c92327ff9 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java @@ -18,12 +18,14 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.springframework.core.ResolvableType; import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.http.MediaType; /** * HAL-FORMS specific configuration extension of {@link HalConfiguration}. @@ -60,4 +62,20 @@ public HalFormsConfiguration registerPattern(Class type, String pattern) { Optional getTypePatternFor(ResolvableType type) { return Optional.ofNullable(patterns.get(type.resolve(Object.class))); } + + /** + * Register other {@link MediaType}s this one should response to. + * + * @param mediatype + * @return HalFormsConfiguration + */ + public HalFormsConfiguration withAdditionalMediatype(MediaType mediatype) { + + this.halConfiguration.getAdditionalMediaTypes().add(mediatype); + return this; + } + + public Collection getAdditionalMediaTypes() { + return this.halConfiguration.getAdditionalMediaTypes(); + } } 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..826fa5323 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,7 @@ import lombok.RequiredArgsConstructor; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.ObjectProvider; @@ -61,7 +62,10 @@ LinkDiscoverer halFormsLinkDiscoverer() { */ @Override public List getMediaTypes() { - return HypermediaType.HAL_FORMS.getMediaTypes(); + + List mediaTypes = new ArrayList<>(HypermediaType.HAL_FORMS.getMediaTypes()); + this.halFormsConfiguration.ifAvailable(halFormsConfig -> mediaTypes.addAll(halFormsConfig.getAdditionalMediaTypes())); + return mediaTypes; } /* diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebFluxIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebFluxIntegrationTest.java new file mode 100644 index 000000000..92fb1e94d --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebFluxIntegrationTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2017 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 + * + * https://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.hateoas.mediatype.hal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.springframework.hateoas.support.JsonPathUtils.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.WebClientConfigurer; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebFluxEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalHandleApplicationJsonWebFluxIntegrationTest { + + @Autowired WebTestClient testClient; + + @BeforeEach + void setUp() { + WebFluxEmployeeController.reset(); + } + + /** + * @see #728 + */ + @Test + void singleEmployee() { + + this.testClient.get().uri("http://localhost/employees/0").accept(MediaType.APPLICATION_JSON).exchange() + + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .expectBody(String.class)// + + .value(jsonPath("$.name", is("Frodo Baggins"))) // + .value(jsonPath("$.role", is("ring bearer"))) // + + .value(jsonPath("$._links.*", hasSize(2))) // + .value(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) // + .value(jsonPath("$._links['employees'].href", is("http://localhost/employees"))); + } + + /** + * @see #728 + */ + @Test + void collectionOfEmployees() { + + this.testClient.get().uri("http://localhost/employees").accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .isOk().expectHeader().contentType(MediaType.APPLICATION_JSON).expectBody(String.class) + .value(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .value(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .value(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .value(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .value(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .value(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .value(jsonPath("$._links.*", hasSize(1))) + .value(jsonPath("$._links['self'].href", is("http://localhost/employees"))); + } + + /** + * @see #728 + */ + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.testClient.post().uri("http://localhost/employees").contentType(MediaType.APPLICATION_JSON) + .bodyValue(specBasedJson) // + .exchange() // + .expectStatus().isCreated() // + .expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2"); + } + + @Configuration + @EnableWebFlux + @EnableHypermediaSupport(type = { HypermediaType.HAL }) + static class TestConfig { + + @Bean + WebFluxEmployeeController employeeController() { + return new WebFluxEmployeeController(); + } + + @Bean + WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) { + + return WebTestClient.bindToApplicationContext(ctx).build().mutate() + .exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build(); + } + + @Bean + HalConfiguration halConfiguration() { + return new HalConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebMvcIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebMvcIntegrationTest.java new file mode 100644 index 000000000..467d987fe --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalHandleApplicationJsonWebMvcIntegrationTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2017-2019 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 + * + * https://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.hateoas.mediatype.hal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebMvcEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalHandleApplicationJsonWebMvcIntegrationTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + + this.mockMvc = webAppContextSetup(this.context).build(); + WebMvcEmployeeController.reset(); + } + + @Test + void singleEmployee() throws Exception { + + this.mockMvc.perform(get("/employees/0").accept(MediaType.APPLICATION_JSON)) // + + .andExpect(status().isOk()) // + .andExpect(jsonPath("$.name", is("Frodo Baggins"))) // + .andExpect(jsonPath("$.role", is("ring bearer"))) + + .andExpect(jsonPath("$._links.*", hasSize(2))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._links['employees'].href", is("http://localhost/employees"))); + } + + @Test + void collectionOfEmployees() throws Exception { + + this.mockMvc.perform(get("/employees").accept(MediaType.APPLICATION_JSON)) // + .andExpect(status().isOk()) // + .andExpect(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .andExpect(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .andExpect(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .andExpect(jsonPath("$._links.*", hasSize(1))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees"))); + } + + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.mockMvc.perform(post("/employees") // + .content(specBasedJson) // + .contentType(MediaType.APPLICATION_JSON_VALUE)) // + .andExpect(status().isCreated()) + .andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2")); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class TestConfig { + + @Bean + WebMvcEmployeeController employeeController() { + return new WebMvcEmployeeController(); + } + + @Bean + HalConfiguration halFormsConfiguration() { + return new HalConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + } + + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class WithHalConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + + @Bean + public HalConfiguration halConfiguration() { + return CONFIG; + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebFluxIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebFluxIntegrationTest.java new file mode 100644 index 000000000..18125aeff --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebFluxIntegrationTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2017 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 + * + * https://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.hateoas.mediatype.hal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.springframework.hateoas.support.JsonPathUtils.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.WebClientConfigurer; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebFluxEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalWebFluxIntegrationTest { + + @Autowired WebTestClient testClient; + + @BeforeEach + void setUp() { + WebFluxEmployeeController.reset(); + } + + /** + * @see #728 + */ + @Test + void singleEmployee() { + + this.testClient.get().uri("http://localhost/employees/0").accept(MediaTypes.HAL_JSON).exchange() + + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_JSON) // + .expectBody(String.class)// + + .value(jsonPath("$.name", is("Frodo Baggins"))) // + .value(jsonPath("$.role", is("ring bearer"))) // + + .value(jsonPath("$._links.*", hasSize(2))) // + .value(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) // + .value(jsonPath("$._links['employees'].href", is("http://localhost/employees"))); + } + + /** + * @see #728 + */ + @Test + void collectionOfEmployees() { + + this.testClient.get().uri("http://localhost/employees").accept(MediaTypes.HAL_JSON).exchange().expectStatus() + .isOk().expectHeader().contentType(MediaTypes.HAL_JSON).expectBody(String.class) + .value(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .value(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .value(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .value(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .value(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .value(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .value(jsonPath("$._links.*", hasSize(1))) + .value(jsonPath("$._links['self'].href", is("http://localhost/employees"))); + } + + /** + * @see #728 + */ + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.testClient.post().uri("http://localhost/employees").contentType(MediaTypes.HAL_JSON) + .bodyValue(specBasedJson) // + .exchange() // + .expectStatus().isCreated() // + .expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2"); + } + + @Configuration + @EnableWebFlux + @EnableHypermediaSupport(type = { HypermediaType.HAL }) + static class TestConfig { + + @Bean + WebFluxEmployeeController employeeController() { + return new WebFluxEmployeeController(); + } + + @Bean + WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) { + + return WebTestClient.bindToApplicationContext(ctx).build().mutate() + .exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build(); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebMvcIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebMvcIntegrationTest.java new file mode 100644 index 000000000..ddb4a2736 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalWebMvcIntegrationTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2019 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 + * + * https://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.hateoas.mediatype.hal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebMvcEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalWebMvcIntegrationTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + + this.mockMvc = webAppContextSetup(this.context).build(); + WebMvcEmployeeController.reset(); + } + + @Test + void singleEmployee() throws Exception { + + this.mockMvc.perform(get("/employees/0").accept(MediaTypes.HAL_JSON)) // + + .andExpect(status().isOk()) // + .andExpect(jsonPath("$.name", is("Frodo Baggins"))) // + .andExpect(jsonPath("$.role", is("ring bearer"))) + + .andExpect(jsonPath("$._links.*", hasSize(2))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._links['employees'].href", is("http://localhost/employees"))); + } + + @Test + void collectionOfEmployees() throws Exception { + + this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_JSON)) // + .andExpect(status().isOk()) // + .andExpect(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .andExpect(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .andExpect(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .andExpect(jsonPath("$._links.*", hasSize(1))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees"))); + } + + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.mockMvc.perform(post("/employees") // + .content(specBasedJson) // + .contentType(MediaTypes.HAL_JSON_VALUE)) // + .andExpect(status().isCreated()) + .andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2")); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class TestConfig { + + @Bean + WebMvcEmployeeController employeeController() { + return new WebMvcEmployeeController(); + } + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class WithHalConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration(); + + @Bean + public HalConfiguration halConfiguration() { + return CONFIG; + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebFluxIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebFluxIntegrationTest.java new file mode 100644 index 000000000..856d6a8fd --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebFluxIntegrationTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2017 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 + * + * https://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.hateoas.mediatype.hal.forms; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.springframework.hateoas.support.JsonPathUtils.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.WebClientConfigurer; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebFluxEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalFormsHandleApplicationJsonWebFluxIntegrationTest { + + @Autowired WebTestClient testClient; + + @BeforeEach + void setUp() { + WebFluxEmployeeController.reset(); + } + + /** + * @see #728 + */ + @Test + void singleEmployee() { + + this.testClient.get().uri("http://localhost/employees/0").accept(MediaType.APPLICATION_JSON).exchange() + + .expectStatus().isOk() // + .expectHeader().contentType(MediaType.APPLICATION_JSON) // + .expectBody(String.class)// + + .value(jsonPath("$.name", is("Frodo Baggins"))) // + .value(jsonPath("$.role", is("ring bearer"))) // + + .value(jsonPath("$._links.*", hasSize(2))) // + .value(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) // + .value(jsonPath("$._links['employees'].href", is("http://localhost/employees"))) // + + .value(jsonPath("$._templates.*", hasSize(2))) // + .value(jsonPath("$._templates['default'].method", is("put"))) // + .value(jsonPath("$._templates['default'].properties[0].name", is("name"))) // + .value(jsonPath("$._templates['default'].properties[0].required", is(true))) // + .value(jsonPath("$._templates['default'].properties[1].name", is("role"))) // + .value(jsonPath("$._templates['default'].properties[1].required").doesNotExist()) // + + .value(jsonPath("$._templates['partiallyUpdateEmployee'].method", is("patch"))) // + .value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].name", is("name"))) // + .value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].required").doesNotExist()) // + .value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].name", is("role"))) // + .value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required").doesNotExist()); + } + + /** + * @see #728 + */ + @Test + void collectionOfEmployees() { + + this.testClient.get().uri("http://localhost/employees").accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .isOk().expectHeader().contentType(MediaType.APPLICATION_JSON).expectBody(String.class) + .value(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .value(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .value(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .value(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .value(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .value(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .value(jsonPath("$._links.*", hasSize(1))) + .value(jsonPath("$._links['self'].href", is("http://localhost/employees"))) + + .value(jsonPath("$._templates.*", hasSize(1))).value(jsonPath("$._templates['default'].method", is("post"))) + .value(jsonPath("$._templates['default'].properties[0].name", is("name"))) + .value(jsonPath("$._templates['default'].properties[0].required", is(true))) + .value(jsonPath("$._templates['default'].properties[1].name", is("role"))) + .value(jsonPath("$._templates['default'].properties[1].required").doesNotExist()); + } + + /** + * @see #728 + */ + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.testClient.post().uri("http://localhost/employees").contentType(MediaType.APPLICATION_JSON) + .bodyValue(specBasedJson) // + .exchange() // + .expectStatus().isCreated() // + .expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2"); + } + + @Configuration + @EnableWebFlux + @EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS }) + static class TestConfig { + + @Bean + WebFluxEmployeeController employeeController() { + return new WebFluxEmployeeController(); + } + + @Bean + WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) { + + return WebTestClient.bindToApplicationContext(ctx).build().mutate() + .exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build(); + } + + @Bean + HalFormsConfiguration halFormsConfiguration() { + return new HalFormsConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebMvcIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebMvcIntegrationTest.java new file mode 100644 index 000000000..39c87e8e0 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsHandleApplicationJsonWebMvcIntegrationTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2017-2019 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 + * + * https://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.hateoas.mediatype.hal.forms; + +import static org.assertj.core.api.Assertions.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.Links; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; +import org.springframework.hateoas.support.MappingUtils; +import org.springframework.hateoas.support.WebMvcEmployeeController; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +class HalFormsHandleApplicationJsonWebMvcIntegrationTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + + this.mockMvc = webAppContextSetup(this.context).build(); + WebMvcEmployeeController.reset(); + } + + @Test + void singleEmployee() throws Exception { + + this.mockMvc.perform(get("/employees/0").accept(MediaType.APPLICATION_JSON)) // + + .andExpect(status().isOk()) // + .andExpect(jsonPath("$.name", is("Frodo Baggins"))) // + .andExpect(jsonPath("$.role", is("ring bearer"))) + + .andExpect(jsonPath("$._links.*", hasSize(2))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._links['employees'].href", is("http://localhost/employees"))) + + .andExpect(jsonPath("$._templates.*", hasSize(2))) + .andExpect(jsonPath("$._templates['default'].method", is("put"))) + .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['default'].properties[0].required").value(true)) + .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['default'].properties[1].required").doesNotExist()) + + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].method", is("patch"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].required").doesNotExist()) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required").doesNotExist()); + } + + @Test + void collectionOfEmployees() throws Exception { + + this.mockMvc.perform(get("/employees").accept(MediaType.APPLICATION_JSON)) // + .andExpect(status().isOk()) // + .andExpect(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .andExpect(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .andExpect(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + + .andExpect(jsonPath("$._links.*", hasSize(1))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees"))) + + .andExpect(jsonPath("$._templates.*", hasSize(1))) + .andExpect(jsonPath("$._templates['default'].method", is("post"))) + .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['default'].properties[0].required").value(true)) + .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['default'].properties[1].required").doesNotExist()); + } + + @Test + void createNewEmployee() throws Exception { + + String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass())); + + this.mockMvc.perform(post("/employees") // + .content(specBasedJson) // + .contentType(MediaType.APPLICATION_JSON_VALUE)) // + .andExpect(status().isCreated()) + .andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2")); + } + + @Test // #832 + public void usesRegisteredHalFormsConfiguration() { + assertInstanceUsed(WithHalFormsConfiguration.class, WithHalFormsConfiguration.CONFIG); + } + + @Test // #832 + public void usesRegisteredHalConfiguration() { + assertInstanceUsed(WithHalConfiguration.class, WithHalConfiguration.CONFIG); + } + + private static void assertInstanceUsed(Class configurationClass, HalConfiguration configuration) { + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass)) { + + HalFormsMediaTypeConfiguration mediaTypeConfiguration = context.getBean(HalFormsMediaTypeConfiguration.class); + ObjectMapper mapper = mediaTypeConfiguration.configureObjectMapper(new ObjectMapper()); + + assertThatCode(() -> { + + JsonSerializer serializer = mapper.getSerializerProviderInstance() // + .findValueSerializer(Links.class); + + assertThat(serializer).isInstanceOfSatisfying(HalLinkListSerializer.class, it -> { + assertThat(ReflectionTestUtils.getField(serializer, "halConfiguration")).isSameAs(configuration); + }); + + }).doesNotThrowAnyException(); + } + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS }) + static class TestConfig { + + @Bean + WebMvcEmployeeController employeeController() { + return new WebMvcEmployeeController(); + } + + @Bean + HalFormsConfiguration halFormsConfiguration() { + return new HalFormsConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + } + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) + static class WithHalFormsConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + + @Bean + public HalFormsConfiguration halFormsConfiguration() { + + HalFormsConfiguration config = mock(HalFormsConfiguration.class); + when(config.getHalConfiguration()).thenReturn(CONFIG); + + return config; + } + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) + static class WithHalConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration().withAdditionalMediatype(MediaType.APPLICATION_JSON); + + @Bean + public HalConfiguration halConfiguration() { + return CONFIG; + } + } +} diff --git a/src/test/resources/org/springframework/hateoas/mediatype/hal/new-employee.json b/src/test/resources/org/springframework/hateoas/mediatype/hal/new-employee.json new file mode 100644 index 000000000..90fbaa4d2 --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/mediatype/hal/new-employee.json @@ -0,0 +1,7 @@ +{ + "name" : "Samwise Gamgee", + "role" : "gardener", + "_links" : { + + } +} \ No newline at end of file