Skip to content

2. Controllers

Jorge edited this page Jan 9, 2018 · 1 revision

Controllers

The code related to the controller layer can be found in jorge.rv.quizzz.controller. In QuizZz, this layer is sub-divided in a REST API Controller and a Web Controller.

REST API Controller

QuizZz offers a REST API for non-web clients and asynchronous calls from AngularJs. The URLs related to the REST API start with /api/. The full code can be found in jorge.rv.quizzz.controller.rest.v1.

Let's go now through the steps to set up a REST endpoint using Spring MVC.

Create a class to hold the Controller

We'll start by creating a new class for our controller code, tell Spring to treat it as a rest controller and what the base URL is for the requests it's going to handle.

@RestController
@RequestMapping(QuizController.ROOT_MAPPING)
public class QuizController {

	public static final String ROOT_MAPPING = "/api/quizzes";
  • @RestController - This will tell Spring that this class is meant to be a controller and that it's a REST API. This pre-configures a few things such as changing the types returned to the end user to json or xml instead of html.
  • @RequestMapping() - This annotation tells Spring what the base URL is.

Obtain a reference to the Service(s)

In order to route requests through to the service layer, we will need an instance of the service(s) we're going to use. We can use Spring's Dependency Injection Container to get an instance of them:

@Autowired
private QuizService quizService;

Define an endpoint

Once Spring knows about our controller and we have a way to access the required services, we can set up an endpoint by creating a method inside the class:

@RequestMapping(value = "", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public Quiz save(@AuthenticationPrincipal AuthenticatedUser user, @Valid Quiz quiz, BindingResult result) {

	RestVerifier.verifyModelResult(result);
	return quizService.save(quiz, user.getUser());
}

@RequestMapping(value = "/registration", method = RequestMethod.POST)
public ResponseEntity<?> signUp(@Valid User user, BindingResult result) {

	RestVerifier.verifyModelResult(result);
	User newUser = registrationService.startRegistration(user);

	if (registrationService.isRegistrationCompleted(newUser)) {
		return new ResponseEntity<User>(newUser, HttpStatus.CREATED);
	} else {
		return new ResponseEntity<User>(newUser, HttpStatus.OK);
	}
}
  • @RequestMapping() - When this annotation comes in a function, it will help Spring define the full URL that's going to be supported by appending the base URL to the URL here provided You can alsodefine the HTTP verbs that are going to be supported.

  • @ResponseStatus() - There are several ways to tell Spring which HTTP Response code we want returned to the user. In QuizZz, we are using this annotation to tell Spring to use a particular status if nothing goes wrong. We'll cover later in this document how exceptions are handled and how that changes the response status.

  • ResponseEntity<> - There are cases when @ResponseStatus() is not enough and we need to return a different non-error status depending on the outcome of the request. Returning a ResponseEntity<> at our endpoint allows us to set both the body and the HTTP code for the response.

As you can see from the rest of the snippet, there are other annotations Spring MVC provides us with in order to get access to the data that's sent along with the request. Here are a few of them used throughout QuizZz:

  • @AuthenticationPrincipal - It gives us access to the user that made the request. This annotation actually belongs to Spring Security, and not Spring MVC.
  • @Valid + BindingResult - Spring can perform checks for you to validate user input. In order for Spring to know whether a model, in this case a Quiz, is valid, you can add constraints such as @Size or @NotNull to the model's attributes. Take a look at the code to get some examples of how this works.
  • @RequestParam() - Looks for a particular paramter on the request. This parameter can come as an URI parameter or a parameter in the request's body.
  • @PathVariable - Looks for variables passed as part of the built URL. You can define variables in the url using curly brackets. For example: "/{quiz_id}".

The last thing to notice about the code is that the function returns a Quiz object. Because we defined the controller as a rest controller, Spring MVC is already configured to render a Json object with the contents of the model using Jackson. You can further configure how the Json object is rendered by using Jackson annotations on the model such as @JsonIgnore or @JsonPropertyOrder.

Web Controller

The Web Controller will serve HTML content to web browsers. All controllers will return references to Thymeleaf views, and the parameters to render them into well-formed HTML files. There are CSS and AngularJs static resources in the build as well. However, these can be accessed directly without the need of a controller. Spring Boot comes pre-configured so you can simply drop your view templates into src/main/resources/templates and your static resources such as Javascript or CSS into src/main/resources/static.

The steps to create a Web Controller are very similar to the Rest Controller. We'll mainly focus on the differences here.

Create a class to hold the Controller

Similar to the Rest Controller, we'll also start by creating a class to hold the controller's code:

@Controller
public class WebQuizController {

As you may have noticed, we are now using @Controller instead of @RestController. This will remove all the pre-configured REST features we had on the REST Controller.

Notice also that we are not using @RequestMapping() here. This is not a because we are configuring a Web controller and not a Rest controller. The only reason we are not using it here is because QuizZz'es Web endpoints are be defined directly at the root, wihtout a base URL.

Obtain a reference to the Service(s)

We'll obtain the refrences to our services in the exact same way as on the REST Controller:

@Autowired
private QuizService quizService;

Something to keep in mind here is that because of how editing data works using a web frontend, the user may attempt to load the screen to edit data he/she is allowed to read but not edit. The access control via Spring AOP would stop the actual editing from happening if the user attempts to make an edition but it would be wrong to present the user with the edit screen altogether if he's not going to be able to edit it afterwards. For this, we'll be adding some Access Control using the Access Control services.

@Autowired
AccessControlService<Quiz> accessControlServiceQuiz;

@Autowired
AccessControlService<Question> accessControlServiceQuestion;

Define an endpoint

Similarly to what we did on the REST Controller, we'll create a method to host the code for our endpoint. There are a few things to notice here

@RequestMapping(value = "/editAnswer/{question_id}", method = RequestMethod.GET)
public ModelAndView editAnswer(@PathVariable long question_id) {
	Question question = questionService.find(question_id);
	accessControlServiceQuestion.canCurrentUserUpdateObject(question);

	ModelAndView mav = new ModelAndView();
	mav.addObject("question", question);
	mav.setViewName("editAnswers");

	return mav;
}
  • @RequestMapping() - This works exactly as it did on our previous example.
  • ModelAndView - As usual with Spring, there are many ways to define a response in Spring MVC. For our Web Controller, we'll be using ModelAndView objects. This class allows us to set a view to display and the parameters on how to render the view. Views in QuizZz are developed using Thymeleaf and can be found in src/main/resources/templates.
  • The call to canCurrentUserUpdateObject() will throw an exception if the permission is denied. We'll cover how these exceptions are handled in the Error Handling section.

Error handling

We've covered how to set up some basic endpoints for REST and Web controllers. However, what happens when things don't go as expected? QuizZz has a few custom exceptions defined in the exceptions package that can be thrown at the Service Layer and which should be handled appropiately at the controller. Errors that need and specific action can be directly caught in our controller code. For the rest, we simply need to inform the user about the problem. We'll let the exception dive into Spring and configure Spring to call our controller advice classes to deal with them in one place.

Create a class to host the controller advice

There is one controller advice class per controller:

Rest Controller:

@ControllerAdvice("jorge.rv.quizzz.controller.rest.v1")
public class RestExceptionHandler {

Web Controller:

@ControllerAdvice("jorge.rv.quizzz.controller.web")
public class WebExceptionHandler {

@ControllerAdvice() tells Spring which package contains the controller code to be handled by the Controller Advice.

Define the advices

We'll now create methods to handle each of the exceptions that may be thrown by the application.

Rest Controller:

@ExceptionHandler(UnauthorizedActionException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public ErrorInfo unauthorizedAction(HttpServletRequest req, Exception ex) {
	return new ErrorInfo(req.getRequestURL().toString(), ex, HttpStatus.UNAUTHORIZED.value());
}

Web Controller:

@ExceptionHandler(UnauthorizedActionException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ModelAndView unauthorizedAction(HttpServletRequest req, Exception ex) {
	return setModelAndView(req.getRequestURL().toString(), ex, HttpStatus.UNAUTHORIZED.value());
}
  • @ExceptionHandler - Use this annotation to let Spring know which exception you want to handle in the method.
  • @ResponseBody - This annotation tells Spring that you are returning an object that needs to be rendered into Json or XML, instead of returning a view. On the Rest Controller, this annotation is already included as part of the @RestController annotation, so we don't need to set it explicitly.