Search code examples
javaspringspring-bootspring-restcontrollerspring-hateoas

How to override only 1 Http Method (POST) in a @RestController, but let Spring handle all the other Http methods with a standard HATEOAS response


I want to expose some REST resources in Spring, by default I can just let Spring Boot deal with this and this generates a nice HATEOAS response where I get everything 'free'.

Now I need to perform some logic when a certain resource is being POSTED, so I implemented my own @RestController class and override the POST method as follows:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;

import java.net.URI;
import java.security.Principal;

@RestController
@RequestMapping(Constants.PROJECTSPATH)
public class ProjectController {
    @Autowired private ProjectRepository projectRepo;
    @Autowired private ProjectRoleRepository projectRoleRepo;
    @Autowired private AccountRepository accountRepo;

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<?> addProject(@RequestBody Project projectFromRequest, Principal principal) {
        Project project = projectRepo.save(projectFromRequest);
        Account currentAccount = accountRepo.findByUsername(principal.getName());
        ProjectRole ownerRole = new ProjectRole(project, currentAccount, ProjectRoleEnum.OWNER);
        projectRoleRepo.save(ownerRole);

        final URI uri =
                MvcUriComponentsBuilder.fromController(getClass())
                        .path("/{id}")
                        .buildAndExpand(project.getId())
                        .toUri();
        return ResponseEntity.created(uri).body(new ProjectResource(project));
    }
}

I use a class inheriting from ResourceSupport and in there I generate the required links for a HATEOAS response. So far so good, that isn't too much custom work.

The problem is that if I now just try to GET all projects, I get this message:

{"timestamp":1518610270403,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'GET' not supported","path":"/projects"}

I really prefer not to implement the other methods too, the default done by Spring Boot is great. But am I now forced to implement these as well and keep my entire HATEOAS response up to date with my domain object changes every time? That is functionality that I love 'out of the box'.


Solution

  • How to override only 1 Http Method (POST) in a @RestController, but let Spring handle all the other Http methods with a standard HATEOAS response

    You can't do that with a @RestController. As stated in the Spring Data REST reference:

    Sometimes you may want to write a custom handler for a specific resource. To take advantage of Spring Data REST’s settings, message converters, exception handling, and more, use the @RepositoryRestController annotation instead of a standard Spring MVC @Controller or @RestController

    It is not explicitly mentionned, but annotating your controller with @RepositoryRestController allows you to define a custom behavior for one endpoint while keeping all the other endpoints that Spring automatically generates.

    For older versions, note that you can use the @RequestMapping annotation at the method level only. Issue fixed in 2.4 M1 (Gosling), 2.3.1 (Fowler SR1), 2.1.6 (Dijkstra SR6).

    Your example becomes:

    @RepositoryRestController
    @RequestMapping(/project)
    public class ProjectController {
    
        @Autowired private ProjectRepository projectRepo;
        @Autowired private ProjectRoleRepository projectRoleRepo;
        @Autowired private AccountRepository accountRepo;
    
        @PostMapping(Constants.PROJECTSPATH) // @PostMapping is a shortcut for @RequestMapping(method = RequestMethod.POST). path can be something like "/prj" (beginning with slash, because on class level, there is no suffix slash)
        public ResponseEntity<?> addProject(@RequestBody Project projectFromRequest, Principal principal) {
            Project project = projectRepo.save(projectFromRequest);
            Account currentAccount = accountRepo.findByUsername(principal.getName());
            ProjectRole ownerRole = new ProjectRole(project, currentAccount, ProjectRoleEnum.OWNER);
            projectRoleRepo.save(ownerRole);
    
            final URI uri =
                    MvcUriComponentsBuilder.fromController(getClass())
                            .path("/{id}")
                            .buildAndExpand(project.getId())
                            .toUri();
            return ResponseEntity.created(uri).body(new ProjectResource(project));
        }
    }