Skip to content

Disable optional parameters in links #535

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

Closed
christophlingg opened this issue Jan 16, 2017 · 19 comments
Closed

Disable optional parameters in links #535

christophlingg opened this issue Jan 16, 2017 · 19 comments

Comments

@christophlingg
Copy link
Contributor

Hi guys!

I just updated from 0.19 to 0.23 and got to know a new feature introduced with issue #169

Now query parameter request appear in the links, like that:

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

I know a lot of people were eager to have it, in our case we would like to suppress it, is it possible?

@odrotbohm
Copy link
Member

Just expand the Link without handing any parameters into the method and you should get a plain URI in turn.

@christophlingg
Copy link
Contributor Author

thanks for the hint @olivergierke

@jalati-paychex
Copy link

@christophlingg Are you able to expand on how you handled this? Were you building links via the ControllerLinkBuilder?

@christophlingg
Copy link
Contributor Author

christophlingg commented Jan 31, 2019

@jalati-paychex I used new Link(…).expand()

@odrotbohm
Copy link
Member

Just call link.expand().

@jalati-paychex
Copy link

@christophlingg @odrotbohm Thanks to both of you for the quick responses.

I am using the ControllerLinkBuilder in the following way to generate some "baseHrefs" to create some paging links:

linkTo(methodOn(MyController.class).myMethod(null, null)).withSelfRel().getHref();

Where the first argument is the requestBody and the second is the optional query parameter. I really just need the href out of the self link, is there another way to get that without building the link?

@gregturn
Copy link
Contributor

No. methodOn Is a mechanism that wraps the class with an interceptor that when you call a method, captures all the details and turns them over to linkTo.

linkTo then uses reflection APIs to extract all the Spring Web annotations associated with the method and the class to divine the full URI.

@jalati-paychex
Copy link

@gregturn Thanks, I'll just modify the string value to remove the '{...}' for my purposes.

@benweizhu
Copy link

Hi @jalati-paychex,

What do you mean by 'modify the string value to remove the '{...}''?

I have the same issue in my code.

@jalati-paychex
Copy link

Hi @benweizhu,

In my paged responses, I have a self link and then any next or prev links that apply. When creating the next and prev links, I take the self link's href and essentially apply a replace all to remove the optional query parameters that the libraries add by default:

String baseSelfHref = linkTo(methodOn(MyController.class).search(myPathVariable)).withSelfRel().getHref();
String baseHref = baseSelfHref.replaceAll("\\{.*?\\}", "");

From there, I can add my offset and limit parameters (which is what use for paging).

@odrotbohm
Copy link
Member

All you need to do is sprinkle an ….expand() (no parameters) into the call chain. That will remove the optional template variables. I.e.

String selfHref = linkTo(methodOn(MyController.class).search(myPathVariable))
  .withSelfRel()
  .expand() // <- important bit!
  .getHref();

@jalati-paychex
Copy link

@odrotbohm

I think I just misunderstood @gregturn's reply, then. Now I can remove that work-around.

Thanks!

@ddieckma
Copy link

ddieckma commented Feb 22, 2019

@odrotbohm, this seems to be a breaking change in the behavior because my client is now receiving URI templates instead of URIs even though I specified all the parameters for the controller method. When I use the linkTo(methodOn(...)) invocation I pass in nulls for the parameters that are not relevant to the link I want to generate. In order to get back to the original behavior of the library I need to call expand() everywhere that I used linkTo(methodOn(...)).

I understand what the code is doing and how it is generating a URI template. I don't understand why I would want a URI template when I call the method to get a link, I would expect to get a link, not a template.

Should my clients be able to accept a URI template as a HATEAOS link? Is that normal nowadays?

Should my servers always expand all Link objects at some point before sent back to the client?

I am mainly trying to figure out if this feature is a breaking change for my applications because my applications are written incorrectly, or if it really is a breaking change to the library.

  @RestController
  @Api(value="student MSR projects", description="Med Scholar Research Project operations")
  @RequestMapping(value=ApiUrls.API_ROOT)
  public class ProjectController {

    @ApiOperation(value = "Get Project list for user", notes = "Returns projects")
    @RequestMapping(method = RequestMethod.GET, value = PROJECTS_URI, produces = { MediaType.APPLICATION_JSON_VALUE })
    @ApiImplicitParams({
      @ApiImplicitParam(name = "p", value = "Page number", required = false, dataType = "long", paramType = "query"),
      @ApiImplicitParam(name = "ps", value = "Page size", required = false, dataType = "long", paramType = "query"),
    })
    public ResponseEntity<PagedResources<ProjectViewResource>> getProjectList(@PageableDefault(size = 2000, page = 0) Pageable pageable,
        PagedResourcesAssembler<ProjectView> assembler,
        @ApiParam(value = "optional parameter to request view of the response.", allowableValues = "compact") @RequestParam(value = "view", required = false, defaultValue = "compact") String view,
        @ApiParam(value = "optional parameter to filter results by user id.") @RequestParam(value="userId", required=false) String userId,
        @ApiParam(value = "optional parameter to filter results by mentor id.") @RequestParam(value="mentorId", required=false) String mentorId,
        @ApiParam(value = "optional parameter to filter results by mentor status.") @RequestParam(value="mentorStatus", required=false) MentorStatus mentorStatus,
        @ApiParam(value = "optional parameter to filter results by Committee Date (starting with).") @RequestParam(value="startCommitteeMeetingDate", required=false) @DateTimeFormat(pattern="MM/dd/yyyy") Date startCommitteeMeetingDate,
        @ApiParam(value = "optional parameter to filter results by Committee Date (ending with).") @RequestParam(value="endCommitteeMeetingDate", required=false) @DateTimeFormat(pattern="MM/dd/yyyy") Date endCommitteeMeetingDate,
        @ApiParam(value = "optional parameter to filter results by Project Status") @RequestParam(value="projectStatus", required=false) List<ProjectStateCode> projectStates,
        @ApiParam(value = "optional parameter to filter results by Parent projects") @RequestParam(value="onlyParentProjectsFlag", required=false) Boolean onlyParentProjectsFlag,
        @ApiParam(value = "optional parameter to filter results by Term") @RequestParam(value="termCode", required=false) List<String> termCode,
        @ApiParam(value = "optional parameter to filter results by Area Ids") @RequestParam(value="areaIds", required=false) List<Long> areaIds
        ) {
      // ...
    }
  }

  // Code that generates a link a list of projects for a specific committee meeting date
  Link projectsLink = linkTo(methodOn(ProjectController.class).getProjectList(null, null, null, null, null, null, startCommitteeMeetingDate, startCommitteeMeetingDate, null, null, null, null))
      .withRel(PROJECT_LIST_REL);

  // spring-hateaos 0.17 generates the following link URL
  // .../api/v1/projects?startCommitteeMeetingDate=03/07/2019&endCommitteeMeetingDate=03/07/2019

  // spring-hateaos 0.25 generates the following link URL
  // ...api/v1/projects?startCommitteeMeetingDate=03/07/2019&endCommitteeMeetingDate=03/07/2019{&view,userId, mentorId, mentorStatus, projectStatus,onlyParentProjectsFlag,termCode,areaIds}

  // If I call projectsLink.expand() in 0.25 I get the following link URL
  // .../api/v1/projects?startCommitteeMeetingDate=03/07/2019&endCommitteeMeetingDate=03/07/2019

  // I am struggling with what appears to be the requirement that I must ALWAYS call expand() for every link.  In this
  // case the REST endpoint has optional parameters so spring-hateaos generates a URI template that needs to be expanded.


  // But for a REST endpoint without request parameters I don't need to expand the resulting Link object.
  @RequestMapping(method = RequestMethod.GET, value = PROJECTS_URL + "/{projectId}", produces = { MediaType.APPLICATION_JSON_VALUE })
  public ResponseEntity<ProjectResource> getProject(
      @ApiParam(value = "id of the Project.") @PathVariable("projectId") Long projectId){ }

  // Link to project without query parameters
  Link projectLink = linkTo(methodOn(ProjectController.class).getProject(123l).withSelfRel();
  // Resulting URL
  // .../api/v1/projects/123

  // Now if some developer decides to add an optional query parameter
  @RequestMapping(method = RequestMethod.GET, value = PROJECTS_URL + "/{projectId}", produces = { MediaType.APPLICATION_JSON_VALUE })
  public ResponseEntity<ProjectResource> getProject(
      @ApiParam(value = "id of the Project.") @PathVariable("projectId") Long projectId,
      @ApiParam(value = "filter by active projects") @RequestParam(value="active", required=false) Boolean active) {}

  // And the developer updates the call signature to get the code to compile
  Link projectLink = linkTo(methodOn(ProjectController.class).getProject(123l, null).withSelfRel();
  // Then the resulting URL is
  // ../api/v1/projects/123{&active}

  // In this case the developer needs to know that when the first optional parameter is added to
  // an endpoint that all links generated from the endpoint method must also call expand(). Since
  // this is likely to be forgotten, it appears that the best defense is to ALWAYS call expand(),
  // and if something ALWAYS needs to be done, then would be nice for my framework to do it for me
  // rather that make me do it for the framework.

@gregturn
Copy link
Contributor

It would help if you posted a web method and how you are forming the link along with the JSON output in order to solve this.

@ddieckma
Copy link

ddieckma commented Feb 22, 2019 via email

@odrotbohm
Copy link
Member

odrotbohm commented Feb 22, 2019

Yes, it is a breaking change. But that's why where on a 0.x version. From a client point of view it's not really breaking even, as the only media type we really have supported so far is HAL which clearly states that URI templates are valid values for the href attribute and whether or not a link is templated is indicated via the templates attribute.

The use case is pretty simple: when you point to a controller method that takes optional parameters and you don't provide a value for one, how else is the client supposed to learn that this additional, optional parameter is available for the resource? That big list of additional annotations to create out of band documentation is really not a RESTful answer to that question.

In general, I think if you find yourself thinking you always need to expand the link on the server, you've put on a non-REST mind set. Have a look at what kind of resource you describe. It's expecting some mandatory parameters and some optional ones. Exactly that is transferred to the client and it's up to the client to make use of those optional parameters. If you hide the existence of those optional parameters by sending an expanded URI, it would have to use some out of band information to reconstruct the URI if it wants to send a parameter value for one of those. That violates rule number one of RESTful APIs: clients must not have any knowledge about URI structure.

Clients of course have to expand URI templates before using a link's href as request URI. But that's not something we impose on the client but the way URIs and URI templates are defined.

I agree that there are cases in which you'd have to present clients with canonical URIs (like self), but they're the exception, not the rule.

@ddieckma
Copy link

This answers my question/concern. Our client applications do not currently handle/expand HAL URI templates. That seems to be the root cause of the problems we experienced as a result of the update. I will pursue a fix for our clients so that they can accept the HAL URI templates.

Thank you for the quick response to my questions.

@durimkryeziu
Copy link

durimkryeziu commented Sep 28, 2021

All you need to do is sprinkle an ….expand() (no parameters) into the call chain. That will remove the optional template variables. I.e.

String selfHref = linkTo(methodOn(MyController.class).search(myPathVariable))
  .withSelfRel()
  .expand() // <- important bit!
  .getHref();

@odrotbohm After upgrading to the latest version of Spring HATEOAS both the required and optional query parameters are shown as {?queryParam} but I want to have the behaviour as before, only optional parameters shown there?

As when I call .expand() to the Link, both types of query params are gone!

thanks

@aisensiy
Copy link

All you need to do is sprinkle an ….expand() (no parameters) into the call chain. That will remove the optional template variables. I.e.

String selfHref = linkTo(methodOn(MyController.class).search(myPathVariable))
  .withSelfRel()
  .expand() // <- important bit!
  .getHref();

@odrotbohm After upgrading to the latest version of Spring HATEOAS both the required and optional query parameters are shown as {?queryParam} but I want to have the behaviour as before, only optional parameters shown there?

As when I call .expand() to the Link, both types of query params are gone!

thanks

I am suffering same issue, do you have any solution for this?

MartB added a commit to DiscoResearchSat/hawkbit that referenced this issue Mar 19, 2023
We need to drop all template parameters for backwards-compatibility reasons.
Do so by expanding all links after withRel|withSelfRel.

Reference:
spring-projects/spring-hateoas#535
Signed-off-by: Martin Böh <[email protected]>
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

8 participants