Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow creation of UriTemplates when pointing to controller methods. #169

Closed
Gotusso opened this issue Apr 14, 2014 · 29 comments
Closed

Allow creation of UriTemplates when pointing to controller methods. #169

Gotusso opened this issue Apr 14, 2014 · 29 comments
Assignees
Milestone

Comments

@Gotusso
Copy link

Gotusso commented Apr 14, 2014

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

@steventhomson
Copy link

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
    }
}

@Gotusso
Copy link
Author

Gotusso commented Apr 28, 2014

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.

@odrotbohm odrotbohm changed the title Return URL templates Allow creation of UriTemplates when pointing to controller methods. May 1, 2014
@odrotbohm odrotbohm added this to the 0.12 milestone May 1, 2014
@odrotbohm odrotbohm self-assigned this May 1, 2014
@odrotbohm odrotbohm modified the milestones: 0.12, 0.13 May 20, 2014
@ksokol
Copy link
Contributor

ksokol commented May 25, 2014

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());

@odrotbohm odrotbohm modified the milestones: 0.14, 0.13 Jun 18, 2014
@odrotbohm odrotbohm modified the milestones: 0.15, 0.14 Jun 30, 2014
@odrotbohm odrotbohm removed this from the 0.15 milestone Jul 8, 2014
@dschulten
Copy link
Contributor

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}

@jiwhiz
Copy link

jiwhiz commented Oct 22, 2014

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?

@meyertee
Copy link

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)

@odrotbohm odrotbohm added this to the 0.18 milestone Mar 27, 2015
@odrotbohm odrotbohm modified the milestones: 0.18, 0.19 Jun 1, 2015
@odrotbohm odrotbohm modified the milestone: 0.19 Aug 31, 2015
@lazee
Copy link

lazee commented Sep 13, 2015

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.

@jiwhiz
Copy link

jiwhiz commented Sep 17, 2015

@lazee +1

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

@mikerahmati
Copy link

+1

@Chumper
Copy link

Chumper commented Nov 9, 2015

👍

@dschulten
Copy link
Contributor

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.

@ryl
Copy link

ryl commented Jan 14, 2016

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.

@pax95
Copy link

pax95 commented Jan 15, 2016

+1

4 similar comments
@megglos
Copy link

megglos commented Apr 4, 2016

+1

@GDownes
Copy link

GDownes commented Apr 7, 2016

+1

@ptahchiev
Copy link

+1

@JoseAlavez
Copy link

+1

@osvaldopina
Copy link

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.

@pgrund
Copy link

pgrund commented Jun 29, 2016

+1

2 similar comments
@tmartinelli
Copy link

+1

@kranjcec
Copy link

kranjcec commented Jul 7, 2016

+1

@odrotbohm
Copy link
Member

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.

@johndeverall
Copy link

+1

2 similar comments
@arobirosa
Copy link

+1

@openwms
Copy link

openwms commented Dec 1, 2016

+1

@odrotbohm odrotbohm added this to the 0.22 milestone Dec 7, 2016
odrotbohm added a commit that referenced this issue Dec 7, 2016
…ller methods.

If not all parameters are handed to a fake controller method invocation, the resulting link is now becoming a URI template. That means there are some tiny changes to the general behavior:

1. Missing required parts (like a path segment) are not rejected immediately but only if the resulting link is expanded itself.
2. Missing request parameters now appear in the resulting link, either in mandatory form (e.g. ?foo={foo}) or optionally (e.g. /{?foo}). To resolve these, the Link has to be expanded in turn.
@odrotbohm
Copy link
Member

That should be in place now.

@rworsnop
Copy link

@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}

@odrotbohm
Copy link
Member

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.

@rworsnop
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests