Search code examples
javaspringrestspring-mvchateoas

Matrix Variables producing incorrect HATEOAS links


EDIT

I have tested this on two different application servers now: Tomcat 8.0.15 and JBoss 6.2 EAP. The link is generated correctly on Tomcat, while it is generated incorrectly on JBoss. I have also created a small sample project to demonstrate the problem:

https://github.com/Vile2539/hateoas-link-test/tree/master

Original Question

I've recently been changing some of my Spring REST services to use matrix variables instead of path variables. This, however, has led to some incorrect HATEOAS links being generated by the ControllerLinkBuilder.

What I currently have:

Controller Method

There's an overall @RequestMapping of /test-items on this. Also, from what I understand and have tested, the @PathVariable is required, as a URL template alone isn't enough. That's why the {fullString} exists.

@RequestMapping(value = "/{fullString}/test", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"})
@ResponseBody
public PagedCollectionResource test(@PathVariable String fullString, @MatrixVariable String testVariable, Pageable pageable) {
    Link link = linkTo(methodOn(TestControllerImpl.class).test(fullString, testVariable, pageable)).withSelfRel();
    Page<TestItem> testItems = testService.getPagedTestItems(testVariable, pageable);
    return testService.getPagedCollectionResource(testItems, link);
}

Spring Config

<mvc:annotation-driven enable-matrix-variables="true">
    <mvc:path-matching path-helper="pathHelper"/>
    <mvc:argument-resolvers>
        <bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver"/>
    </mvc:argument-resolvers>
</mvc:annotation-driven>

<bean id="pathHelper" class="org.springframework.web.util.UrlPathHelper">
    <property name="alwaysUseFullPath" value="true"/>
    <property name="urlDecode" value="false"/>
    <property name="removeSemicolonContent" value="false"/>
</bean>

Service Call

http://localhost:8080/testItem/test-items/stringhere;testVariable=036/test

Result

"links":[{"rel":"self","href":"http://localhost:8080/testItem/test-items/stringhere/test-items/stringhere;testVariable=036/test{?page,size,sort}"}]

As you can see, there's an additional /test-items/stringhere for some reason. I'm almost certain that it's caused by the matrix varible, and it seems to be during the ControllerLinkBuilder methodOn call, but I cannot figure out why.

Would anyone have any suggestions on how to fix this issue, or (ideally) fix it and eliminate the need for the {fullString} path variable?

Additional Question

Additionally, the reason for using the matrix variables is to get around a URL encoding issue. I was previously separating the path variables with a hyphen -, so something like:

{variable1}-{variable2}

Unfortunately, both can contain special characters, such as /, -, &, etc. Using the alwaysUseFullPath and urlDecode properties seen above, I was able to pass in the majority of special characters encoded - so calling the above with forward slashes would be:

/test-items/%2Fab-v%2Fc/test
variable1 = /ab
variable2 = v/c

This didn't, however, work for hyphens - encoded hyphens would be unencoded and result in the wrong path variables:

/test-items/a%2db-vc/test
variable1 = a
variable2 = b-vc

Double encoding the hyphens, however, does work - but is obviously a horrible hack.

If anyone has any suggestions on getting around this while maintaining the GET call, I'd be very appreciative. No assumptions can be made about the order or type of characters in the URL (so the path variables can't simply be separated with 2 hyphens).


Solution

  • It's a bit of a late update, but was swamped with work.

    I solved this issue by extending the LinkBuilderSupport for my own LinkBuilder class:

    public class RootContextLinkBuilder extends LinkBuilderSupport<RootContextLinkBuilder> {
        ...
    }
    

    The problem with the initial implementation was in the getBuilder() method. The ControllerLinkBuilder used ServletUriComponentsBuilder.fromServletMapping(request);, which I changed to ServletUriComponentsBuilder.fromContextPath(request);. This now generates the correct href, and is called in the same manner as the ControllerLinkBuilder.

    For my additional question, we stuck with the matrix variables, and so didn't solve this.