Skip to content

Commit 2919d60

Browse files
gregturnodrotbohm
authored andcommitted
#1197 - 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.
1 parent a044ac0 commit 2919d60

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

src/main/java/org/springframework/hateoas/server/core/MethodParameters.java

+43
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import java.lang.annotation.Annotation;
1919
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
2022
import java.util.List;
2123
import java.util.Map;
2224
import java.util.Optional;
@@ -153,6 +155,9 @@ private static class AnnotationNamingMethodParameter extends SynthesizingMethodP
153155
private final AnnotationAttribute attribute;
154156
private String name;
155157

158+
@Nullable
159+
private volatile Annotation[] combinedAnnotations;
160+
156161
/**
157162
* Creates a new {@link AnnotationNamingMethodParameter} for the given {@link Method}'s parameter with the given
158163
* index.
@@ -190,5 +195,43 @@ public String getParameterName() {
190195

191196
return super.getParameterName();
192197
}
198+
199+
/**
200+
* Return the annotations associated with the specific method/constructor parameter and any parent interfaces.
201+
*/
202+
@Override
203+
public Annotation[] getParameterAnnotations() {
204+
Annotation[] anns = this.combinedAnnotations;
205+
if (anns == null) {
206+
anns = super.getParameterAnnotations();
207+
Class<?>[] interfaces = getDeclaringClass().getInterfaces();
208+
for (Class<?> iface : interfaces) {
209+
try {
210+
Method method = iface.getMethod(getExecutable().getName(), getExecutable().getParameterTypes());
211+
Annotation[] paramAnns = method.getParameterAnnotations()[getParameterIndex()];
212+
if (paramAnns.length > 0) {
213+
List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length);
214+
merged.addAll(Arrays.asList(anns));
215+
for (Annotation fieldAnn : paramAnns) {
216+
boolean existingType = false;
217+
for (Annotation ann : anns) {
218+
if (ann.annotationType() == fieldAnn.annotationType()) {
219+
existingType = true;
220+
break;
221+
}
222+
}
223+
if (!existingType) {
224+
merged.add(fieldAnn);
225+
}
226+
}
227+
anns = merged.toArray(new Annotation[]{});
228+
}
229+
} catch (NoSuchMethodException ex) {
230+
}
231+
}
232+
this.combinedAnnotations = anns;
233+
}
234+
return anns;
235+
}
193236
}
194237
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.server.mvc;
17+
18+
import static org.hamcrest.Matchers.*;
19+
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
20+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
22+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
23+
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
24+
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.hateoas.MediaTypes;
32+
import org.springframework.hateoas.RepresentationModel;
33+
import org.springframework.hateoas.config.EnableHypermediaSupport;
34+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
35+
import org.springframework.http.HttpHeaders;
36+
import org.springframework.test.context.ContextConfiguration;
37+
import org.springframework.test.context.junit.jupiter.SpringExtension;
38+
import org.springframework.test.context.web.WebAppConfiguration;
39+
import org.springframework.test.web.servlet.MockMvc;
40+
import org.springframework.web.bind.annotation.GetMapping;
41+
import org.springframework.web.bind.annotation.RequestParam;
42+
import org.springframework.web.bind.annotation.RestController;
43+
import org.springframework.web.context.WebApplicationContext;
44+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
45+
46+
/**
47+
* @author Greg Turnquist
48+
*/
49+
@ExtendWith(SpringExtension.class)
50+
@WebAppConfiguration
51+
@ContextConfiguration
52+
public class WebMvcLinkBuilderInterfaceClassTest {
53+
54+
@Autowired WebApplicationContext context;
55+
56+
MockMvc mockMvc;
57+
58+
@BeforeEach
59+
void setUp() {
60+
this.mockMvc = webAppContextSetup(this.context).build();
61+
}
62+
63+
@Test
64+
void parentInterfaceCanHoldSpringWebAnnotations() throws Exception {
65+
66+
this.mockMvc.perform(get("http://example.com/api?view=short").accept(MediaTypes.HAL_JSON_VALUE)) //
67+
.andDo(print()) //
68+
.andExpect(status().isOk()) //
69+
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) //
70+
.andExpect(jsonPath("$._links.*", hasSize(1))) //
71+
.andExpect(jsonPath("$._links.self.href", is("http://example.com/api?view=short")));
72+
}
73+
74+
interface WebMvcInterface {
75+
76+
@GetMapping("/api")
77+
RepresentationModel<?> root(@RequestParam String view);
78+
}
79+
80+
@RestController
81+
static class WebMvcClass implements WebMvcInterface {
82+
83+
@Override
84+
public RepresentationModel<?> root(String view) {
85+
86+
RepresentationModel<?> model = new RepresentationModel<>();
87+
model.add(linkTo(methodOn(WebMvcClass.class).root(view)).withSelfRel());
88+
return model;
89+
}
90+
}
91+
92+
@Configuration
93+
@EnableWebMvc
94+
@EnableHypermediaSupport(type = { HypermediaType.HAL })
95+
static class TestConfig {
96+
97+
@Bean
98+
WebMvcClass concreteController() {
99+
return new WebMvcClass();
100+
}
101+
}
102+
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.server.reactive;
17+
18+
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
19+
import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*;
20+
21+
import reactor.core.publisher.Mono;
22+
import reactor.test.StepVerifier;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.hateoas.IanaLinkRelations;
31+
import org.springframework.hateoas.Link;
32+
import org.springframework.hateoas.MediaTypes;
33+
import org.springframework.hateoas.RepresentationModel;
34+
import org.springframework.hateoas.config.EnableHypermediaSupport;
35+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
36+
import org.springframework.hateoas.config.WebClientConfigurer;
37+
import org.springframework.test.context.ContextConfiguration;
38+
import org.springframework.test.context.junit.jupiter.SpringExtension;
39+
import org.springframework.test.context.web.WebAppConfiguration;
40+
import org.springframework.test.web.reactive.server.WebTestClient;
41+
import org.springframework.web.bind.annotation.GetMapping;
42+
import org.springframework.web.bind.annotation.RequestParam;
43+
import org.springframework.web.bind.annotation.RestController;
44+
import org.springframework.web.reactive.config.EnableWebFlux;
45+
46+
/**
47+
* @author Greg Turnquist
48+
*/
49+
@ExtendWith(SpringExtension.class)
50+
@WebAppConfiguration
51+
@ContextConfiguration
52+
public class WebFluxLinkBuilderInterfaceClassTest {
53+
54+
@Autowired WebTestClient testClient;
55+
56+
@Test
57+
void parentInterfaceCanHoldSpringWebAnnotations() throws Exception {
58+
59+
this.testClient.get().uri("http://example.com/api?view=short") //
60+
.accept(MediaTypes.HAL_JSON) //
61+
.exchange() //
62+
.expectStatus().isOk() //
63+
.expectHeader().contentType(MediaTypes.HAL_JSON) //
64+
.returnResult(RepresentationModel.class) //
65+
.getResponseBody() //
66+
.as(StepVerifier::create) //
67+
.expectNextMatches(resourceSupport -> {
68+
69+
assertThat(resourceSupport.getLinks())//
70+
.containsExactly(Link.of("http://example.com/api?view=short", IanaLinkRelations.SELF));
71+
72+
return true;
73+
}) //
74+
.verifyComplete();
75+
}
76+
77+
interface WebFluxInterface {
78+
79+
@GetMapping("/api")
80+
Mono<RepresentationModel<?>> root(@RequestParam String view);
81+
}
82+
83+
@RestController
84+
static class WebFluxClass implements WebFluxInterface {
85+
86+
@Override
87+
public Mono<RepresentationModel<?>> root(String view) {
88+
89+
Mono<Link> selfLink = linkTo(methodOn(WebFluxClass.class).root(view)).withSelfRel().toMono();
90+
91+
return selfLink.map(RepresentationModel::new);
92+
}
93+
}
94+
95+
@Configuration
96+
@EnableWebFlux
97+
@EnableHypermediaSupport(type = { HypermediaType.HAL })
98+
static class TestConfig {
99+
100+
@Bean
101+
WebFluxClass concreteController() {
102+
return new WebFluxClass();
103+
}
104+
105+
@Bean
106+
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
107+
108+
return WebTestClient.bindToApplicationContext(ctx).build().mutate()
109+
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
110+
}
111+
}
112+
113+
}

0 commit comments

Comments
 (0)