Search code examples
javaspringspring-mvcspring-dataspring-data-rest

RestRepositoryController hide REST repository endpoints


I'm using Spring Boot 2.3.1 with SDR, HATEOAS, Hibernate. In my project I've exposed Repositories via Spring Data REST. I already use SDR in other project, but in this one I've a strange issue with one Controller. In fact if I add some custom endpoints in this controller, all default endpoints for the related Entity are hidden and not available anymore.

Let me explain better. I've this entity:

@EntityListeners(TenantListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class Tenant extends AbstractEntity {

    @Enumerated(value = EnumType.STRING)
    @Column(nullable = false, updatable = false)
    private TenantType type;

    @NotBlank
    @Column(nullable = false)
    private String fullName;

    @NotBlank
    @Column(nullable = false)
    private String lastName;

    @NotBlank
    @Column(nullable = false)
    private String firstName;

    @Username
    @Size(min = 4, max = 16)
    @Column(nullable = false, unique = true)
    @ColumnTransformer(write = "LOWER(?)")
    private String tenantId;

    //other fields

and this is the Repository:

@Transactional
@IsManagementUser
public interface TenantRepository extends JpaRepository<Tenant, Long>, JpaSpecificationExecutor {

    @Caching(evict = {
            @CacheEvict(value = "tenants", allEntries = true),
            @CacheEvict(value = "tenants#id", allEntries = true),
            @CacheEvict(value = "tenants#sid", allEntries = true),
            @CacheEvict(value = "tenants#exists", allEntries = true),
    })
    @Override
    <S extends Tenant> S save(S s);

    @Caching(evict = {
            @CacheEvict(value = "tenants", allEntries = true),
            @CacheEvict(value = "tenants#id", allEntries = true),
            @CacheEvict(value = "tenants#sid", allEntries = true),
            @CacheEvict(value = "tenants#exists", allEntries = true),
    })
    @Override
    void deleteById(Long aLong);

  
    @Caching(evict = {
            @CacheEvict(value = "tenants", allEntries = true),
            @CacheEvict(value = "tenants#id", allEntries = true),
            @CacheEvict(value = "tenants#sid", allEntries = true),
            @CacheEvict(value = "tenants#exists", allEntries = true),
    })
    @Modifying
    void deleteByTenantId(String tenantId);


  
    @Cacheable(cacheNames = "tenants#id")
    Optional<Tenant> findByTenantId(@Param("tenantId") String tenantId);

    @Cacheable(cacheNames = "tenants#sid")
    @Query("SELECT sid FROM Tenant t WHERE t.tenantId=:tenantId")
    String findSidByTenantId(@Param("tenantId") String tenantId);

  
    @Cacheable(cacheNames = "tenants#exists")
    @Query("SELECT case WHEN (COUNT(*) > 0)  THEN true ELSE false end FROM Tenant WHERE tenantId=:tenantId")
    boolean existsTenantId(@Param("tenantId") String tenantId);

    @Caching(evict = {
            @CacheEvict(value = "tenants", allEntries = true),
            @CacheEvict(value = "tenants#id", allEntries = true),
            @CacheEvict(value = "tenants#exists", allEntries = true),
    })
    @Modifying
    @Query("UPDATE Tenant t SET t.verified=:verified, t.version=t.version+1, t.lastModifiedDate=UTC_TIMESTAMP() WHERE t.tenantId=:tenantId")
    void setVerified(@Param("tenantId") String tenantId, @Param("verified") boolean verified);

    @Caching(evict = {
            @CacheEvict(value = "tenants", allEntries = true),
            @CacheEvict(value = "tenants#id", allEntries = true),
            @CacheEvict(value = "tenants#exists", allEntries = true),
    })
    @Modifying
    @Query("UPDATE Tenant t SET t.enabled=:enabled, t.version=t.version+1, t.lastModifiedDate=UTC_TIMESTAMP() WHERE t.tenantId=:tenantId")
    void setEnabled(@Param("tenantId") String tenantId, @Param("enabled") boolean enabled);
}

and this is REST controller:

@RepositoryRestController
@RequestMapping(path = "/api/v1")
@PreAuthorize("isAuthenticated()")
public class TenantController {

    @Autowired
    private LocalValidatorFactoryBean validator;

    @Autowired
    private TenantRepository tenantRepository;


    @Autowired
    private DbmsManager dbmsManager;

    @Autowired
    private PagedResourcesAssembler pagedResourcesAssembler;

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(validator);
    }


    @IsManagementUser
    @DeleteMapping(path = "/tenants/{id}")
    public ResponseEntity<?> deleteTenant(@PathVariable("id") Long id) {
        Optional<Tenant> optionalTenant = tenantRepository.findById(id);
        if (optionalTenant.isPresent()) {
            dbmsManager.dropTenant(optionalTenant.get().getTenantId());
        }
        return ResponseEntity.noContent().build();
    }

}

If I call the endpoint GET http://localhost/api/v1/tenants, I get the entire list of tenants. If I try to call any other method like GET http://localhost/api/v1/tenants/1 or PATCH http://localhost/api/v1/tenants/1 I get a warning in console:

12/07/2020 21:51:01,440  WARN http-nio-9999-exec-2 PageNotFound:209 - Request method 'GET' not supported

If I comment out all endpoint in my TenantController, then everything works fine. It seems, no matter what endpoint I create in my controller, hides all others SDR defaults endpoint.

This is happening just with this entity and this controller but I don't see any particular thing. Any hint is really appreciated in order to understand where the problem is.


Solution

  • If you want to blend into the URI space of Spring Data REST, you must not uses @RequestMapping on the type level. If you do so, the controller is included into the Spring MVC handler mapping, not Spring Data REST's one. For handler selection, that causes a URI match to be found and Spring MVC then only inspecting the handlers in that particular mapping for downstream selection regarding HTTP method, produces and consumes clauses etc.

    Unfortunately that behavior has been established in Spring MVC for ages and cannot be changed as it would break existing applications that knowing or unknowingly depend on this.

    I've filed this ticket with Spring MVC to reconsider this (even if for 6.0 only).

    Btw. to simply trigger some business logic based on lifecycle events of the aggregates, please also refer to the events Spring Data REST publishes. Read up more on that in the reference documentation. They allow not having to write a custom controller in scenarios like these in the first place.