#1189 - Look for web annotations in interfaces.
When forming links, look at a controller class's interface definitions for possible Spring Web annotations. Related issues: spring-projects/spring-framework#15682 Original pull request: #1194.
This commit is contained in:
committed by
Oliver Drotbohm
parent
d1c1fbb01f
commit
d499b63c9d
@@ -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<Annotation> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<RepresentationModel<?>> root(@RequestParam String view);
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class WebFluxClass implements WebFluxInterface {
|
||||
|
||||
@Override
|
||||
public Mono<RepresentationModel<?>> root(String view) {
|
||||
|
||||
Mono<Link> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user