I'm wondering about proper way of implementating of controller in case of GET request with multiple request params given. In my understanding of REST it's much better to have one endpoint with additional parameters for filtering/sorting than several endpoints (one for each case). I'm just wondering about maintanance and extensibility of such endpoint. Please have a look on example below :
#RestController
#RequestMapping("/customers")
public class CustomerController {
#Autowired
private CustomerRepository customerRepo;
#GetMapping
public Page<Customer> findCustomersByFirstName(
#RequestParam("firstName") String firstName,
#RequestParam("lastName") String lastName,
#RequestParam("status") Status status, Pageable pageable) {
if (firstName != null) {
if (lastName != null) {
if (status != null) {
return customerRepo.findByFirstNameAndLastNameAndStatus(
firstName, lastName, status, pageable);
} else {
return customerRepo.findByFirstNameAndLastName(
firstName, lastName, pageable);
}
} else {
// other combinations omitted for sanity
}
} else {
// other combinations omitted for sanity
}
}
}
Such endpoint seems to be very convenient to use (order of parameters doesn't matter, all of them are optional...), but maintaining something like this looks like a hell (number of combinations can be enormous).
My question is - what is the best way to deal with something like this? How is it designed in "professional" APIs?
What is the best way to deal with something like this?
The best way to deal with it is to use the tools already available. As you are using Spring Boot and, I assume therefore, Spring Data JPA then enable the QueryDsl suppport and web support extensions for Spring Data JPA.
You controller then simply becomes:
#GetMapping
public Page<Customer> searchCustomers(
#QuerydslPredicate(root = Customer.class) Predicate predicate, Pageable pageable) {
return customerRepo.findBy(predicate, pageable);
}
and your repository is simply extended to support QueryDsl:
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long>,
QueryDslPredicateExecutor<Customer>{
}
You can now query by any combination of params without writing any further code.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web.type-safe
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl
Good day. I can't call myself a professional, but here are some tips which can make this controller looks better.
Use DTO instead of using a group of parameters
public class CustomerDTO {
private String firstName;
private String lastName;
private String status;
}
With this class your method's signature will look like this:
#GetMapping
public Page<Customer> findCustomersByFirstName(CustomerDTO customerDTO, Pageable pageable) {
...
}
Use validation if you need one
For example, you can make some of these fields are required:
public class CustomerDTO {
#NotNull(message = "First name is required")
private String firstName;
private String lastName;
private String status;
}
Don't forget to add #Valid annotation before the DTO parameter in your controller.
Use specification instead of this block with if-else
Here is a great guide on how to do it - REST Query Language with Spring Data JPA Specifications
Use the service layer, don't need to call repository from the controller
#GetMapping
public Page<Customer> findCustomersByFirstName(#Valid CustomerDTO customerDTO, BindingResult bindingResult, Pageable pageable) {
if (bindingResult.hasErrors()) {
// error handling
}
return customerService.findAllBySpecification(new CustomerSpecification(customerDTO));
}
Your controller should not contain any logic about working with entities or some business stuff. It's only about handling request/errors, redirects, views, etc...
Its good to have a POST request with such validations instead of a GET request.You can use following method for the controller.
#PostMapping(value = "/findCustomer",produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> findCustomersByFirstName(#Valid #RequestBody Customer customer){
return customerRepo.findByFirstNameAndLastNameAndStatus(customer.getFirstName, customer.getLastName(), customer.getStatus(), pageable);
}
use the DTO as follows.
public class Customer {
private String firstName;
private String lastName;
private String status;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName= firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName= lastName;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status= status;
}
public LivenessInputModel(String firstName, String lastName, String status) {
this.firstName= firstName;
this.lastName= lastName;
this.status= status;
}
public LivenessInputModel() {
}
}
And add a controller level exception advice to return the response in errors.
#ControllerAdvice
public class ControllerExceptionAdvice {
private static final String EXCEPTION_TRACE = "Exception Trace:";
private static Logger log = LoggerFactory.getLogger(ControllerExceptionAdvice.class);
public ControllerExceptionAdvice() {
super();
}
#ExceptionHandler({ BaseException.class })
public ResponseEntity<String> handleResourceException(BaseException e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(e);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, e.getHttpStatus());
}
#ExceptionHandler({ Exception.class })
public ResponseEntity<String> handleException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.INTERNAL_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ MethodArgumentNotValidException.class })
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException e,
HttpServletRequest request, HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
ValidationException validationEx = new ValidationException(e);
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(validationEx);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, validationEx.getHttpStatus());
}
#ExceptionHandler({ HttpMediaTypeNotSupportedException.class, InvalidMimeTypeException.class,
InvalidMediaTypeException.class, HttpMessageNotReadableException.class })
public ResponseEntity<String> handleMediaTypeNotSupportException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.BAD_REQUEST_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
public ResponseEntity<String> handleMethodNotSupportException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.METHOD_NOT_ALLOWED;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.METHOD_NOT_ALLOWED);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
#ExceptionHandler({ MissingServletRequestParameterException.class })
public ResponseEntity<String> handleMissingServletRequestParameterException(Exception e, HttpServletRequest request,
HttpServletResponse response) {
log.error(EXCEPTION_TRACE, e);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
BaseExceptionResponse exceptionDto = new BaseExceptionResponse(httpStatus.value(),
ExceptionMessages.BAD_REQUEST_DEFAULT);
return new ResponseEntity<>(exceptionDto.toString(), responseHeaders, httpStatus);
}
}
Actually, you answered half of the answer yourself, Query Params are used for filtration purposes, and as you can see in your code this will be allowed via GET request. But your question regarding validations is something a trade-off.
For example; if you don't want to have this kind of check, you can depend on mandatory required = true which is the default in #RequestParam, and handle it in the response immediately.
Or you can use alternatively #RequestBody with support of #Valid for more clear info for what wrong had occurred; for example
#PostMapping(value = "/order")
public ResponseEntity<?> submitRequest(#RequestBody #Valid OrderRequest requestBody,
Errors errors) throws Exception {
if (errors.hasErrors())
throw new BusinessException("ERR-0000", "", HttpStatus.NOT_ACCEPTABLE);
return new ResponseEntity<>(sendOrder(requestBody), HttpStatus.CREATED);
}
// Your Pojo
public class OrderRequest {
#NotNull(message = "channel is required")
private String channel;
#NotNull(message = "Party ID is required")
private long partyId;
}
For more information check this #Valid usage in Spring
This way will decouple your validation mechanism from Controller layer to Business layer. which in turns will save lots of boilerplate code, but as you noticed with change to POST instead.
So in general, there is no direct answer to your question, and the short answer is it depends, so choose whatever easy for you with good capabilities and less maintainance will be the best practice
As an alternative solution besides other ones, you can use JpaSpecificationExecutor<T> in your repository and create a specification object based on your arguments and pass it to the findAll method.
So, your repository should extend from the JpaSpecificationExecutor<Customer> interface as follows:
#Repository
interface CustomerRepository extends JpaSpecificationExecutor<Customer> {
}
Your controller should get the required arguments as Map<String, String to gain dynamic behavior.
#RestController
#RequestMapping("/customers")
public class CustomerController {
private final CustomerRepository repository;
#Autowired
public CustomerController(CustomerRepository repository) {
this.repository = repository;
}
#GetMapping
public Page<Customer> findAll(#RequestBody HashMap<String, String> filters, Pageable pageable) {
return repository.findAll(QueryUtils.toSpecification(filters), pageable);
}
}
And, you should define a method to convert provided arguments to Specification<Customer>:
class QueryUtils {
public static Specification<Customer> toSpecification(Map<String, String> filters) {
Specification<Customer> conditions = null;
for (Map.Entry<String, String> entry : filters.entrySet()) {
Specification<Customer> condition = Specification.where((root, query, cb) -> cb.equal(root.get(entry.getKey()), entry.getValue()));
if (conditions == null) {
conditions = condition;
} else {
conditions = conditions.and(condition);
}
}
return conditions;
}
}
Also, You can use the Meta model to make better criteria query and combine it with the provided solution.
Related
I have the following REST endpoints in Spring boot
#GetMapping(value = "students", params = {"name"})
public ResponseEntity<?> getByName(#RequestParam final String name) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
#GetMapping(value = "students", params = {"tag"})
public ResponseEntity<?> getByTag(#RequestParam final String tag) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
The above handlers work fine for the following requests:
localhost:8080/test/students?name="Aron"
localhost:8080/test/students?tag="player"
However, whenever I try the following:
localhost:8060/test/students?name="Aron"&tag="player"
it throws java.lang.IllegalStateException: Ambiguous handler methods mapped and responds with an HTTP 500
How can I change this behavior? I want my app to respond only when I get either a tag query parameter or a name query parameter.
For anything else, I want it to ignore even if it's a combination of two parameters.
Why is it throwing the ambiguous error here and how can we handle that?
You can use #RequestParam(required = false):
#GetMapping(value = "students")
public ResponseEntity<?> get(
#RequestParam(required = false) final String name,
#RequestParam(required = false) final String tag) {
if ((name == null) == (tag == null)) {
return new ResponseEntity<>(false, HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(true, HttpStatus.OK);
}
it seems you can use negations in params. Something like:
#GetMapping(value = "students", params = {"name", "!tag"})
public ResponseEntity<?> getByName(#RequestParam final String name) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
#GetMapping(value = "students", params = {"tag", "!name"})
public ResponseEntity<?> getByTag(#RequestParam final String tag) {
return new ResponseEntity<>(true, HttpStatus.OK);
}
References: Advanced #RequestMapping options
I do have a class CustomResponseEntityExceptionHandler class extending ResponseEntityExceptionHandler with 2 methods one for handling wrong formats and the other one when a resource (url) is requested but does not exist. I would like to give the user a custom message instead of the default White Label Error page of Spring. I'm trying to understand why my handleResourceNotFoundException method from CustomResponseEntityExceptionHandler is not called when a non existing URI is requested but instead I keep having the White Label Error page. Thanks for your help!
curl localhost:8080/doesnotexist
This is my CustomResponseEntityExceptionHandler
#ExceptionHandler(Exception.class)
public final ResponseEntity<ErrorDetails> handleAllWrongFormatExceptions(WrongFormatException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(true));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(ResourceNotFoundException.class)
public final ResponseEntity<ErrorDetails> handleResourceNotFoundException(ResourceNotFoundException ex,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
Simple class for holding custom error details
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
} }
And here is my controller
#Controller
public class StringManipController {
#Autowired
private StringProcessingService stringProcessingService;
#GetMapping("/")
#ExceptionHandler(ResourceNotFoundException.class)
#ResponseBody
public String home(#RequestParam(name = "value", required = false, defaultValue = "") String value) {
return "Welcome to the String Processing Application";
}
#GetMapping("/stringDedup")
#ResponseBody
public ProcessedString doManip(#RequestParam(name = "value", required = false, defaultValue = "") String value) {
String result = stringProcessingService.getStringManipulation(value);
return new ProcessedString(result);
}
#GetMapping("/writeNumber")
#ResponseBody
public ProcessedString getWriteNumber(
#RequestParam(name = "value", required = false, defaultValue = "") String value) {
String number = stringProcessingService.getNumberToWords(value);
return new ProcessedString(number);
} }
And here the ResourceNotFoundException class
#ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = 266853955330077478L;
public ResourceNotFoundException(String exception) {
super(exception);
} }
If you used Spring Boot:
#ControllerAdvice
public class CustomResponseEntityExceptionHandler {
#ExceptionHandler(value = { NoHandlerFoundException.class })
public ResponseEntity<Object> noHandlerFoundException(Exception ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("test");
}
}
or
#ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, "Test", headers, status, request);
}
}
In the org.springframework.web.servlet.DispatcherServlet class there is a variable called throwExceptionIfNoHandlerFound. If this is set to true a method called noHandlerFound will throw a NoHandlerFoundException. Your exception handler will now catch it.
Add this property to your application.properties file: spring.mvc.throw-exception-if-no-handler-found=true
I am trying to use Kafka as a messaging queue in my controller (so task):
#RequestMapping("users")
#RestController
public class UserController extends BaseController {
private static final String KAFKA_TOPIC = "virto_users";
#Autowired
private KafkaTemplate<String, String> mKafkaTemplate;
#PutMapping(value = "{id}")
public ResponseEntity<String> put(#PathVariable UUID id,
#RequestBody String profile) {
String url = ServerConfig.USERS_HOST + "/users/" + id;
ResponseEntity<String> entity = request.put(url, profile);
HttpStatus status = entity.getStatusCode();
if (status == HttpStatus.CREATED || status == HttpStatus.NO_CONTENT) {
return entity;
}
JSONObject json = new JSONObject();
json.put("id", id);
json.put("profile", profile);
sendMessage(json.toString());
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
public void sendMessage(String msg) {
mKafkaTemplate.send(KAFKA_TOPIC, msg);
}
#KafkaListener(topics = KAFKA_TOPIC, groupId = KafkaConfig.GROUP_ID)
public void listen(String message) {
JSONObject json = new JSONObject(message);
UUID id = UUID.fromString(json.getString("id"));
String profile = json.getString("profile");
put(id, profile);
}
}
If my inner service is available and returned CREATED or NO_CONTENT then all is right and I can return result else I need to return the NO_CONTENT status and put this message in my queue to try again until it will be processed. I made it like above but it doesn't look like a good solution, imho. I wanted to ask just for a some advice how can I improve this solution or that it's normal.
The normal uri which triggers the default controller to get all cars is just "/cars"
I want to be able to search for cars aswell with an uri, for example: "/cars?model=xyz" which would return a list of matching cars. All the request parameters should be optional.
The problem is that even with the querystring the default controller triggers anyway and I always get "all cars: ..."
Is there a way to do this with Spring without a separate search uri (like "/cars/search?..")?
code:
#Controller
#RequestMapping("/cars")
public class CarController {
#Autowired
private CarDao carDao;
#RequestMapping(method = RequestMethod.GET, value = "?")
public final #ResponseBody String find(
#RequestParam(value = "reg", required = false) String reg,
#RequestParam(value = "model", required = false) String model
)
{
Car searchForCar = new Car();
searchForCar.setModel(model);
searchForCar.setReg(reg);
return "found: " + carDao.findCar(searchForCar).toString();
}
#RequestMapping(method = RequestMethod.GET)
public final #ResponseBody String getAll() {
return "all cars: " + carDao.getAllCars().toString();
}
}
You can use
#RequestMapping(method = RequestMethod.GET, params = {/* string array of params required */})
public final #ResponseBody String find(#RequestParam(value = "reg") String reg, #RequestParam(value = "model") String model)
// logic
}
ie, the #RequestMapping annotation has a property called params. If all of the parameters that you specify are contained in your request (and all other RequestMapping requirements match), then that method will be called.
Try a variation of this:
#Controller
#RequestMapping("/cars")
public clas CarController
{
#RequestMapping(method = RequestMethod.get)
public final #ResponseBody String carsHandler(
final WebRequest webRequest)
{
String parameter = webRequest.getParameter("blammy");
if (parameter == null)
{
return getAll();
}
else
{
return findCar(webRequest);
}
}
}
I am working on a REST api using Spring-MVC and json. I running my automatic tests using Jetty and an in-memory database. I want to test that posting an invalid domain object gives me the error message from the #NotEmpty annotation. But all I get is the default Jetty 400 Bad Request page.
I have a domain class with some validation:
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotEmpty(message = "Name is a required field")
private String name;
/* getters, setters */
}
Here's the controller
#Controller
public class CompanyController {
#RequestMapping(value = "/company",
method = RequestMethod.POST,
consumes = "application/json")
public void createCompany(
#Valid #RequestBody final Company company,
final HttpServletResponse response) {
//persist company
response.setStatus(HttpServletResponse.SC_CREATED);
response.setHeader("location", "/company/" + company.getId());
}
}
How can I get the value "Name is a required field" returned as part of the response?
This does it:
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public void exceptionHandler(final MethodArgumentNotValidException ex,
final HttpServletResponse response) throws IOException {
for (final ObjectError objectError : ex.getBindingResult().getAllErrors()) {
response.getWriter().append(objectError.getDefaultMessage()).append(".");
}
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
Spring 3.2 variant using ControllerAdvice and ResponseEntityExceptionHandler:
#ControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object>
handleMethodArgumentNotValid(MethodArgumentNotValidException e,
HttpHeaders headers, HttpStatus status,
WebRequest request) {
BindingResult bindingResult = e.getBindingResult();
ArrayList<String> errors = new ArrayList<String>();
for (ObjectError error : bindingResult.getGlobalErrors()) {
errors.add(error.getDefaultMessage());
}
for (FieldError error : bindingResult.getFieldErrors()) {
errors.add(
String.format("%s %s", error.getField(), error.getDefaultMessage())
);
}
return handleExceptionInternal(e, StringUtils.join(errors, ", "),
headers, HttpStatus.UNPROCESSABLE_ENTITY, request);
}
You need to add a BindingResult to your handler method. That object will automatically add the results to the Model, which can then be easily accessed from the Spring Forms taglib.