Skip to content

Commit

Permalink
#1189 - Look for web annotations in interfaces.
Browse files Browse the repository at this point in the history
When forming links, look at a controller class's interface definitions for possible Spring Web annotations.

Related issues: spring-projects/spring-framework#15682
  • Loading branch information
gregturn committed Feb 7, 2020
1 parent 753ce92 commit 1452b38
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

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;
Expand Down Expand Up @@ -153,6 +155,9 @@ private static class AnnotationNamingMethodParameter extends SynthesizingMethodP
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.
Expand Down Expand Up @@ -190,5 +195,43 @@ public String getParameterName() {

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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.mvc.WebMvcLinkBuilder.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.test.StepVerifier;
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("/api?view=short", IanaLinkRelations.SELF));

return true;
}) //
.verifyComplete();
}

interface WebFluxInterface {

@GetMapping("/api")
RepresentationModel<?> root(@RequestParam String view);
}

@RestController
static class WebFluxClass implements WebFluxInterface {

@Override
public RepresentationModel<?> root(String view) {

RepresentationModel<?> model = new RepresentationModel<>();
model.add(linkTo(methodOn(WebFluxClass.class).root(view)).withSelfRel());
return model;
}
}

@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();
}
}

}

0 comments on commit 1452b38

Please sign in to comment.