Skip to content

Commit 98400eb

Browse files
committed
#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
1 parent 753ce92 commit 98400eb

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-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,111 @@
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.mvc.WebMvcLinkBuilder.*;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import reactor.test.StepVerifier;
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.hateoas.IanaLinkRelations;
29+
import org.springframework.hateoas.Link;
30+
import org.springframework.hateoas.MediaTypes;
31+
import org.springframework.hateoas.RepresentationModel;
32+
import org.springframework.hateoas.config.EnableHypermediaSupport;
33+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
34+
import org.springframework.hateoas.config.WebClientConfigurer;
35+
import org.springframework.test.context.ContextConfiguration;
36+
import org.springframework.test.context.junit.jupiter.SpringExtension;
37+
import org.springframework.test.context.web.WebAppConfiguration;
38+
import org.springframework.test.web.reactive.server.WebTestClient;
39+
import org.springframework.web.bind.annotation.GetMapping;
40+
import org.springframework.web.bind.annotation.RequestParam;
41+
import org.springframework.web.bind.annotation.RestController;
42+
import org.springframework.web.reactive.config.EnableWebFlux;
43+
44+
/**
45+
* @author Greg Turnquist
46+
*/
47+
@ExtendWith(SpringExtension.class)
48+
@WebAppConfiguration
49+
@ContextConfiguration
50+
public class WebFluxLinkBuilderInterfaceClassTest {
51+
52+
@Autowired WebTestClient testClient;
53+
54+
@Test
55+
void parentInterfaceCanHoldSpringWebAnnotations() throws Exception {
56+
57+
this.testClient.get().uri("http://example.com/api?view=short") //
58+
.accept(MediaTypes.HAL_JSON) //
59+
.exchange() //
60+
.expectStatus().isOk() //
61+
.expectHeader().contentType(MediaTypes.HAL_JSON) //
62+
.returnResult(RepresentationModel.class) //
63+
.getResponseBody() //
64+
.as(StepVerifier::create) //
65+
.expectNextMatches(resourceSupport -> {
66+
67+
assertThat(resourceSupport.getLinks())//
68+
.containsExactly(Link.of("http://example.com/api?view=short", IanaLinkRelations.SELF));
69+
70+
return true;
71+
}) //
72+
.verifyComplete();
73+
}
74+
75+
interface WebFluxInterface {
76+
77+
@GetMapping("/api")
78+
RepresentationModel<?> root(@RequestParam String view);
79+
}
80+
81+
@RestController
82+
static class WebFluxClass implements WebFluxInterface {
83+
84+
@Override
85+
public RepresentationModel<?> root(String view) {
86+
87+
RepresentationModel<?> model = new RepresentationModel<>();
88+
model.add(linkTo(methodOn(WebFluxClass.class).root(view)).withSelfRel());
89+
return model;
90+
}
91+
}
92+
93+
@Configuration
94+
@EnableWebFlux
95+
@EnableHypermediaSupport(type = { HypermediaType.HAL })
96+
static class TestConfig {
97+
98+
@Bean
99+
WebFluxClass concreteController() {
100+
return new WebFluxClass();
101+
}
102+
103+
@Bean
104+
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
105+
106+
return WebTestClient.bindToApplicationContext(ctx).build().mutate()
107+
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
108+
}
109+
}
110+
111+
}

0 commit comments

Comments
 (0)