diff --git a/src/main/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscoverer.java new file mode 100644 index 000000000..db1fcea7a --- /dev/null +++ b/src/main/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscoverer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019-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 java.util.Optional.ofNullable; + +import org.springframework.core.env.PropertyResolver; +import org.springframework.hateoas.server.core.MappingDiscoverer; +import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; +import org.springframework.web.context.ContextLoader; + +import java.lang.reflect.Method; +import java.util.Collection; + +/** + * Property resolving adapter of {@link MappingDiscoverer}. + * + * @author Lars Michele + */ +public class PropertyResolvingMappingDiscoverer implements MappingDiscoverer { + + private final MappingDiscoverer delegate; + + private PropertyResolvingMappingDiscoverer(MappingDiscoverer delegate) { + this.delegate = delegate; + } + + public static PropertyResolvingMappingDiscoverer of(MappingDiscoverer delegate) { + return new PropertyResolvingMappingDiscoverer(delegate); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class) + */ + @Nullable + @Override + public String getMapping(Class type) { + return ofNullable(delegate.getMapping(type)).map(getPropertyResolver()::resolvePlaceholders).orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.reflect.Method) + */ + @Nullable + @Override + public String getMapping(Method method) { + return ofNullable(delegate.getMapping(method)).map(getPropertyResolver()::resolvePlaceholders).orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class, java.lang.reflect.Method) + */ + @Nullable + @Override + public String getMapping(Class type, Method method) { + return ofNullable(delegate.getMapping(type, method)).map(getPropertyResolver()::resolvePlaceholders).orElse(null); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getRequestMethod(java.lang.Class, java.lang.reflect.Method) + */ + @Override + public Collection getRequestMethod(Class type, Method method) { + return delegate.getRequestMethod(type, method); + } + + private static PropertyResolver getPropertyResolver() { + return ContextLoader.getCurrentWebApplicationContext().getEnvironment(); + } +} diff --git a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java index 0d1934974..921aaa666 100644 --- a/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilder.java @@ -47,12 +47,13 @@ * @author Andrew Naydyonock * @author Oliver Trosien * @author Greg Turnquist + * @author Lars Michele */ @SuppressWarnings("deprecation") public class WebMvcLinkBuilder extends TemplateVariableAwareLinkBuilderSupport { private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer - .of(new AnnotationMappingDiscoverer(RequestMapping.class)); + .of(PropertyResolvingMappingDiscoverer.of(new AnnotationMappingDiscoverer(RequestMapping.class))); private static final WebMvcLinkBuilderFactory FACTORY = new WebMvcLinkBuilderFactory(); private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler(); diff --git a/src/test/java/org/springframework/hateoas/TestUtils.java b/src/test/java/org/springframework/hateoas/TestUtils.java index 9acedb1b3..db206857b 100644 --- a/src/test/java/org/springframework/hateoas/TestUtils.java +++ b/src/test/java/org/springframework/hateoas/TestUtils.java @@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; +import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -86,4 +87,7 @@ public static void assertNotEqualAndDifferentHashCode(Object left, Object right) assertThat(left.hashCode()).isNotEqualTo(right.hashCode()); assertThat(left.toString()).isNotEqualTo(right.toString()); } + + @Configuration + public static class Config {} } diff --git a/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java b/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java index 14df898cc..245e649f1 100755 --- a/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/core/ControllerEntityLinksUnitTest.java @@ -26,10 +26,12 @@ import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.TestUtils; import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.ExposesResourceFor; @@ -37,8 +39,12 @@ import org.springframework.hateoas.server.LinkBuilderFactory; import org.springframework.hateoas.server.TypedEntityLinks; import org.springframework.hateoas.server.TypedEntityLinks.ExtendedTypedEntityLinks; +import org.springframework.mock.web.MockServletContext; import org.springframework.stereotype.Controller; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; /** * Unit tests for {@link ControllerEntityLinks}. @@ -46,10 +52,20 @@ * @author Oliver Gierke */ @ExtendWith(MockitoExtension.class) +@SpringJUnitWebConfig(classes = TestUtils.Config.class) class ControllerEntityLinksUnitTest extends TestUtils { @Mock LinkBuilderFactory linkBuilderFactory; + @Autowired + WebApplicationContext context; + + @BeforeEach + void contextLoading() { + ContextLoader contextLoader = new ContextLoader(context); + contextLoader.initWebApplicationContext(new MockServletContext()); + } + @Test void rejectsUnannotatedController() { diff --git a/src/test/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscovererUnitTest.java new file mode 100755 index 000000000..a2f972051 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/server/mvc/PropertyResolvingMappingDiscovererUnitTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-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.assertj.core.api.Assertions.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.TestUtils; +import org.springframework.hateoas.server.core.AnnotationMappingDiscoverer; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; + +/** + * Unit tests for {@link PropertyResolvingMappingDiscoverer}. + * + * @author Lars Michele + */ +@SpringJUnitWebConfig(classes = TestUtils.Config.class) +@TestPropertySource(properties = {"test.parent=resolvedparent", "test.child=resolvedchild"}) +class PropertyResolvingMappingDiscovererUnitTest extends TestUtils { + + @Autowired + WebApplicationContext context; + + @BeforeEach + void contextLoading() { + ContextLoader contextLoader = new ContextLoader(context); + contextLoader.initWebApplicationContext(new MockServletContext()); + } + + /** + * @see #361 + */ + @Test + void resolvesVariablesInMappings() throws NoSuchMethodException { + Method method = ResolveMethodEndpointController.class.getMethod("method"); + AnnotationMappingDiscoverer annotationMappingDiscoverer = new AnnotationMappingDiscoverer(RequestMapping.class); + + // Test plain AnnotationMappingDiscoverer first + assertThat(annotationMappingDiscoverer.getMapping(ResolveEndpointController.class)).isEqualTo("/${test.parent}"); + assertThat(annotationMappingDiscoverer.getMapping(ResolveMethodEndpointController.class, method)) + .isEqualTo("/${test.parent}/${test.child}"); + + PropertyResolvingMappingDiscoverer propertyResolvingMappingDiscoverer = PropertyResolvingMappingDiscoverer + .of(annotationMappingDiscoverer); + + assertThat(propertyResolvingMappingDiscoverer.getMapping(ResolveEndpointController.class)).isEqualTo("/resolvedparent"); + assertThat(propertyResolvingMappingDiscoverer.getMapping(method)).isEqualTo("/resolvedparent/resolvedchild"); + assertThat(propertyResolvingMappingDiscoverer.getMapping(ResolveMethodEndpointController.class, method)) + .isEqualTo("/resolvedparent/resolvedchild"); + } + + @RequestMapping("/${test.parent}") + interface ResolveEndpointController {} + + @RequestMapping("/${test.parent}") + interface ResolveMethodEndpointController { + + @RequestMapping("/${test.child}") + void method(); + } +} diff --git a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java index a61cfeb9e..da8aa3c04 100644 --- a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactoryUnitTest.java @@ -25,7 +25,9 @@ import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; @@ -36,11 +38,15 @@ import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderUnitTest.PersonControllerImpl; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderUnitTest.PersonsAddressesController; import org.springframework.http.HttpEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.util.UriComponentsBuilder; /** @@ -51,8 +57,18 @@ * @author Kamill Sokol * @author Ross Turner */ +@SpringJUnitWebConfig(classes = TestUtils.Config.class) class WebMvcLinkBuilderFactoryUnitTest extends TestUtils { + @Autowired + WebApplicationContext context; + + @BeforeEach + void contextLoading() { + ContextLoader contextLoader = new ContextLoader(context); + contextLoader.initWebApplicationContext(new MockServletContext()); + } + WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory(); @Test diff --git a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderUnitTest.java index 90999f54a..2fee4f86e 100644 --- a/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/mvc/WebMvcLinkBuilderUnitTest.java @@ -23,7 +23,9 @@ import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariable; @@ -31,6 +33,8 @@ import org.springframework.hateoas.TestUtils; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -38,6 +42,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -53,8 +59,18 @@ * @author Oliver Trosien * @author Greg Turnquist */ +@SpringJUnitWebConfig(classes = TestUtils.Config.class) class WebMvcLinkBuilderUnitTest extends TestUtils { + @Autowired + WebApplicationContext context; + + @BeforeEach + void contextLoading() { + ContextLoader contextLoader = new ContextLoader(context); + contextLoader.initWebApplicationContext(new MockServletContext()); + } + @Test void createsLinkToControllerRoot() {