1

I am developing a set of rest resources over a database and exposing the core CRUD functionality using Spring Data Rest to directly interact with the Repositories.

In my simplified sample I have Users:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;

    public String name;

    @OneToMany(mappedBy = "user")
    public Collection<Project> projects;
}

and users Own Projects:

@Entity
public class Project {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public long id;

    public String name;

    public String oneOfManyComplexDerivedProperties;

    @ManyToOne
    public User user;
}

Directly interacting with the repositories is fine, so for creating Users (other other simple entities), the problem comes with creating Projects. Projects have a large number of server derived fields based on the users form input, so I wrote a custom controller to generate them and persist the result. In order to persist the result, I need to associate the Project with it's owning User. I want my client to be able to use the user Link for this, just as when creating a new entity by going direct to the repository (going direct to the repository just works):

@RepositoryRestController
public class CustomProjectController {

    @Autowired
    ProjectRepo projectRepo;

    @RequestMapping(value = "/createProject", method = RequestMethod.POST)
    public HttpEntity<Project> createProject(@RequestParam User userResource,
                                         @RequestParam String formField1, // actually an uploaded file that gets processed, but i want simple for example purposes
                                         @RequestParam String formfield2)
{
    Project project = new Project();

    /*
    Actually a large amount of complex business logic to derive properties from users form fields, some of these results are binary.
     */
    String result = "result";
    project.oneOfManyComplexDerivedProperties = result;
    project.user = userResource;
    projectRepo.save(project);

    // aware that this is more complex than I've written.
    return ResponseEntity.ok(project);
}
}

When I call:
http://localhost:9999/api/createProject?userResource=http://localhost:9999/api/users/1&formField1=data1&formField2=Otherdata

I get:

{
    "timestamp": 1510588643801,
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
    "message": "Failed to convert value of type 'java.lang.String' to required type 'com.badger.User'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Long] for value 'http://localhost:9999/api/users/1'; nested exception is java.lang.NumberFormatException: For input string: \"http://localhost:9999/api/users/1\"",
    "path": "/api/createProject"
}

If I change userResource to type Resource then I get a different error:
"Failed to convert value of type 'java.lang.String' to required type 'org.springframework.hateoas.Resource'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'org.springframework.hateoas.Resource': no matching editors or conversion strategy found"

I can't find any reference to using repository URI's in custom controllers in the docs, the closest I found was Resolving entity URI in custom controller (Spring HATEOAS) but the API's have changed since that was written and I have not been able to get it to work.

adickinson
  • 553
  • 1
  • 3
  • 14
Draconas
  • 185
  • 1
  • 8
  • What `User userResource` requires is a `User` object what you passed in was a String `http://localhost:9999/api/users/1` – Ayo K Nov 13 '17 at 16:34

3 Answers3

1

I would suggest that what you should really be doing is:

http://localhost:9999/api/users/1/projects?formField1=data1&formField2=Otherdata

By enable Spring Data's web support you can have the path variable automatically bound to the entity instance.

@RequestMapping(value = "users/{id}/projects", method = RequestMethod.POST)
        public HttpEntity<Project> createProject(
                            @PathVariable("id") User user,
                            @RequestParam String formField1,
                            @RequestParam String formfield2)
{

}

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web

Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • This works pretty well, though alas isn't going to work for a later object that has 2 parents, but I can find another work around. – Draconas Nov 15 '17 at 09:03
  • Love the path variable binding to the entity - learnt something new! – Bacon Nov 15 '17 at 09:37
0

What User userResource requires is a User object what you passed in was a String http://localhost:9999/api/users/1

So I would recommend you changing your request to a POST request and passing your User Object as the body of that request.

The body of the request will look like in JSON format:

{
    id: 1,
    name: "myName",
    projects: []
}

so you then remove userResource=http://localhost:9999/api/users/1 from your URL. Finally change @RequestParam User userResource to @RequestBody User userResource

EDIT since you have the user Id in your request you can make a call within the controller to find the user by the Id. So you can change the user object from @RequestParam User userResource to @RequestParam long userId then make a call to find the user something like findUserById(userId) inside your method

so you url will look like http://localhost:9999/api/createProject?userId=1&formField1=data1&formField2=Otherdata

Ayo K
  • 1,719
  • 2
  • 22
  • 34
  • I'd like to avoid having to embed the User object in the Project request as the Real user object is a lot more complicated and there is more than one. (Straight embedding, or embedding the User id and then manually retrieving is my fallback option) – Draconas Nov 13 '17 at 17:02
  • I know it can be done using the resource URL, as it works seamlessly if directly interacting directly with the repository via rest (which I am doing in other cases, it is just that this one needs a lot of processing) – Draconas Nov 13 '17 at 17:03
0

If you want to stick with the url params you could change the @RequestParam User userResource to @RequestParam String userId

And then in your createProject code have something along the lines of

User user = userRepo.findOne(userId);
project.user = user;
....
projectRepo.save(project);

If it was me I'd define a CreateProjectRequest object which you pass in, e.g.

{
   "userId" : "1",
   "formField1" : "whatever",
   "formField2" : "whatever"
}

And change your createProject to

createProject(@RequestBody CreateProjectRequest createProjectRequest)
    ...
    project.setField1(createProjectRequest.getFormField1());
    ...

    User user = userRepo.findOne(createProjectRequest.getUserId());
    project.user = user;
    ....
    projectRepo.save(project);
Bacon
  • 1,229
  • 2
  • 14
  • 26
  • 1
    That is my fallback if I can't get the URL to work, I've been cursing the damn thing because the infrastructure to work it is clearly in the framework, they just don't tell me what parameter types/advice/converter I need to pull out to use it. – Draconas Nov 13 '17 at 17:12