Introduced MvcUriComponentsBuilder to create URIs pointing to controller methods.

MvcUriComponentsBuilder allows creating URIs that point to Spring MVC
controller methods annotated with @RequestMapping. It builds them by
exposing a mock method invocation API similar to Mockito, records the
method invocations and thus builds up the URI by inspecting the mapping
annotations and the parameters handed into the method invocations.

Introduced a new SPI UriComponentsContributor that should be implemented 
by HandlerMethodArgumentResolvers that actually contribute path segments 
or query parameters to a URI. While the newly introduced 
MvcUriComponentsBuilder looks up those UriComponentsContributor instances 
from the MVC configuration.

The MvcUriComponentsBuilderFactory (name to be discussed - MvcUris maybe?) 
prevents the multiple lookups by keeping the UriComponentsBuilder 
instances in an instance variable. So an instance of the factory could 
be exposed as Spring bean or through a HandlerMethodArgumentResolver to 
be injected into Controller methods.

Issue: SPR-10665, SPR-8826
This commit is contained in:
Oliver Gierke
2013-08-02 19:19:48 +02:00
committed by Rossen Stoyanchev
parent 92a48b72d7
commit 4fd27b12fc
18 changed files with 2036 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2012-2013 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
*
* http://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.web.servlet.hypermedia;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.core.AnnotationAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link AnnotationMappingDiscoverer}.
*
* @author Oliver Gierke
*/
public class AnnotationMappingDiscovererUnitTests {
AnnotationMappingDiscoverer discoverer = new AnnotationMappingDiscoverer(
RequestMapping.class);
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAnnotationType() {
new AnnotationMappingDiscoverer((Class<? extends Annotation>) null);
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAnnotationAttribute() {
new AnnotationMappingDiscoverer((AnnotationAttribute) null);
}
@Test
public void discoversTypeLevelMapping() {
assertThat(discoverer.getMapping(MyController.class), is("/type"));
}
@Test
public void discoversMethodLevelMapping() throws Exception {
Method method = MyController.class.getMethod("method");
assertThat(discoverer.getMapping(method), is("/type/method"));
}
@Test
public void returnsNullForNonExistentTypeLevelMapping() {
assertThat(discoverer.getMapping(ControllerWithoutTypeLevelMapping.class),
is(nullValue()));
}
@Test
public void resolvesMethodLevelMappingWithoutTypeLevelMapping() throws Exception {
Method method = ControllerWithoutTypeLevelMapping.class.getMethod("method");
assertThat(discoverer.getMapping(method), is("/method"));
}
@Test
public void resolvesMethodLevelMappingWithSlashRootMapping() throws Exception {
Method method = SlashRootMapping.class.getMethod("method");
assertThat(discoverer.getMapping(method), is("/method"));
}
/**
* @see #46
*/
@Test
public void treatsMissingMethodMappingAsEmptyMapping() throws Exception {
Method method = MyController.class.getMethod("noMethodMapping");
assertThat(discoverer.getMapping(method), is("/type"));
}
@RequestMapping("/type")
interface MyController {
@RequestMapping("/method")
void method();
@RequestMapping
void noMethodMapping();
}
interface ControllerWithoutTypeLevelMapping {
@RequestMapping("/method")
void method();
}
@RequestMapping("/")
interface SlashRootMapping {
@RequestMapping("/method")
void method();
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2013 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
*
* http://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.web.servlet.hypermedia;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.http.HttpEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonControllerImpl;
import org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilderUnitTests.PersonsAddressesController;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
/**
* Unit tests for {@link MvcUriComponentsBuilderFactory}.
*
* @author Ricardo Gladwell
* @author Oliver Gierke
*/
public class MvcUriComponentsBuilderFactoryUnitTests extends TestUtils {
List<UriComponentsContributor> contributors = Collections.emptyList();
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
@Test
public void createsLinkToControllerRoot() {
URI link = factory.from(PersonControllerImpl.class).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/people"));
}
@Test
public void createsLinkToParameterizedControllerRoot() {
URI link = factory.from(PersonsAddressesController.class, 15).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/people/15/addresses"));
}
@Test
public void appliesParameterValueIfContributorConfigured() {
List<? extends UriComponentsContributor> contributors = Arrays.asList(new SampleUriComponentsContributor());
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
SpecialType specialType = new SpecialType();
specialType.parameterValue = "value";
URI link = factory.from(
methodOn(SampleController.class).sampleMethod(1L, specialType)).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/sample/1?foo=value"));
}
/**
* @see #57
*/
@Test
public void usesDateTimeFormatForUriBinding() {
DateTime now = DateTime.now();
MvcUriComponentsBuilderFactory factory = new MvcUriComponentsBuilderFactory(
contributors);
URI link = factory.from(methodOn(SampleController.class).sampleMethod(now)).build().toUri();
assertThat(link.toString(),
endsWith("/sample/" + ISODateTimeFormat.date().print(now)));
}
static interface SampleController {
@RequestMapping("/sample/{id}")
HttpEntity<?> sampleMethod(@PathVariable("id") Long id, SpecialType parameter);
@RequestMapping("/sample/{time}")
HttpEntity<?> sampleMethod(
@PathVariable("time") @DateTimeFormat(iso = ISO.DATE) DateTime time);
}
static class SampleUriComponentsContributor implements UriComponentsContributor {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return SpecialType.class.equals(parameter.getParameterType());
}
@Override
public void enhance(UriComponentsBuilder builder, MethodParameter parameter,
Object value) {
builder.queryParam("foo", ((SpecialType) value).parameterValue);
}
}
static class SpecialType {
String parameterValue;
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright 2012-2013 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
*
* http://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.web.servlet.hypermedia;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.web.servlet.hypermedia.MvcUriComponentsBuilder.*;
/**
* Unit tests for {@link MvcUriComponentsBuilder}.
*
* @author Oliver Gierke
* @author Dietrich Schulten
*/
public class MvcUriComponentsBuilderUnitTests extends TestUtils {
@Test
public void createsLinkToControllerRoot() {
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), Matchers.endsWith("/people"));
}
@Test
public void createsLinkToParameterizedControllerRoot() {
URI link = from(PersonsAddressesController.class, 15).build().toUri();
assertThat(link.toString(), endsWith("/people/15/addresses"));
}
/**
* @see #70
*/
@Test
public void createsLinkToMethodOnParameterizedControllerRoot() {
URI link = from(
methodOn(PersonsAddressesController.class, 15).getAddressesForCountry(
"DE")).build().toUri();
assertThat(link.toString(), endsWith("/people/15/addresses/DE"));
}
@Test
public void createsLinkToSubResource() {
URI link = from(PersonControllerImpl.class).pathSegment("something").build().toUri();
assertThat(link.toString(), endsWith("/people/something"));
}
@Test(expected = IllegalStateException.class)
public void rejectsControllerWithMultipleMappings() {
from(InvalidController.class);
}
@Test
public void createsLinkToUnmappedController() {
URI link = from(UnmappedController.class).build().toUri();
assertThat(link.toString(), is("http://localhost/"));
}
@Test
public void appendingNullIsANoOp() {
URI link = from(PersonControllerImpl.class).path(null).build().toUri();
assertThat(link.toString(), endsWith("/people"));
}
@Test
public void linksToMethod() {
URI link = from(methodOn(ControllerWithMethods.class).myMethod(null)).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/something/else"));
}
@Test
public void linksToMethodWithPathVariable() {
URI link = from(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).build().toUri();
assertPointsToMockServer(link);
assertThat(link.toString(), endsWith("/something/1/foo"));
}
/**
* @see #33
*/
@Test
public void usesForwardedHostAsHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://somethingDifferent"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndRequestParams() {
URI link = from(
methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).build().toUri();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("offset"), contains("10"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndMultiValueRequestParams() {
URI link = from(
methodOn(ControllerWithMethods.class).methodWithMultiValueRequestParams(
"1", Arrays.asList(3, 7), 5)).build().toUri();
UriComponents components = toComponents(link);
assertThat(components.getPath(), is("/something/1/foo"));
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("items"), containsInAnyOrder("3", "7"));
}
/**
* @see #90
*/
@Test
public void usesForwardedHostAndPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobar:8088");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://foobar:8088"));
}
/**
* @see #90
*/
@Test
public void usesFirstHostOfXForwardedHost() {
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
URI link = from(PersonControllerImpl.class).build().toUri();
assertThat(link.toString(), startsWith("http://barfoo:8888"));
}
private static UriComponents toComponents(URI link) {
return UriComponentsBuilder.fromUri(link).build();
}
static class Person {
Long id;
public Long getId() {
return id;
}
}
@RequestMapping("/people")
interface PersonController {
}
class PersonControllerImpl implements PersonController {
}
@RequestMapping("/people/{id}/addresses")
static class PersonsAddressesController {
@RequestMapping("/{country}")
public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) {
return null;
}
}
@RequestMapping({ "/persons", "/people" })
class InvalidController {
}
class UnmappedController {
}
@RequestMapping("/something")
static class ControllerWithMethods {
@RequestMapping("/else")
HttpEntity<Void> myMethod(@RequestBody Object payload) {
return null;
}
@RequestMapping("/{id}/foo")
HttpEntity<Void> methodWithPathVariable(@PathVariable String id) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodForNextPage(@PathVariable String id,
@RequestParam Integer offset, @RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodWithMultiValueRequestParams(@PathVariable String id,
@RequestParam List<Integer> items, @RequestParam Integer limit) {
return null;
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012 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
*
* http://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.web.servlet.hypermedia;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Oliver Gierke
*/
public class RecordedInvocationUtilsUnitTests extends TestUtils {
@Test
public void test() {
MvcUriComponentsBuilder.from(RecordedInvocationUtils.methodOn(SampleController.class).someMethod(
1L));
}
@RequestMapping("/sample")
static class SampleController {
@RequestMapping("/{id}/foo")
HttpEntity<Void> someMethod(@PathVariable("id") Long id) {
return new ResponseEntity<Void>(HttpStatus.OK);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2013 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
*
* http://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.web.servlet.hypermedia;
import java.net.URI;
import org.junit.Before;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
* Utility class to ease tesing.
*
* @author Oliver Gierke
*/
public class TestUtils {
protected MockHttpServletRequest request;
@Before
public void setUp() {
request = new MockHttpServletRequest();
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(requestAttributes);
}
protected void assertPointsToMockServer(URI link) {
assertThat(link.toString(), startsWith("http://localhost"));
}
}