Skip to content

Commit 3f8b90e

Browse files
committed
#33 - ControllerLinkBuilder now regards X-Forwarded-Host header.
When setting up the builder we now set the host to the value held in the X-Forwarded-Host [0] header if present. This allows to render appropriate URIs in reverse proxy scenarios. This is essentially an extension of ServletUriComponentsBuilder that will be fixed in there eventually. See SPR-10110 [1] for details. [0] http://tools.ietf.org/html/draft-ietf-appsawg-http-forwarded-10 [1] https://jira.springsource.org/browse/SPR-10110
1 parent 25f68fa commit 3f8b90e

File tree

4 files changed

+70
-17
lines changed

4 files changed

+70
-17
lines changed

src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Iterator;
2222
import java.util.Map;
2323

24+
import javax.servlet.http.HttpServletRequest;
25+
2426
import org.aopalliance.intercept.MethodInvocation;
2527
import org.springframework.hateoas.Link;
2628
import org.springframework.hateoas.core.AnnotationAttribute;
@@ -30,8 +32,12 @@
3032
import org.springframework.hateoas.core.LinkBuilderSupport;
3133
import org.springframework.hateoas.core.MappingDiscoverer;
3234
import org.springframework.util.Assert;
35+
import org.springframework.util.StringUtils;
3336
import org.springframework.web.bind.annotation.PathVariable;
3437
import org.springframework.web.bind.annotation.RequestMapping;
38+
import org.springframework.web.context.request.RequestAttributes;
39+
import org.springframework.web.context.request.RequestContextHolder;
40+
import org.springframework.web.context.request.ServletRequestAttributes;
3541
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
3642
import org.springframework.web.util.UriComponentsBuilder;
3743
import org.springframework.web.util.UriTemplate;
@@ -79,7 +85,7 @@ public static ControllerLinkBuilder linkTo(Class<?> controller, Object... parame
7985

8086
Assert.notNull(controller);
8187

82-
ControllerLinkBuilder builder = new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping());
88+
ControllerLinkBuilder builder = new ControllerLinkBuilder(getBuilder());
8389
String mapping = DISCOVERER.getMapping(controller);
8490

8591
if (mapping == null) {
@@ -95,7 +101,7 @@ public static ControllerLinkBuilder linkTo(Method method, Object... parameters)
95101

96102
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
97103
URI uri = template.expand(parameters);
98-
return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri);
104+
return new ControllerLinkBuilder(getBuilder()).slash(uri);
99105
}
100106

101107
/**
@@ -141,7 +147,7 @@ public static ControllerLinkBuilder linkTo(Object invocationValue) {
141147
values.putAll(accessor.getBoundParameters(invocation));
142148
URI uri = template.expand(values);
143149

144-
return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri);
150+
return new ControllerLinkBuilder(getBuilder()).slash(uri);
145151
}
146152

147153
/**
@@ -173,4 +179,38 @@ protected ControllerLinkBuilder getThis() {
173179
protected ControllerLinkBuilder createNewInstance(UriComponentsBuilder builder) {
174180
return new ControllerLinkBuilder(builder);
175181
}
182+
183+
/**
184+
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with the host tweaked in case the
185+
* request contains an {@code X-Forwarded-Host} header.
186+
*
187+
* @return
188+
*/
189+
private static UriComponentsBuilder getBuilder() {
190+
191+
HttpServletRequest request = getCurrentRequest();
192+
ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
193+
194+
String header = request.getHeader("X-Forwarded-Host");
195+
if (StringUtils.hasText(header)) {
196+
builder.host(header);
197+
}
198+
199+
return builder;
200+
}
201+
202+
/**
203+
* Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110 gets fixed.
204+
*
205+
* @return
206+
*/
207+
private static HttpServletRequest getCurrentRequest() {
208+
209+
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
210+
Assert.state(requestAttributes != null, "Could not find current request via RequestContextHolder");
211+
Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
212+
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
213+
Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
214+
return servletRequest;
215+
}
176216
}

src/test/java/org/springframework/hateoas/TestUtils.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
2020

21-
import javax.servlet.http.HttpServletRequest;
22-
2321
import org.junit.Before;
2422
import org.springframework.mock.web.MockHttpServletRequest;
2523
import org.springframework.web.context.request.RequestContextHolder;
@@ -32,10 +30,12 @@
3230
*/
3331
public class TestUtils {
3432

33+
protected MockHttpServletRequest request;
34+
3535
@Before
3636
public void setUp() {
3737

38-
HttpServletRequest request = new MockHttpServletRequest();
38+
request = new MockHttpServletRequest();
3939
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(request);
4040
RequestContextHolder.setRequestAttributes(requestAttributes);
4141
}

src/test/java/org/springframework/hateoas/mvc/ControllerLinkBuilderUnitTest.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717

1818
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
20-
import static org.mockito.Mockito.*;
2120
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
2221

2322
import org.hamcrest.Matchers;
2423
import org.junit.Test;
24+
import org.mockito.Mockito;
2525
import org.springframework.hateoas.Identifiable;
2626
import org.springframework.hateoas.Link;
2727
import org.springframework.hateoas.TestUtils;
@@ -48,23 +48,23 @@ public void createsLinkToParameterizedControllerRoot() {
4848

4949
Link link = linkTo(PersonsAddressesController.class, 15).withSelfRel();
5050
assertThat(link.getRel(), is(Link.REL_SELF));
51-
assertThat(link.getHref(), Matchers.endsWith("/people/15/addresses"));
51+
assertThat(link.getHref(), endsWith("/people/15/addresses"));
5252
}
5353

5454
@Test
5555
public void createsLinkToSubResource() {
5656

5757
Link link = linkTo(PersonControllerImpl.class).slash("something").withSelfRel();
5858
assertThat(link.getRel(), is(Link.REL_SELF));
59-
assertThat(link.getHref(), Matchers.endsWith("/people/something"));
59+
assertThat(link.getHref(), endsWith("/people/something"));
6060
}
6161

6262
@Test
6363
public void createsLinkWithCustomRel() {
6464

6565
Link link = linkTo(PersonControllerImpl.class).withRel(Link.REL_NEXT);
6666
assertThat(link.getRel(), is(Link.REL_NEXT));
67-
assertThat(link.getHref(), Matchers.endsWith("/people"));
67+
assertThat(link.getHref(), endsWith("/people"));
6868
}
6969

7070
@Test(expected = IllegalStateException.class)
@@ -81,35 +81,47 @@ public void createsLinkToUnmappedController() {
8181
@SuppressWarnings("unchecked")
8282
public void usesIdOfIdentifyableForPathSegment() {
8383

84-
Identifiable<Long> identifyable = mock(Identifiable.class);
85-
when(identifyable.getId()).thenReturn(10L);
84+
Identifiable<Long> identifyable = Mockito.mock(Identifiable.class);
85+
Mockito.when(identifyable.getId()).thenReturn(10L);
8686

8787
Link link = linkTo(PersonControllerImpl.class).slash(identifyable).withSelfRel();
88-
assertThat(link.getHref(), Matchers.endsWith("/people/10"));
88+
assertThat(link.getHref(), endsWith("/people/10"));
8989
}
9090

9191
@Test
9292
public void appendingNullIsANoOp() {
9393

9494
Link link = linkTo(PersonControllerImpl.class).slash(null).withSelfRel();
95-
assertThat(link.getHref(), Matchers.endsWith("/people"));
95+
assertThat(link.getHref(), endsWith("/people"));
9696

9797
link = linkTo(PersonControllerImpl.class).slash((Object) null).withSelfRel();
98-
assertThat(link.getHref(), Matchers.endsWith("/people"));
98+
assertThat(link.getHref(), endsWith("/people"));
9999
}
100100

101101
@Test
102102
public void linksToMethod() {
103103

104104
Link link = linkTo(methodOn(ControllerWithMethods.class).myMethod(null)).withSelfRel();
105-
assertThat(link.getHref(), Matchers.endsWith("/something/else"));
105+
assertThat(link.getHref(), endsWith("/something/else"));
106106
}
107107

108108
@Test
109109
public void linksToMethodWithPathVariable() {
110110

111111
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).withSelfRel();
112-
assertThat(link.getHref(), Matchers.endsWith("/something/1/foo"));
112+
assertThat(link.getHref(), endsWith("/something/1/foo"));
113+
}
114+
115+
/**
116+
* @see #33
117+
*/
118+
@Test
119+
public void usesForwardedHostAsHostIfHeaderIsSet() {
120+
121+
request.addHeader("X-Forwarded-Host", "somethingDifferent");
122+
123+
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
124+
assertThat(link.getHref(), startsWith("http://somethingDifferent"));
113125
}
114126

115127
static class Person implements Identifiable<Long> {

template.mf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Bundle-ManifestVersion: 2
55
Import-Template:
66
com.fasterxml.jackson.*;version="${jackson2.version:[=.=.=,+1.0.0)}";resolution:=optional,
77
com.jayway.jsonpath.*;version="${jsonpath.version:[=.=.=,+1.0.0)}";resolution:=optional,
8+
javax.servlet.*;version="[2.5,4.0)";resolution:=optional,
89
javax.ws.rs.*;version="${jaxrs.version:[=.=.=,+1.0.0)}";resolution:=optional,
910
javax.xml.bind.*;version="0",
1011
net.minidev.json.*;version="${minidevjson.version:[=.=.=,+1.0.0)}";resolution:=optional,

0 commit comments

Comments
 (0)