diff --git a/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java b/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java index 1a8177f0..bcb901ce 100644 --- a/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java +++ b/src/main/java/org/springframework/hateoas/server/core/MethodParameters.java @@ -17,6 +17,8 @@ package org.springframework.hateoas.server.core; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -153,6 +155,9 @@ public class MethodParameters { private final AnnotationAttribute attribute; private String name; + @Nullable + private volatile Annotation[] combinedAnnotations; + /** * Creates a new {@link AnnotationNamingMethodParameter} for the given {@link Method}'s parameter with the given * index. @@ -190,5 +195,43 @@ public class MethodParameters { return super.getParameterName(); } + + /** + * Return the annotations associated with the specific method/constructor parameter and any parent interfaces. + */ + @Override + public Annotation[] getParameterAnnotations() { + Annotation[] anns = this.combinedAnnotations; + if (anns == null) { + anns = super.getParameterAnnotations(); + Class[] interfaces = getDeclaringClass().getInterfaces(); + for (Class iface : interfaces) { + try { + Method method = iface.getMethod(getExecutable().getName(), getExecutable().getParameterTypes()); + Annotation[] paramAnns = method.getParameterAnnotations()[getParameterIndex()]; + if (paramAnns.length > 0) { + List merged = new ArrayList<>(anns.length + paramAnns.length); + merged.addAll(Arrays.asList(anns)); + for (Annotation fieldAnn : paramAnns) { + boolean existingType = false; + for (Annotation ann : anns) { + if (ann.annotationType() == fieldAnn.annotationType()) { + existingType = true; + break; + } + } + if (!existingType) { + merged.add(fieldAnn); + } + } + anns = merged.toArray(new Annotation[]{}); + } + } catch (NoSuchMethodException ex) { + } + } + this.combinedAnnotations = anns; + } + return anns; + } } } diff --git a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderInterfaceClassTest.java b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderInterfaceClassTest.java new file mode 100644 index 00000000..652c07fe --- /dev/null +++ b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderInterfaceClassTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020 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.server.mvc; + +import static org.hamcrest.Matchers.*; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +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.hateoas.MediaTypes; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +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.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +public class WebMvcLinkBuilderInterfaceClassTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + this.mockMvc = webAppContextSetup(this.context).build(); + } + + @Test + void parentInterfaceCanHoldSpringWebAnnotations() throws Exception { + + this.mockMvc.perform(get("http://example.com/api?view=short").accept(MediaTypes.HAL_JSON_VALUE)) // + .andDo(print()) // + .andExpect(status().isOk()) // + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) // + .andExpect(jsonPath("$._links.*", hasSize(1))) // + .andExpect(jsonPath("$._links.self.href", is("http://example.com/api?view=short"))); + } + + interface WebMvcInterface { + + @GetMapping("/api") + RepresentationModel root(@RequestParam String view); + } + + @RestController + static class WebMvcClass implements WebMvcInterface { + + @Override + public RepresentationModel root(String view) { + + RepresentationModel model = new RepresentationModel<>(); + model.add(linkTo(methodOn(WebMvcClass.class).root(view)).withSelfRel()); + return model; + } + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = { HypermediaType.HAL }) + static class TestConfig { + + @Bean + WebMvcClass concreteController() { + return new WebMvcClass(); + } + } + +} diff --git a/src/test/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilderInterfaceClassTest.java b/src/test/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilderInterfaceClassTest.java new file mode 100644 index 00000000..65573920 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/server/reactive/WebFluxLinkBuilderInterfaceClassTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020 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.server.reactive; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +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.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.WebClientConfigurer; +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.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +public class WebFluxLinkBuilderInterfaceClassTest { + + @Autowired WebTestClient testClient; + + @Test + void parentInterfaceCanHoldSpringWebAnnotations() throws Exception { + + this.testClient.get().uri("http://example.com/api?view=short") // + .accept(MediaTypes.HAL_JSON) // + .exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_JSON) // + .returnResult(RepresentationModel.class) // + .getResponseBody() // + .as(StepVerifier::create) // + .expectNextMatches(resourceSupport -> { + + assertThat(resourceSupport.getLinks())// + .containsExactly(Link.of("http://example.com/api?view=short", IanaLinkRelations.SELF)); + + return true; + }) // + .verifyComplete(); + } + + interface WebFluxInterface { + + @GetMapping("/api") + Mono> root(@RequestParam String view); + } + + @RestController + static class WebFluxClass implements WebFluxInterface { + + @Override + public Mono> root(String view) { + + Mono selfLink = linkTo(methodOn(WebFluxClass.class).root(view)).withSelfRel().toMono(); + + return selfLink.map(RepresentationModel::new); + } + } + + @Configuration + @EnableWebFlux + @EnableHypermediaSupport(type = { HypermediaType.HAL }) + static class TestConfig { + + @Bean + WebFluxClass concreteController() { + return new WebFluxClass(); + } + + @Bean + WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) { + + return WebTestClient.bindToApplicationContext(ctx).build().mutate() + .exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build(); + } + } + +}