1

I have a Spring project with spring-data-rest as a dependency. I have quite a number of repositories in my project, which spring-data-rest automatically created REST API endpoints for. This suited my needs pretty well until now. Now I have a requirement to change the default functionality of one endpoint for all my repositories, specifically, /BASE_PATH/REPOSITORY. This path responds with a paged list of all records of my db.

Now I want to reimplement this endpoint for all my repositories. This is where I am hitting a roadblock. I tried

@RestController
public class MyTableResource {
    private MyTableService myTableService;

    @Autowired
    public MyTableResource(MyTableService myTableService) {
        this.myTableService = myTableService;
    }

    @GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
    public ResponseEntity getMyTables(@QuerydslPredicate(root = MyTable.class) Predicate predicate) throws NoSuchMethodException {
        // My custom implementation
    }
}

Now this somewhat works but the problem is I need to write almost the same code for all my repositories. I tried @GetMapping(value = "/api/v1/{repository}", produces = MediaTypes.HAL_JSON_VALUE) but this is also matching /api/v1/notarepository which I have implemented separately.

Also, even if I do @GetMapping(value = "/api/v1/{repository}", produces = MediaTypes.HAL_JSON_VALUE) I would like to get a handle to a repository object (MyTable) using {repository} path variable, which would be myTables in this case.

In short, I want to write a single custom controller for all my repositories, since the logic would be the same for each of them, while making sure the correct repository is called based on the path called also making sure that any path variables I introduce does not hide other controller classes I have written.

More things I have tried

I was attempting to get paged HATEOAS resource objects automatically from my list of entities. For this I found that I can use PagedResourceAssembler

@RestController
public class MyTableResource {
    private MyTableService myTableService;

    @Autowired
    public MyTableResource(MyTableService myTableService) {
        this.myTableService = myTableService;
    }

    @GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
    public ResponseEntity getMyTables(@QuerydslPredicate(root = MyTable.class) Predicate predicate, PagedResourcesAssembler<Object> pagedResourcesAssembler) throws NoSuchMethodException {
        // My custom implementation
        return ResponseEntity.ok(pagedResourcesAssembler.toResource(myTableList);
    }
}

This gives me a good response with the required links for the page but does not give links per entity. Then I found I can hook up PersistentEntityResourceAssembler and pass it to toResource above so I did

@RestController
public class MyTableResource {
    private MyTableService myTableService;

    @Autowired
    public MyTableResource(MyTableService myTableService) {
        this.myTableService = myTableService;
    }

    @GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
    public ResponseEntity getMyTables(@QuerydslPredicate(root = MyTable.class) Predicate predicate, PagedResourcesAssembler<Object> pagedResourcesAssembler, PersistentEntityResourceAssembler assembler) throws NoSuchMethodException {
        // My custom implementation
        return ResponseEntity.ok(pagedResourcesAssembler.toResource(myTableList, assembler);
    }
}

This does not work as reported in How to have PersistentEntityResourceAssembler injected into request methods of custom @RepositoryRestController in a @WebMvcTest unit test .

It kind of works if I replace @RestController with RepositoryRestController but then Predicate stops working as mentioned in https://jira.spring.io/browse/DATAREST-838 .

So, I tried using @QuerydslPredicate RootResourceInformation resourceInformation instead of @QuerydslPredicate(root = MyTable.class) Predicate predicate. This also did not work as my controller endpoint does not have /{repository} in it.

Then I tried setting @GetMapping(value = "/{repository}" produces = MediaTypes.HAL_JSON_VALUE). This threw a mapping conflict error.

So I am completely stuck as to what to do next.

Sayak Mukhopadhyay
  • 1,332
  • 2
  • 19
  • 34

1 Answers1

2

You can extend the default behavior provided by Spring Data Rest by extending RepositoryRestMvcConfiguration.

RepositoryRestMvcConfiguration has a DelegatingHandlerMapping bean which holds a list of HandlerMapping. Spring iterates over this list and tries to find a handler for the request. The order of this list is important. The first one gets picked up first for the execution. So if we add a new handler in front of the ones we already have then our HandlerMapping will be called.

You can use whatever logic you want to find the handler for the request. In your case, this would be if the path variable is a repository name.

The following code adds a new handler:

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Configuration
public class CustomRestMvcConfiguration extends RepositoryRestMvcConfiguration {
    public CustomRestMvcConfiguration(ApplicationContext context,
                                      ObjectFactory<ConversionService> conversionService) {
        super(context, conversionService);
    }

    @Override public DelegatingHandlerMapping restHandlerMapping() {
        DelegatingHandlerMapping delegatingHandlerMapping = super.restHandlerMapping();

        List<HandlerMapping> delegates = delegatingHandlerMapping.getDelegates();
        delegates.add(0, new HandlerMapping() {
            @Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
                //Your custom logic to decide if you should handle the request
                //If you don't want to handle the request return null
                return null;
            }
        });

        return new DelegatingHandlerMapping(delegates);
    }
}

Hope this helps!

Note: RepositoryRestHandlerMapping is the default one you can check it while writing your logic. It might be helpful.

mahsum
  • 350
  • 3
  • 11