Spring-hateoas: Allow creation of UriTemplates when pointing to controller methods.

Created on 14 Apr 2014  路  29Comments  路  Source: spring-projects/spring-hateoas

Would be nice to have an easy way of return URL templates directly. For instance, suppose we have a method like

@RequestMapping
public HttpEntity<ListResource> list(
        @RequestParam(value = "query", required = false) String query,
        @RequestParam(value = "page", defaultValue = "1") Integer page,
        @RequestParam(value = "limit", defaultValue = "10") Integer limit) {
    // ...
}

I can reference an specific invocation with linkTo(methodOn(Controller.class).list("foo", 1, 20) but I think there is no easy way to reference a generic call without specifing the method parameters. Invoking with null results in a exception.

I would like to get something like

{
    rel: "list"
    href: "http://localhost:8080/myresource{?query,page,limit}"
}

Is that possible?
Regards

Most helpful comment

Just a quick heads up for you guys, we're going to have this fixed in a larger effort that will bring in an advanced accordances model built by @dschulten.

All 29 comments

It is possible. You should use PagedResources.

For Example:

@RequestMapping(method = RequestMethod.GET)
public HttpEntity<PagedResources<Resource<?>>> viewResources(Pageable pageable, PagedResourcesAssembler<?> assembler)
{
    ...
    // query your repository
    List<?> resources = ... 
    ...

    return new ResponseEntity<PagedResources<Resource<?>>>(assembler.toResource(new PageImpl<?>(
        resources,
        pageable,
        resources.size())), HttpStatus.OK);
}

? is whatever resource class you are querying.

The response should be like this:

{
    "links": [
        {
            "rel": "self",
            "href": "/rest/resources{?page,size,sort}"
        }
    ],
    "content": [
        ...
    ],
    "page": {
        "size": 20,
        "totalElements": 10,
        "totalPages": 1,
        "number": 0
    }
}

The key of your example seems to be PagedResourcesAssembler, as it's the responsible for build the link. However, this process looks a bit cumbersome for me, and would be nice to have a simpler method, and generic enough to add it to any resource, not just lists.

ControllerLinkBuilder has all ingredients it needs to build a template url. What is missing is a convenience method for this:

Link link = ControllerLinkBuilder.linkTo(methodOn(YourController.class).yourMethod(...).withRel("yourRel");
DummyInvocationUtils.LastInvocationAware invocations = (DummyInvocationUtils.LastInvocationAware) ControllerLinkBuilder.methodOn(YourController.class).yourMethod(...);
DummyInvocationUtils.MethodInvocation invocation = invocations.getLastInvocation();
Method method = invocation.getMethod();

MappingDiscoverer discoverer = new AnnotationMappingDiscoverer(RequestMapping.class); //taken from ControllerLinkBuilder
String mapping = discoverer.getMapping(YourController.class, method);

UriTemplate uriTemplate = new UriTemplate(mapping);
List<TemplateVariable> variables = link.getVariables();
//the templated link
Link templatedLink = new Link(uriTemplate.with(new TemplateVariables(variables)), link.getRel());

A possible workaround in getByXXX scenarios is a class-level RequestMapping such as @RequestMapping("/events") on an EventController. The 'default handler method' which returns events needs a no-value @RequestMapping annotation to make this work, and the method that returns single items by XXX must simply add the parameter to the path defined on class-level, like @RequestMapping("/id") .

@Controller
@RequestMapping("/events")
public class EventController {

    // 'default' handler
    @RequestMapping
    public @ResponseBody Resources<Resource<Event>> getEvents() {
    }

    @RequestMapping("/{id}") 
    public @ResponseBody Resource<Event> getEventById(@PathVariable int id) {
    }
    ...

Now, to point to getEventById with a templated URL you can say somewhere else:

new Link(linkTo(EventController.class).toString() + "{/eventId}", "event") 

resulting in a templated link likehttp://localhost/events{/eventId}

I have a similar issue, but a little tricky. My PublicBlogRestController has a method to get blog comment by ID:

    @RequestMapping(method = RequestMethod.GET, value = "/public/blogs/{blogId}/comments/{commentId}")
    public HttpEntity<PublicCommentResource> getBlogApprovedCommentPostById(
            @PathVariable("blogId") String blogId,
            @PathVariable("commentId") String commentId) {
...
    }

I want my PublicBlogResource will have a templated link to its comment, like "/public/blogs/blog123/comments/{commentId}".

Maybe UriTemplate class can partially expand the template with path parameter values, like setPathParameter(String, Object), so I can create link like this:

Link commentLink = new Link(
    new UriTemplate(baseUri+"/public/blogs/{blogId}/comments/{commentId}").setPathParameter("blogId", blog.getId()), 
    "comment");
        resource.add(commentLink);

Does it make sense? Or any other options?

I wonder if you guys have had any thoughts on this, a related problem also appears on this StackOverflow question. The solution of _Chris DaMour_ kind of works, but only for non-required request-params.
The same problem exists when using EntityLinks as it's using the ControllerLinkBuilder under the hood.

Digging into the code I didn't find an obvious solution, I think there are two underlying problems:

  • the escaping of _{_ and _}_ in path variables in HierarchicalUriComponents.encodeBytes() which can't be overridden. The only way I found is to replace _%7B_ and _%7D_ back to curly brackets in the resulting string.
  • the fact that UriComponents.expandUriComponent() doesn't accept null values for required attributes & path variables. The method is used by HierarchicalUriComponents expandInternal(), which is used by UriComponentsBuilder.buildAndExpand and ultimately by ControllerLinkBuilderFactory.linkTo() - the point being that it's also not easy to override or change.

I found out that when using EntityLinks.linkFor(Class<?> type, Object... parameters) you can pass UriComponents.UriTemplateVariables.SKIP_VALUE as parameters which causes UriComponents.expandUriComponent() to accept them even if they're required, that's the workaround I'm using for now (plus unescaping _{}_)

The workarounds are compilated and insufficient though, so it would be really nice to have a proper and easy way to generate templated links - they're so common in HAL, that it's painful to have to create them with hacks.

In terms of signature maybe extra methods on the LinkBuilder could work:

Link linkToCollectionResource(Class<?> type, TemplateVariables variables);
Link linkToSingleResource(Class<?> type, TemplateVariables variables);

It could then build the link using the variables set on the TemplateVariables instance and automatically construct a templated link... just a thought.
Something similar could be passed to the builder: linkTo(methodOn(Controller.class, variables).list(null, null, null)

The HAL implementation in this project is really useless without a good support for templated urls.

So, I really hope this could be prioritised. All other HAL implementations for Java has its own problems that prevents me from using it. HALBuilder for instance doesn't respect Jackson annotations in beans added to it.

@lazee +1

My resource assembler classes are full of hack to work around this issue. Hope @olivergierke can fix it ASAP :)

+1

:+1:

AffordanceBuilder from the feature/affordances branch, or from spring-hateoas-ext (maven released) allows to define templated URIs by passing null for variables that should be templated. Otherwise it aims to be a drop-in replacement for ControllerLinkBuilder. See also https://github.com/dschulten/hydra-java#affordancebuilder-for-rich-hyperlinks-from-v-0-2-0

We are working to get AffordanceBuilder into spring-hateoas, feedback appreciated.

I'm also running up against this problem. I would normally use spring data rest, but in this particular case, I'm working on a project with a backend for which there is no suitable spring-data-xxx (jpa, elasticsearch, mongodb, etc) flavor. My solution was to just use hateoas directly and expose things similar to how you would expect from spring data rest -- but without a means of creating templated urls, its really difficult. Would really like to see this feature.

+1

+1

+1

+1

+1

I had this problem in the project that I'm currently working and I developed a framework to create links and templates from controller calls. It also allows the use of spel expressions to control link rendering. In the next major version it will be possible to generate links and templates using only annotations. The repository is https://github.com/osvaldopina/linkbuilder. Any feedback will be appreciated.

+1

+1

+1

Just a quick heads up for you guys, we're going to have this fixed in a larger effort that will bring in an advanced accordances model built by @dschulten.

+1

+1

+1

That should be in place now.

@olivergierke I'm seeing templates generated in this format:
http://localhost/foo?a={a}&b={b}
For query parameters, shouldn't they look like this?
http://localhost/foo{?a,b}

That depends on whether the parameter is required (former format) or optional (latter format). Mixing the two, works, too, but if you see the former format, I am pretty sure, your @RequestParams are mapped with required = true.

Thanks for the quick response! Yes, that was exactly it.

On a related note, what should I see when @RequestParam has a defaultValue? I'm seeing the parameter omitted from the template.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ingogriebsch picture ingogriebsch  路  6Comments

jbcpollak picture jbcpollak  路  8Comments

dusan-turajlic picture dusan-turajlic  路  7Comments

Doogiemuc picture Doogiemuc  路  8Comments

pmihalcin picture pmihalcin  路  3Comments