I have two web services based on Spring boot, REST and MVC. The first service(DesignService) project calls the second service(UserService) project passing a email as parameter and gets the user object(User.java) back. How can I handle if no user is found.
DesignService project
repository.java
#Autowired
RestTemplate restTemplate;
getUser(String email){
restTemplate.postForObject("url for User service",email,User.class); //calls User service for Object[1]
}
UserService
UserController.java
#RequestMapping()
public ResponseEntity<User> getUser(#RequestBody String email){
User user = repository.getUser(email); //return a user object from DB
return new ResponseEntity<User>(user);[2]
}
Repository.java
public User getUser(String email){
//query db for user
return user; //what to return if no user is found [3]
}
what to do at point [3] if user not found .throw exception or return empty user object?
what to do at [2] any excpetion handling / http status code / ... ??
how do I handle these exceptions at [1]. I dont think checking for user as null is a good idea.
* excuse me if am wrong in some syntax as I have typed it directly .
Return null, or if you're on Java 8, an empty Optional
If no user is found, return an empty Response with a 404 (NOT_FOUND) status, i.e.
new ResponseEntity(HttpStatus.NOT_FOUND);
Catch org.springframework.web.client.HttpClientErrorException
Finally, why are you POSTing to an endpoint that only returns a resource? You should be doing a GET, and from the looks of it, your request mapping should only support GET (method = RequestMethod.GET).
Related
In my Spring Boot app, I just use Optional for the first time and after examining several projects and topics, now I am trying to build an approach as shown below:
Repository:
Optional<Employee> findByEmail(String email);
Service:
public Response findByEmail(String email) {
return employeeRepository.findByEmail(email)
// if record is found, I think no need to return status or message
.map(e -> Response.builder().data(e).build())
.orElseGet(() -> Response.builder().status(404)
.data(null).message("Not found!").build());
}
Response:
#Data
#Builder
public class Response {
private int status;
private Object data;
private String message;
}
Controller:
#GetMapping("/employees/{email}")
public ResponseEntity<Response> findByEmail(#PathVariable String email) {
final Response response = employeeService.findByEmail(email);
return ResponseEntity
.status(response.getStatus())
.body(response.getMessage(), response.getData());
// throws "Expected 1 arguments but found 2" error
}
Here is the points that I need to be clarified:
1. Is this a proper approach to use a common response for all the Optional types in a Spring Boot app? If not, how should I change it (I want to return a common response from the Service)?
2. How to fix the throws "Expected 1 arguments but found 2" error in the Controller?
From my comment above - You are mixing concerns. Service is supposed to only care about business logic (e.g. not HTTP Status codes). That's controller's job. Use of Optional is correct, but the Response return type from service layer is not. Also errors like Not Found are automatically handled by a Rest Controller in Spring boot if a resource is not found. If you want to add custom logic and prepare generic responses, include a proper exception handling e.g. #ControllerAdvice (which allows you reuse exceptions for controllers).
As an example, one of the solutions would be to throw NoSuchElementException.
This is illustrative and would apply if you want to handle other such situations (e.g. null pointers, internal server error, authentication errors in a more custom manner) in a generic manner.
public Employee findByEmail(String email) {
return employeeRepository.findByEmail(email) //assuming findByEmail is returning an Optional<Employee>, otherwise - simply use a null check.
.orElseThrow(NoSuchElementException::new)
}
Inside #ControllerAdvice class
#ExceptionHandler(NoSuchElementException.class)
#ResponseBody
#Order(Ordered.HIGHEST_PRECEDENCE)
public final ResponseEntity<APIResponseErrorContainer> handleNotFound(
NoSuchElementException ex) {
// log exception here if you wish to
return new ResponseEntity<>(createCustomResponseBody(), HttpStatus.NOT_FOUND);
}
I'm using a bit of a personalized security back-end due to the nature of the app and was trying out how to implement a few simple error returns in my REST API controller. It's simple enough to do in a html page controller like I have in the following:
#Controller
public class HomeController {
#Autowired
private UserService userService;
#GetMapping("/home.html")
public String home(Model model) {
String redirect = "home";
if(!userService.getCurrentUser().isCanAccessService()) {
redirect = "unauthorized";
}
return redirect;
}
}
I can easily just redirect it to the unauthorized page that I made since I'm returning the string value here. However, when I go to a REST API it's not as simple:
#RestController
public class bagelController {
#Autowired
private bagelService bagelService;
#Autowired
private UserService userService;
#GetMapping("/rest/bagel/search")
public Bagel searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel;
if(!userService.getCurrentUser().isBagelEditAccess()) {
bagel = null;
// I want to return a 401 or direct to my unathorized page if I get an invalid user here.
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
// if my bagel object returns null, I want to return a 404 or direct to a 404 not
found page here.
}
return bagel;
}
You can have a ControllerAdvice which handles exceptions and their HTTP return code. Then you can annotate a method in it the following way for example:
#ExceptionHandler(NoSuchEntityException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
This will return a 404 code every time it encounters a NoSuchEntityException (custom exception). So you can throw such an exception when you check if an entity is null. You can use the same thing for 401 or any other HTTP code as well.
One way to do this.
#GetMapping("/rest/bagel/search")
public ResponseEntity<Bagel> searchBagel (#RequestParam(value = "bagel", required = false) String bagel,
#RequestParam(value = "bagelInd", required = false, defaultValue = "1") int bagelInd) {
Bagel bagel = null;
if(!userService.getCurrentUser().isBagelEditAccess()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
else {
bagel = bagelService.getbagel(bagel, bagelInd);
if(bagel == null) {
return ResponseEntity.notFound().build();
}
}
return ResponseEntity.ok(bagel);
}
You can create custom exceptions within your application for this scenario like BagelNotFoundException and UnauthorizedException. Both these custom exception classes can extend Exception class or more specific classes from java exception hierarchy. You can annotate these custom exception classes with #ResponseStatus annotation to provide the http status code that should be sent in the response.
Next, you need to throw the objects of these exceptions within your controller.
Once this exception is thrown, an exception handler should be present within your application to take care of these exceptions. The same can be defined using #ControllerAdvice and #ExceptionHandler within your custom exception handler classes.
This way you'll be able to send appropriate response to the client, and the client application needs to redirect the user to error pages based on the response code received.
Hope this helps!
I have just started learning spring web flux. and there is complete change or perspective of how to do the
work in reactive programming instead of imperative programming.
So, i would like to achieve a very simple output.
I have response class with fields success, message and List data.
#Data
#Accessors(chain = true)
public class Response {
private boolean success;
private String message;
private List data;
}
and a Request class
#Data
#Accessors(chain = true)
public class LoginRequest {
private String email;
private String password;
}
I also have userRepository with webFlux.
Mono<User> findUserByEmail(String email);
and i have a login action like this.
#PostMapping("/login")
public Mono<Response> login(#RequestBody Mono<LoginRequest> request) {
}
Now i have to return response based on what userRepository gave me.
It could return null if no user is present
It could give me User class object if user is found
now i have to check if password matches with the password given in LoginRequest
So I have to change response based on User repository
like if user not found
I have to return response with success = false and message = "No user found"
if user found with invalid password
I have to return response with success = false and message = "invalid password"
and if everything is good then
I have to return success = true , message = "Welcome", and List with username,email etc.
I have tried lot of ways but at the end i failed to achieve this.
You don't need a Mono as your argument to your controller, you can accept the value after standard data binding from Spring. Check the Spring docs for examples: https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-controller
You also shouldn't get null from your repo, if the user can't be found you'll get an empty Mono (so .map, .filter, etc won't be called). That being the case, you can use .switchIfEmpty as replacement for null checking.
If you get data you can simply .map it to what you need since you don't need to block for any other data:
public Mono<Response> login(LoginRequest request) {
return repo.findUserByEmail(request.getEmail())
.map(user ->
Objects.equals(request.getPassword(), user.getPassword())
? new Response(true, "Welcome", Collections.emptyList())//populate list here
: new Response(false, "invalid password", Collections.emptyList()))
//user wasn't found in the repo
.switchIfEmpty(Mono.just(new Response(false, "No user found", Collections.emptyList())));
}
I am developing a web app using spring boot and mvc. I have controllers and in every controller i have to check if session is exist. In example in getAll method i am checking if session is existing but i have to write this check code in every method , every controller. Is there any shortcut to do that?
#Controller
#RequestMapping("/Sale")
public class SaleController
{
#Autowired
private SaleRepository saleRepository;
#GetMapping
public ModelAndView getAll(#SessionAttribute(required=false) User user)
{
if(user==null)
return new ModelAndView("redirect:/");
else
{
ModelAndView modelAndView=new ModelAndView("/view/sales.jsp");
List<Sale> sales=saleRepository.findAll();
modelAndView.addObject("sales",sales);
return modelAndView;
}
}
}
You can use a Filter and apply it to all requests or only those matching a specific pattern.
To check for a session you would use HttpServletRequest.getSession(false) and check for null.
I have a model for logging in user in my REST API, corresponds to User table (email and password as table columns)
#Entity
public class User {
#Id
#GeneratedValues
private Long id;
private String email;
private String password;
+GET , +SET
}
Then there is #Controller which is making call to above User Entity using JPAService
#Controller
#RequestMapping("/rest/auths")
public class AuthController {
#Autowired
private UserService authService;
#RequestMapping(value = "/login", method = RequestMethod.POST)
public #ResponseBody ResponseEntity<AuthLoginFormResource> login(#RequestBody AuthLoginFormResource sentAuth) {
User user = authService.login(sentAuth.toUser());
AuthLoginFormResource res = new AuthLoginFormResourceAsm().toResource(user);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(res.getLink("self").getHref()));
return new ResponseEntity<AuthLoginFormResource>(res, HttpStatus.OK);
}
}
AuthLoginFormResource : -
public class AuthLoginFormResource extends ResourceSupport {
private String email;
private String password;
private boolean success;
public User toUser() {
User user = new User();
user.setEmail(email);
user.setPassword(password);
//user.setSuccess(false);
return user;
}
+GET, +SET
}
AuthLoginFormResourceAsm : -
public class AuthLoginFormResourceAsm extends ResourceAssemblerSupport<User, AuthLoginFormResource> {
public AuthLoginFormResourceAsm() {
super(User.class, AuthLoginFormResource.class);
}
#Override
public AuthLoginFormResource toResource(User user) {
AuthLoginFormResource res = new AuthLoginFormResource();
res.setEmail(user.getEmail());
res.setPassword(user.getPassword());
//res.setSuccess(user.isSuccess()); // Success is not existing in USER
res.add(linkTo(AuthController.class).withSelfRel());
return res;
}
}
There are 2 issues -
I need to send a success flag as boolean in response for which i have added a boolean success to AuthLoginFormResource. But, AuthLoginFormResource gets set
only from AuthLoginFormResourceAsm.toResource method , which in turn does
it from entity User. As User entity models database where there is
no success column, I am not able to set success at this place.
So, should I add dummy success field to User Entity and set that from service
method , though there is no such field in database or create a new Entity representing Login Form here and return that ?
Same problem with another field that is a token for authentication
which does not exist in database but is part of response.
What is correct place for setting such fields in ResourceSupport object - inside database Entity and return from Service / creating another Form Model entity on top of Domain Model and return from service.
This is basic question I am facing in many places where data model and forms don't match one to one.
I strongly recommend the following;
Modify UserService.login method to return true or false based on successfull authentication instead of retrieved user object from database.
Return only true or false with status OK and FAIL, as part of the response not the entire AuthLoginFormResource. This is a bad practice because you are sending out the username and password as part of the request and response, back and forth in a roundtrip. If someone is evesdropping they can easily figure out what username passwords work and what don't.
Or
Consider using Basic Authorization, Digest Authorization or OAuth if you fancy than this custom Authentication Implementation. Using Spring Security you can achieve any of the aforementioned really easily.