Spring Data REST JPA: Integrate automatic CRUD operations with manual controller - java

I'm using Spring Data REST JPA to build a RESTful web service. So far, Spring is auto-generating all the responses for all the possible methods and for listing all the resources available and even for searches over them:
#RepositoryRestResource(collectionResourceRel = "scans", path = "scans")
public interface ScanRepository extends PagingAndSortingRepository<Scan, Long> {
List<Scan> findByProjectId(#Param("pid") String pid);
}
Now I would like to modify what is returned "only" to POST requests while leaving intact the support to all the others.
I thought I'd create a controller for this purpose like the following:
#Controller
public class ScanController {
#RequestMapping(value = "/scans", method = POST, produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody Result parseScan(#RequestParam String projectId, #RequestParam String tool) {
return null;
}
However when I do this, the JPA-data auto-generated responses for all the other methods and searches etc. ceases to exist. For instance, I get "Method not allowed" if I forward a GET request.
Besides, how could I access a JSON payload from the controller?
UPDATE
Now only one of the exposed resource does back to the default methods for requests not manually handled in my own controller. However I have no idea why it does and why this doesn't happen for any of the other resources.*
Despite they all only differ in their entity's attributes.
The following particular resource is the one that does back to the default request handlers for anything that is not POST scan/ or GET /scan/// which I declared in the controller:
#Controller
public class ScanController {
#Autowired
private ScanService scanService;
#RequestMapping(
value = "/scan",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan parseScan(#RequestBody Scan rbody) {
<...do something...>
}
#RequestMapping(value = "/scans/{id}/{totvuln}/{nth}", method = RequestMethod.GET,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan getScan(#PathVariable String id, #PathVariable int totvuln, #PathVariable int nth) throws ScanNotFound {
<...do something...>
}
It has the following repository interface:
public interface ScanRepository extends PagingAndSortingRepository<Scan, Long> {}
and the following service:
#Service
public class ScanServiceImpl implements ScanService {
#Resource
private ScanRepository scanRepository;
#Resource
private ResultRepository resultRepository;
#Override
#Transactional
public Scan create(Scan shop) {
<some code>
}
#Override
#Transactional
public Scan findById(long id) {
<some code>
}
#Override
#Transactional(rollbackFor = ScanNotFound.class)
public Scan delete(long id) throws ScanNotFound {
<some code>
}
#Override
#Transactional
public List<Scan> findAll() {
<some code>
}
#Override
#Transactional(rollbackFor = ScanNotFound.class)
public Scan update(Scan scan) throws ScanNotFound {
<some code>
}
}
and the resource itself has the following attributes:
#Entity
public class Scan {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Long projectId;
#OneToMany
private Collection<Result> result;
private int totV;
<getters and setters>
}
While the following semi-identical resource "Rules" does not back to any of the default request handlers. It returns "Method not Allowed" for anything different from POST /rule:
#Controller
public class RulesController {
#Autowired
private RulesService rService;
#Resource
private ScanRepository scanRepository;
#RequestMapping(
value = "/rule",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Rules generateRules(#RequestBody Scan rbody) throws Exception {
<do something>
}
}
It has the same repository interface:
public interface RulesRepository extends PagingAndSortingRepository<Rules, Long> {}
and also the same service implementation:
#Service
public class RulesServiceImpl implements RulesService {
#Resource
private RulesRepository rRepository;
#Resource
private ResultRepository resultRepository;
#Override
#Transactional
public Rules create(Rules shop) {
<do something>
}
#Override
#Transactional
public Rules findById(long id) {
<do something>
}
#Override
#Transactional(rollbackFor = RulesNotFound.class)
public Rules delete(long id) throws RulesNotFound {
<do something>
}
#Override
#Transactional
public List<Rules> findAll() {
<do something>
}
#Override
#Transactional
public Rules findByScanId(long id) throws RulesNotFound {
<do something>
}
#Override
#Transactional(rollbackFor = RulesNotFound.class)
public Rules update(Rules scan) throws RulesNotFound {
<do something>
}
}
and the resource Rules itself has the following attributes:
#Entity
public class Rules {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne
private Scan scan;
#OneToMany
private Collection<Result> result;
private String rules;
<getters and setters>
}
Why isn't Spring exposing the default request handlers also for "Rules" for any request that hasn't been specified manually in my controller class?
I would truly appreciate if you could point out why. Thank you so much!

I've figured out how to access a JSON payload from the controller:
#RequestMapping(
value = "/scan",
method = POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = {MediaType.APPLICATION_JSON_VALUE})
public #ResponseBody
Scan parseScan(#RequestBody Scan rbody) {
Scan scan = new Scan();
scan.setProjectId(rbody.getProjectId());
scan.setTool(rbody.getTool());
return scan;
}
Also I've realised the automatic CRUD operations were actually being already supported for every request not handled by my own controller: I was just requesting the wrong URL.
I got the list of correct URLs to use by requesting "curl http://localhost:8080"
However, the preferred URL for any of the auto-generated operations can be set with
#RepositoryRestResource(collectionResourceRel = pref_URL_suffix, path = pref_URL_suffix)
^^somehow during all the changes I tried, that line above went missing.

Related

how can I do to make it return error when restful url has invalid parameter

#RestController()
#RequestMapping(path = "/users")
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam Integer pageSize, UserRequest userRequest) {
//TODO: some implementation
}}
public class UserRequest{
public String name;
public String age;
}
send the request with invalid parameter, like localhost:8800/users?name1=1234, I want to return error. but in fact it ignore the invalid parameter name1.
I tried to add the user defined annotation on the method parameter and on the class , codes like below
#RestController()
#RequestMapping(path = "/users")
#Validated
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam #Validated Integer pageSize, #Validated UserRequest userRequest} {
//TODO: some implementation
}
}
But it does not working.
I think it is happened because framework has ignore the invalid parameter before the method was called.
where did framework handle the url and how can I do to make it return error instead of ignore?
You can reject parameters that are not valid. You can do so in a HandlerInterceptor class.
Reference: Rejecting GET requests with additional query params
In addition to what is done in the above reference, in your addInterceptors, you can specify the path that is intercepted.
Like this:
#Configuration
public class WebConfig implements WebMvcConfigurer {
private String USERS_PATH = "/users";
// If you want to cover all endpoints in your controller
// private String USERS_PATH = List.of("/users", "/users/**");
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FooHandlerInterceptor()).addPathPatterns(USERS_PATH);
}
}

Java Spring 5 Get and findById mongo MonoOnErrorResume

Spring newbie here, trying to make a GET http query in a mongo db via findById(id, Object).
But it doesn't seem to be working. I can POST and PUT but when calling a query via ID i get this err MonoOnErrorResume
I'm using EmbeddedMongoDB
Controller
public class ContentController {
public static final String CONTENT_V_1_CONT = "/contents/v1/cont/";
private final ContentService contentService;
#Autowired
public ContentController(ContentService contentService) {
this.contentService = contentService;
}
#GetMapping(path = "{id}", produces =
MediaType.APPLICATION_JSON_UTF8_VALUE)
public Mono<Content> getContent(#PathVariable String id) {
System.out.println(contentService.getContent(id)); //
MonoOnErrorResume
return contentService.getContent(id);
}
#PostMapping(path = "", produces =
MediaType.APPLICATION_JSON_UTF8_VALUE, consumes =
MediaType.APPLICATION_JSON_UTF8_VALUE)
public Mono<Content> createContent(#RequestBody Mono<Content> content){
return contentService.createContent(content);
}
Service Implmentation
public final ReactiveMongoOperations reactiveMongoOperations;
#Autowired
public ContentServiceImplementation(ReactiveMongoOperations reactiveMongoOperations) {
this.reactiveMongoOperations = reactiveMongoOperations;
}
#Override
public Mono<Content> getContent(String id) {
return reactiveMongoOperations.findById(id, Content.class);
}
#Override
public Mono<Content> createContent(Mono<Content> contentMono) {
return reactiveMongoOperations.save(contentMono);
}
Data Config Dont know is this is useful
#Bean
public ReactiveMongoDatabaseFactory mongoDatabaseFactory(MongoClient mongoClient){
return new SimpleReactiveMongoDatabaseFactory(mongoClient, DATABASE_NAME);
}
#Bean
public ReactiveMongoOperations reactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory){
return new ReactiveMongoTemplate(mongoDatabaseFactory);
}
Lmk if i'm missing some critical info
Your problem may come from your controller, you declare your path like so:
#GetMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
So unless you have a / at the end of your controller class mapping you will have issues because your final URL will look like this :
http://localhost:8080/my/route/get1
instead of :
http://localhost:8080/my/route/get/1
Your #PathVariable looks strange as well, try doing this instead :
#PathVariable("id") String id
To ensure Spring is going to map {id} to your #PathVariable

Validation in service layer (SpringBoot)

I have a DTO which is validated at Controller layer with a mix of BeanValidation (javax.validation) and a custom Validator (org.springframework.validation.Validator). This way I can check if the input provided is valid and then convert the DTO in an entity and forward it to the Service layer.
#Data
public class UserDTO {
#NotBlank
#Size(max = 25)
private String name;
#NotNull
private Date birthday;
#NotNull
private Date startDate;
private Date endDate;
private Long count;
}
public class UserDTOValidator implements Validator {
private static final String START_DATE= "startDate";
private static final String END_DATE= "endDate";
private static final String COUNT= "count";
#Override
public boolean supports(Class<?> clazz) {
return UserDTO.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
UserDTO vm = (UserDTO) target;
if (vm.getEndDate() != null) {
if (vm.getStartDate().after(vm.getEndDate())) {
errors.rejectValue(START_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
if (vm.getEndDate().equals(vm.getStartDate()) || vm.getEndDate().before(vm.getStartDate())) {
errors.rejectValue(END_DATE, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
}
if (vm.getCount() < 1) {
errors.rejectValue(COUNT, ErrorCode.ILLEGAL_ARGUMENT.toString(), ErrorCode.ILLEGAL_ARGUMENT.description());
}
.....
}
}
public class UserController {
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new UserDTOValidator());
}
#PostMapping()
public ResponseEntity<UserDTO> create(#RequestBody #Valid UserDTO userDTO) {
.....
}
.....
}
Then there is the business logic validation. For example: the #Entity User's startDate must be after some event occurred and the count has to be greater than some X if the last created User's birthDay is in Summer, in other case, the entity should be discarded by the User service.
#Service
#Transactional
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private SomeEventService someEventService ;
#Override
public User create(User entity) {
String error = this.validateUser(entity);
if (StringUtils.isNotBlank(error)) {
throw new ValidationException(error);
}
return this.userRepository.save(entity);
}
....
private String validateUser(User entity) {
SomeEvent someEvent = this.someEventService.get(entity.getName());
if (entity.getStartDate().before(someEvent.getDate())) {
return "startDate";
}
User lastUser = this.userRepository.findLast();
....
}
}
However I feel like this is not the best approach to handle business logic validation. What should I do? ConstraintValidator/HibernateValidator/JPA Event listeners? Can they work at #Entity class level or I have to create X of them for each different field check? How do you guys do it in a real production application?
In my suggestion,
Use classic field level validation by #Valid
sample
void myservicemethod(#Valid UserDTO user)
For custom business level validation in entity level, create validate method in DTO itself
sample
class UserDTO {
//fields and getter setter
void validate() throws ValidationException {
//your entity level business logic
}
}
This strategy will help to keep entity specific validation logic will be kept within the entity
If still you have validation logic that requires some other service call, then create custom validation annotation with custom ConstraintValidator (eg. question on stackoverflow). In this case, my preference will be to invoke UserDTO.validate() from this custom validator in spiote of calling from service
This will help to keep your validation logic separated from service layer and also portable and modular

#GetMapping, ambigious mapping

I'm trying to create several routes for #GetMapping. For example, localhost:8080/tasks and localhost:8080/tasks/?status=...
So I created several methods as below.
Controller
#RestController
#RequestMapping(value = "/tasks", produces = MediaType.APPLICATION_JSON_VALUE)
#ExposesResourceFor(Task.class)
public class TaskRepresentation {
private final TaskResource taskResource;
public TaskRepresentation(TaskResource taskResource) {
this.taskResource = taskResource;
}
#GetMapping
public ResponseEntity<?> getAllTasks() {
return new ResponseEntity<>(this.taskResource.findAll(), HttpStatus.OK);
}
#GetMapping
public ResponseEntity<?> getTasksStatus(#RequestParam("status") int status) {
return new ResponseEntity<>(this.taskResource.getTasksByStatus(status), HttpStatus.OK);
}
}
Resource
#RepositoryRestResource(collectionResourceRel = "task")
public interface TaskResource extends JpaRepository<Task, String> {
#GetMapping
List<Tache> getTasksByStatus(#RequestParam int status);
}
Error
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'taskRepresentation' method
public org.springframework.http.ResponseEntity<?> org.miage.tache.boundary.TacheRepresentation.getTasksStatus(int)
to {GET /tasks, produces [application/json]}: There is already 'taskRepresentation' bean method
(The only solution is to create only one route for #GetMapping with optionnal params?)
Can you help me ?
Thanks for help.
Coming from the other answer, as this one more specific.
You can narrow down your endpoint mapping by specifying the needed query parameters.
#GetMapping
public ResponseEntity<?> getAllTasks() {
return ResponseEntity.ok().body(this.taskResource.findAll());
}
#GetMapping(params = "status")
public ResponseEntity<?> getAllTasksWithStatus(#RequestParam("status") final int status) {
return ResponseEntity.ok().body(this.tacheResource.getTachesByEtat(status));
}
Docs link.
Note : As params is an array, you can specify multiple values with
#GetMapping(params = { "status", "date" })
You can do something like this :
#RestController
#RequestMapping(value = "/tasks", produces = MediaType.APPLICATION_JSON_VALUE)
#ExposesResourceFor(Task.class)
public class TaskRepresentation {
private final TaskResource taskResource;
public TaskRepresentation(TaskResource taskResource) {
this.taskResource = taskResource;
}
#GetMapping
public ResponseEntity<?> getTasksStatus(#RequestParam(value="status", required=false) Integer status) {
if(status==null){
return new ResponseEntity<>(this.taskResource.findAll(), HttpStatus.OK);
}
return new ResponseEntity<>(this.taskResource.getTasksByStatus(status.intValue()), HttpStatus.OK);
}
}

How to validate generic bean in REST service?

In my current project I frequently use bulk requests. I have simple BulkRequest<T> class:
import java.util.List;
import javax.validation.constraints.NotNull;
public class BulkRequest<T> {
#NotNull private List<T> requests;
public List<T> getRequests() { return this.requests; }
public void setRequests(List<T> requests) { this.requests = requests; }
}
It very simple to use with other beans, for example:
#RequestMapping(value = "/departments/{departmentId}/patterns",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> post(
final #PathVariable long departmentId,
final #Valid #RequestBody BulkRequest<AddPatternRequest> bulkRequest
) {
...
}
AddPatternRequest contains own rules for validation and represents only one request, which can be collected to bulk request:
import javax.validation.constraints.NotNull;
public class AddPatternRequest {
#NotNull private Long pattern;
public Long getPattern() { return this.pattern; }
public void setPattern(Long pattern) { this.pattern = pattern; }
}
But there's a problem. After the controller receives the bulk request, it validates only BulkRequest and checks if requests collection is null or not, but I need to validate nested request too.
How can I do it?
Add #Valid to the requests. Like this
#NotNull
#Valid
private List<T> requests;
Then nested objects are also validated

Categories

Resources