Spring-hateoas: ControllerLinkBuilder does not take Spring Data REST's base path into account

Created on 15 Feb 2016  Â·  27Comments  Â·  Source: spring-projects/spring-hateoas

I have the repository REST base path set to /rest, and a @RepositoryRestController with some custom handlers:

@RepositoryRestController
public class RestController {
    @RequestMapping(value = "/foo", method = RequestMethod.GET)
    public ResponseEntity<Bar> foo() {
        return new ResponseEntity<>(new Bar(), HttpStatus.OK);
    }
    ...
}
public class Bar extends ResourceSupport {
    public Bar() {
        ...
        add(linkTo(methodOn(RestController.class).foo()).withSelfRel());
    }
}

The endpoint works fine at http://localhost:8080/rest/foo, however, the link is incorrect:

{
  ...
  _links: {
    self: {
      href: "http://localhost:8080/foo"
    }
  }
}

For some reason, the base path (/rest) was ignored.

I'm using Spring HATEOAS 0.19.0.RELEASE (comes with Spring Boot 1.3.1.RELEASE).

Most helpful comment

I have the same problem and implemented the following service that prepends the base path as a workaround:

@Service
public class BasePathAwareLinks {

  private final URI contextBaseURI;
  private final URI restBaseURI;

  @Autowired
  public BasePathAwareLinks(ServletContext servletContext, RepositoryRestConfiguration config) {
    contextBaseURI = URI.create(servletContext.getContextPath());
    restBaseURI = config.getBasePath();
  }

  public LinkBuilder underBasePath(ControllerLinkBuilder linkBuilder) {
    return BaseUriLinkBuilder.create(contextBaseURI)
      .slash(restBaseURI)
      .slash(contextBaseURI.relativize(URI.create(linkBuilder.toUri().getPath())));
  }
}

To use it, just pass the LinkBuilder returned from linkTo to underBasePath before actually building the link:

public class MyResourceProcessor<Person> implements ResourceProcessor<Person> {

  @Autowired
  private BasePathAwareLinks service;

  public Resource<Person> process(Resource<Person> resource) {
    resource.add(
      service.underBasePath(
        linkTo(methodOn(SpecialPersonController.class).doSomething())
      )
      .withRel("something")
    );
    return resource;
  }
}

Hope this helps.

All 27 comments

I just came across this issue as well. Did you find a workaround?

Out of curiousity, is this a spring-hateoas problem or a spring-data-rest problem stemming from @RepositoryRestController?

You should be using @BasePathAwareController to mark up a controller which you want to recognize the prefix.

For more details, check out http://docs.spring.io/spring-data/rest/docs/2.5.4.RELEASE/reference/html/#customizing-sdr.overriding-sdr-response-handlers

Maybe I'm confused about what this means:

If you’re NOT interested in entity-specific operations

I have a @RepositoryRestResource for Invoice at /api/invoices. I need some special hand holding to transition the status of the Invoice (e.g. Going from Processing to Paid). I'm trying to create a URI of /api/invoices/{id}/transitionTo?newStatus=Paid handled by my @RepositoryRestController.

Am I not still interested in entity-specific operations in this case?

Sorry. @RepositoryRestController extends @BasePathAwareController, so that isn't the source of our problem.

Have exactly the same problem.

Same problem here. I also created a JIRA ticket:
https://jira.spring.io/browse/DATAREST-972

Intersting fact: When you create a controller with @RepositoryRestController (which extends BasePathAwareController), then the resource is added twice: Once under the root and once with basePath prefix. This can be seen in the spring logs. But only the version mapped under root actually does work. Smells like a bug for me ...

Please don't open duplicate tickets. If you are moving to JIRA for data rest, then close this ticket.

Sorry for the duplicate. I already opened the spring-data-rest JIRA ticket. before I found this here. I am actually not sure, if this is more a spring-hateoas or a spring-data-rest issue ... What do you think?

I have the same problem and implemented the following service that prepends the base path as a workaround:

@Service
public class BasePathAwareLinks {

  private final URI contextBaseURI;
  private final URI restBaseURI;

  @Autowired
  public BasePathAwareLinks(ServletContext servletContext, RepositoryRestConfiguration config) {
    contextBaseURI = URI.create(servletContext.getContextPath());
    restBaseURI = config.getBasePath();
  }

  public LinkBuilder underBasePath(ControllerLinkBuilder linkBuilder) {
    return BaseUriLinkBuilder.create(contextBaseURI)
      .slash(restBaseURI)
      .slash(contextBaseURI.relativize(URI.create(linkBuilder.toUri().getPath())));
  }
}

To use it, just pass the LinkBuilder returned from linkTo to underBasePath before actually building the link:

public class MyResourceProcessor<Person> implements ResourceProcessor<Person> {

  @Autowired
  private BasePathAwareLinks service;

  public Resource<Person> process(Resource<Person> resource) {
    resource.add(
      service.underBasePath(
        linkTo(methodOn(SpecialPersonController.class).doSomething())
      )
      .withRel("something")
    );
    return resource;
  }
}

Hope this helps.

Is there any progress on this issue?
I am running into the same problem: the REST base path is set to /api but ignored when building links to a @RepositoryRestController. I have tried with @BasePathAwareController as well, but without success. The code of @HiaslTiasl did not work either. For me contextBaseURI is always empty and therefore the resulting link is missing the host (http://localhost:8080 in my case).

I have started poking at this issue. It appears, I can get the custom route to work when I use @BasePathAwareController but NOT with @RepositoryRestController. I have marked up https://jira.spring.io/browse/DATAREST-972 with my concerns about having two annotations that appear to do the same thing and will await @olivergierke 's feedback on that.

In the meantime, I reproduced the fact that ControllerLinkBuilder does NOT have insight into BasePathAwareHandlingMapping, a component registered by Spring Data REST, and hence cannot (yet) factor that into the URI it builds.

I assume we will need a RestControllerLinkBuilder class in the data-rest package...

We'd either need a link builder in SDR or the concept of a base path must move into Spring HATEOAS. I prefer the latter.

As you probably might have expected, I have a slightly different view on this :).

First of all, due to it's static nature, all a ControllerLinkBuilder can do is interpret the static information attached to the class. I guess we can improve things for situations where client code uses the ControllerLinkBuilderFactory instances via dependency injection. But even in case of the latter I'd argue we should rather inspect the mappings defined in the HandlerMappings to lookup the template to be used as this is the point where SD plugs the API prefix.

The reason we didn't do that already is that this naturally creates a mismatch between static and non-static usage of ControllerLinkBuilder, which I tried to avoid so far. However it seems that putting emphasis on the non-static usage is not a bad idea.

The work around code from @HiaslTiasl is useful, but does not work for templated links.

@HiaslTiasl thanks for your code. It works but it looses the first part "http://ip:port. Is that supposed to happen?

@drenda happy to help! 😺

For me the first part was not important, but in general we probably should consider it as well. I guess you could extract it from linkBuilder.toUri(), maybe something like the following (didn't test) could work:

  public LinkBuilder underBasePath(ControllerLinkBuilder linkBuilder) {
    URI uri = linkBuilder.toUri();
    URI origin = new URI(uri.getScheme(), uri.getAuthority(), null, null, null);
    URI suffix = new URI(null, null, uri.getPath(), uri.getQuery(), uri.getFragment());
    return BaseUriLinkBuilder.create(origin)
      .slash(contextBaseURI)
      .slash(restBaseURI)
      .slash(suffix);
  }

We basically take the link from the ControllerLinkBuilder and just prepend the context base URI and the rest base URI to its path, while leaving everything else intact. So maybe now it also works for templated links.

@HiaslTiasl Thanks it works perfectly!

Here is my workaround:
Inject basepath:
@Value("${spring.data.rest.base-path}") String dataRestBasePath through constructor end inside the constructor make sure it ends with '/' and does not start with '/':

   if(dataRestBasePath.charAt(dataRestBasePath.length() - 1) != '/'){
        dataRestBasePath += "/";
    }

    if(dataRestBasePath.charAt(0) == '/'){
        this.dataRestBasePath = dataRestBasePath.substring(1);
    } else {
        this.dataRestBasePath = dataRestBasePath;
    }

Then I have a method which inserts dataRestBasePath right after the third slash (I assume the url is _http://blabla.com:7777/blabla_):

private Link prependBasePath(Link link){

    String hrefSrc = link.getHref();

    int count = 0;
    int i = 0;
    //noinspection StatementWithEmptyBody
    for (; count != 3 && i < hrefSrc.length(); i++){
        if(hrefSrc.charAt(i) == '/') count++;
    }

    String href = hrefSrc.substring(0, i) + dataRestBasePath + hrefSrc.substring(i, hrefSrc.length());

    return new Link(href, link.getRel());
}

And now I just wrap all Link instances with this method call.

Example:
basePath="/api/v1.1"
url="http://localhost:8085/jobs/1"
resultUrl="http://localhost:8085/api/v1.1/jobs/1"

Hey, I was fighting with this issue myself and found out that you can add base path of SDR to @RequestMapping annotation explicitly and here is what happens: you get proper link using static ControllerLinkBuilder AND your method gets registered properly as you want it - the prefix with base path will be ignored somehow.

Although I'm not sure how it is happening, this is what I'm using and it works fine for me.

Simple workaround, based on Michael Tran solution:

@Autowired
private final RepositoryRestConfiguration config;

private Link fixLinkSelf(Object invocationValue) {
    return fixLinkTo(invocationValue).withSelfRel();
}

@SneakyThrows
private Link fixLinkTo(Object invocationValue) {
    UriComponentsBuilder uriComponentsBuilder = linkTo(invocationValue).toUriComponentsBuilder();
    URL url = new URL(uriComponentsBuilder.toUriString());
    uriComponentsBuilder.replacePath(config.getBasePath() + url.getPath());
    return new Link(uriComponentsBuilder.toUriString());
}

Usage is the same as linkTo:

resources.add(fixLinkSelf(methodOn(VoteController.class).history()));    
resources.add(fixLinkTo(methodOn(VoteController.class).current()).withRel("current"));

Other workaround for simple case based on current request with addPath:

new Link(ServletUriComponentsBuilder.fromCurrentRequest().path(addPath).build().toUriString())

this code work fine for me :

    /**
     * bugfix https://github.com/spring-projects/spring-hateoas/issues/434
     */
    private fun linkTo(invocationValue: Any): LinkBuilder {
        val target = ControllerLinkBuilder.linkTo(invocationValue)
        val context = BaseUriLinkBuilder.create(ServletUriComponentsBuilder.fromCurrentContextPath().build().toUri())
        val basedContext = context.slash(config.getBasePath()).toUri()
        val suffix = context.toUri().relativize(target.toUri())
        return BaseUriLinkBuilder.create(basedContext).slash(suffix)
    }

Faced the same problem,

You can now use RepositoryEntityLinks
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#integration

public class MyWebApp {

    private RepositoryEntityLinks entityLinks;

    @Autowired
    public MyWebApp(RepositoryEntityLinks entityLinks) {
        this.entityLinks = entityLinks;
    }
}

Method | Description
-- | --
entityLinks.linkToCollectionResource(Person.class) | Provide a link to the collection resource of the specified type (Person, in this case).
entityLinks.linkToSingleResource(Person.class, 1) | Provide a link to a single resource.
entityLinks.linkToPagedResource(Person.class, new PageRequest(…​)) | Provide a link to a paged resource.
entityLinks.linksToSearchResources(Person.class) | Provides a list of links for all the finder methods exposed by the corresponding repository.
entityLinks.linkToSearchResource(Person.class, "findByLastName") | Provide a finder link by rel (that is, the name of the finder

I recently faced the same problem and tried to solve the issue with the above mentioned fixes. Unfortunately all of them seem to have the same problem:
If you have a character in your url that needs to be escaped, using toUri() will escape these characters for you. But in a link builder context, you don't want these characters to be escaped.
Simple example: You want to link to a controller with a request parameter. The expected result for a link builder would be:
http://localhost:8080/api/endpoint?requestParam={requestParam}
With the above mentioned fixes you would get:
http://localhost:8080/api/endpoint?requestParam=%7BrequestParam%7D
because toUri() escapes the curly brackets.

My solution would look like this:

@Component
public class RepositoryRestControllerLinkBuilder {

    private final RepositoryRestConfiguration repositoryRestConfiguration;

    public RepositoryRestControllerLinkBuilder(RepositoryRestConfiguration repositoryRestConfiguration) {
        this.repositoryRestConfiguration = repositoryRestConfiguration;
    }

    public <T> Builder<T> of(@NonNull Class<T> controllerType) {
        Assert.notNull(controllerType, "ControllerType cannot be null!");
        return new Builder<>(controllerType, this);
    }

    private String uriWithBasePath(WebMvcLinkBuilder linkBuilder) {
        var uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(linkBuilder.toString());
        var basePath = repositoryRestConfiguration.getBasePath().getPath();
        var originalUri = linkBuilder.toUri();
        return uriComponentsBuilder
                .replacePath(basePath + originalUri.getPath())
                .build()
                .toString();
    }

    public static class Builder<T> {

        private final Class<T> controllerType;
        private final RepositoryRestControllerLinkBuilder origin;

        private WebMvcLinkBuilder linkBuilder;

        private Builder(Class<T> controllerType, RepositoryRestControllerLinkBuilder origin) {
            this.controllerType = controllerType;
            this.origin = origin;
            this.linkBuilder = WebMvcLinkBuilder.linkTo(controllerType);
        }

        public Builder<T> forMethod(@NonNull Function<T, Object> invocationFunction) {
            Assert.notNull(invocationFunction, "InvocationFunction cannot be null!");
            this.linkBuilder = WebMvcLinkBuilder.linkTo(
                    invocationFunction.apply(WebMvcLinkBuilder.methodOn(controllerType))
            );
            return this;
        }

        public Link withRel(@NonNull String rel) {
            Assert.hasText(rel, "Rel cannot be null, empty or blank!");
            return Link.of(origin.uriWithBasePath(linkBuilder), rel);
        }

        public String asUriString() {
            return origin.uriWithBasePath(linkBuilder);
        }
    }
}

It would be used like this:

linkBuilder.of(Controller.class).forMethod(controller -> controller.method(parameter)).withRel("relation");

Was this page helpful?
0 / 5 - 0 ratings