Now that we have seen how to build back ends for applications the next logical step for us is to construct front ends for applications. In this course I have focused on building REST APIs with Spring Boot, since that approach gives us the maximum flexibility in building front ends. The back ends we have been building can serve as back ends for mobile applications, web applications, and even desktop applications. Our back ends can also integrate with just about every major framework for writing web applications, all the way from plain JavaScript apps up through React applications.
In the CMSC 445 course coming up in the fall I will be showing how to write mobile apps that integrate with our back ends. In the CMSC 406 course in the spring of next year we will construct React apps that integrate with our Spring Boot back end applications.
In these notes I am going to demonstrate yet another method for writing web front ends. This method will turn our Spring Boot applications into full stack applications that can implement both the front end and the back end for a web application.
The Spring framework was originally designed as a framework for writing full stack web applications. Originally, the framework used a technology called Java Server Pages (JSP) to implement the front end. Over time JSPs have been replaced with more modern alternatives. Today, most full stack Spring Boot web applications make use of a more modern approach called the Thymeleaf template library to construct web front ends.
To integrate Thymeleaf into a Spring Boot project we begin by adding a dependency for the Thymeleaf library:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
To demonstrate how Thymeleaf works I am going to build a full stack web application based on the survey system you encountered in the second midterm exam. Specifically, I am going to construct a full stack web application that allows users to construct surveys along with their questions.
The lecture notes below assume that you have a basic familiarity with HTML. If you do not know anything about HTML, the World Wide Web consortium has a basic tutorial on HTML available at w3schools.com/html/.
To implement our REST APIs we have been building controller classes annotated with the @RestController annotation. To construct web front ends we are going to make use of a much older style of the controller class, a controller class annotated with @Controller.
Here is the beginning of the controller class for our web application:
@Controller
public class BuilderController {
@Autowired
private SurveyService service;
@GetMapping("/")
public String landingPage() {
return "landing.html";
}
}
Just as we are used to doing, this controller class is going to act as a container for a set of controller methods. Just as before, those methods are going to be annotated with @GetMapping and @PostMapping.
The big difference in our new controller methods is that they will not be returning JSON data. Our new controllers are going to be setting up and returning HTML pages to be displayed in a browser.
The first controller method you see here simply returns the name of a HTML file we would like to have displayed.
The HTML file itself appears in a separate templates folder in the resources section of our application. Here is the content of that first HTML page:
<!DOCTYPE html>
<head>
<title>
Survey Builder
</title>
</head>
<body>
<h1>Welcome to the Survey Builder</h1>
<p>To get started, log in to an existing account or create a new one.</p>
<p><a href="#" th:href="@{/login}">Log in to existing account</a></p>
<p><a href="#" th:href="@{/newaccount}">Create a new account</a></p>
</body>
This first page is actually a Thymeleaf template. A template is an HTML page with placeholders for content that we want the Thymeleaf system to insert into the page. Thymeleaf content in our templates will appear in the form of special attributes we attach to HTML elements in the page. These special attributes are all identifiable by their 'th:' prefix.
In the example above I have inserted Thymeleaf attributes in a couple of link elements. These attributes will replace the link's usual href attribute with special Thymeleaf expressions that tell the links where to go in our application.
To set up links, Thymeleaf uses @ expressions. The first example above, @{/login} points the first of our links at the /login route in our application. To implement that route I will add an appropriate controller method to our application's controller.
Here is the code for the controller method that handles the /login route for our application.
@GetMapping("/login")
public String loginPage(Model model) {
UserDTO dto = new UserDTO();
model.addAttribute("user",dto);
return "login.html";
}
This controller method takes a special parameter, a Model object. The Model object will act as a container for data we want to insert into a page that will be rendered by a Thymeleaf template. In this first example, we are going to insert a UserDTO object into the model. That object will act as a backing bean for a form that our login page will contain.
The last thing the controller method does is to return the name of the template file that contains the HTML code for our login page.
Here now is the HTML code for that login.html page:
<!DOCTYPE html>
<head>
<title>Log In</title>
</head>
<body>
<p>Log in to an existing account</p>
<form th:action="@{/doLogin}" th:object="${user}" method="post">
<label>Name: <input type="text" th:field="*{name}" /></label>
<label>Password: <input type="password" th:field="*{password}" /></label>
<button type="submit">Submit</button>
</form>
</body>
The most important thing going on in this page is an HTML form element that implements the login form for our web application. In Thymeleaf forms, each form is expected to have an action method and a backing bean. The action method is specified via an @ expression in a th:action attribute. This @ expression points to a route in our application that will handle the mechanics of logging in. The backing bean is set up by a th:object attribute. To specify which object will act as the backing bean, we use the Spring Expression Language expression ${user}, which specifies that we want to use the attribute from the Model with the name of "user". This is the object we added to the Model in the loginPage() method above.
HTML forms contain input elements where the user will be entering values. The form example here has two input elements in it, a text input element for the user to enter a user name and a password input element where they will enter a password. When the user enters values in these input elements and clicks the Submit button to submit the form, the Thymeleaf will copy the values entered by the user into properties of the backing bean. The property linked to each input element is specified by a special th:field attribute in each input element. This attribute uses a field expression to name the property of the backing bean we want linked to the input element. The first input element uses the expression*{name}, which links that element to the name property of the backing bean. The second input element uses the expression *{password} which links the input element to the password property of the backing bean.
When the user clicks the Submit button in the login form the application will go to the controller method that acts as the action method for the form.
Here is the controller method that serves as the action method for our login form.
@PostMapping("/doLogin")
public String doLogin(UserDTO user,HttpSession session)
{
int id = service.checkLogin(user);
if(id > 0) {
session.setAttribute("user", id);
return "redirect:/main";
}
return "redirect:/login";
}
The first thing to note here is that the UserDTO that we added to our Model in setting up the form is now going to come back to us as a parameter with its values filled in by the user. Our only job then is to send the UserDTO to our service class to check whether or not its values are valid.
Another parameter that appears here for the first time is an HttpSession object. In the world of HTTP when browsers connect to a server they will frequently establish a special structure called a session. When a browser first contacts a server the server will establish that session data structure and then send the browser a token that uniquely identifies that session. Then, with each subsequent request that the browser sends the server it will embed that token in the request headers and the server can use that token to look up the session data structure. In the Java world that session data structure lives in an HttpSession object. We can ask Spring Boot to give us that HttpSession object as a parameter in any controller method where we want it. The main thing that the HttpSession does for us is that it provides a way to store information that we want to access in later requests from the same client. In this example we are going to be storing the user id of the user once they have successfully logged in. That will give a way to know which user is making any later requests when they come in.
Finally, the controller method that handles the post request will need to return a string that tells the system what page to navigate to next. If the user has logged in successfully we will want to go to the application's main page at /main. If the user was not able to log in we will want to return to the login page at /login instead.
Normally, to go to /main we would just return "/main". Here I have used the form "redirect:/main" instead to head off a simple problem. If we had had our method return "/main" the system would try to go directly to the template "main.html". This is the wrong thing to do, since we haven't set up the Model for that page yet. By using the redirect form we tell the system that we instead want to go to the controller method for the route "/main", which will set up the Model and then go to the template "main.html".
Once a user has successfully logged in we will take them to the application's main page. This page will display a list of all of the surveys the user has created along with a form for creating a new survey.
Here is the code for that controller method.
@GetMapping("/main")
public String mainPage(HttpSession session,Model model) {
int id = (Integer) session.getAttribute("user");
model.addAttribute("surveys",service.getSurveys(id));
SurveyDTO newSurvey = new SurveyDTO();
model.addAttribute("newSurvey",newSurvey);
return "main.html";
}
The main page will display a list of surveys that the user has created. To support that feature we will get the user's id from the session object and then ask the service class to fetch the list of surveys for that user. In preparation for showing a form that will allow the user to make a new survey we also need to embed an appropriate backing object for the form into the Model.
Here is the HTML for the main page:
</head>
<body>
<div th:unless="${#lists.isEmpty(surveys)}">
<h4>Your surveys</h4>
<ul>
<li th:each="s : ${surveys}">
<a href="#"
th:href="@{/survey/{id}/questions(id=${s.id})}"
th:text="${s.title}">
</a>
</li>
</ul>
</div>
<p>Create a new survey:</p>
<form th:action="@{/doNewSurvey}"
th:object="${newSurvey}" method="post">
<label>Title:
<input type="text" th:field="*{title}" />
</label>
<label>Description:
<input type="text" th:field="*{description}" />
</label>
<label>Prompt:
<input type="text" th:field="*{prompt}" />
</label>
<button type="submit">Submit</button>
</form>
</body>
The page has two parts. At the top there is a division that will display a list of surveys. At the bottom there is a form for creating a new survey.
The div that contains the list of surveys uses the th:unless attribute that specifies that the div should be visible in the page unless the list of surveys is empty. The Spring Expression Language offers a number of useful global functions for us to use. Here we are using the #lists.isEmpty() function.
Inside the division we will have an HTML ul element to implement an unordered list. To make the individual elements of that list we use an HTML list item, or li, element. To make one li element for each survey in our list of surveys we equip the li element with the special th:each property. The value of the th:each property
s : ${surveys}
says to iterate over the list of surveys in the Model and assign each survey to a loop object s. For each object in the list we will make a new li element for that object.
Instead of simply displaying the title for each survey in our list we will instead display a link that takes us to a page for that survey. The link will display the title of the survey, and is linked to a parameterized path of the form
/survey/{id}/questions
that embeds the id of the survey. Thymeleaf constructs parameterized paths by embedding one or more placeholders in a URL and then providing values for those placeholders at the end of the @ expression.
Here is the controller method for the new survey form at the bottom of the main page:
@PostMapping("/doNewSurvey")
public String doNewSurvey(SurveyDTO survey,HttpSession session,Model model)
{
int id = (Integer) session.getAttribute("user");
Survey s = service.createSurveyForUser(survey,id);
return "redirect:/survey/" + s.getId() + "/questions";
}
Both the links at the top of the main page and the post method for creating a new survey will take us to page that displays questions for a survey.
Here is the controller method that sets up the Model for that page:
@GetMapping("/survey/{id}/questions")
public String editQuestions(@PathVariable("id") Integer id,Model model) {
List<Question> qs = service.findQuestions(id);
model.addAttribute("survey",id);
model.addAttribute("questions",qs);
QuestionDTO newQuestion = new QuestionDTO();
model.addAttribute("newQuestion",newQuestion);
return "questions.html";
}
Here I have taken advantage of the fact that Spring controller methods can work with path variables. The path variable here stores the id of the survey we are looking at.
Here is the HTML for the questions page:
<!DOCTYPE html>
<head>
<title>Write Questions</title>
</head>
<body>
<div th:unless="${#lists.isEmpty(questions)}">
<h4>Your questions</h4>
<table>
<tr><th>Question</th><th>Options</th></tr>
<tr th:each="q : ${questions}">
<td th:text="${q.prompt}"></td>
<td th:text="${q.options}"></td>
</tr>
</table>
</div>
<p>Add a new question:</p>
<form th:action="@{/survey/{id}/questions(id=${survey})}"
th:object="${newQuestion}" method="post">
<label>Question:
<input type="text" th:field="*{prompt}" />
</label>
<label>Options:
<input type="text" th:field="*{options}" />
</label>
<button type="submit">Submit</button>
</form>
<p><a href="#" th:href="@{/main}">Done</a></p>
</body>
The top of this page displays a list of questions for a survey in a table. Once again we use the th:each construct to make one row in the table for each question in the survey.
At the bottom of the page there is a form for adding a new question to the survey. Here is the controller method for that form's action:
@PostMapping("/survey/{id}/questions")
public String addQuestion(@PathVariable("id") Integer id,QuestionDTO dto) {
dto.setSurvey(id);
service.saveQuestion(dto);
return "redirect:/survey/" + id + "/questions";
}
In order to make the HTML code in the templates as easy as possible to understand I limited myself to using only plain, vanilla HTML code in those templates. This gives the application an exceedingly plain appearance. To make the application look more modern, I made a copy of the project and then asked Cline to add CSS code to style the application using the Bootstrap CSS library.
The application I just built will allow users to create surveys. As a follow-up to this, I asked Cline to build a simple HTML/JavaScript single page application that will allow users to take surveys. The code for that application appears in the index.html file in the resources/static folder. Cline also wrote additional controller methods and made some changes to repositories and other classes to support this new application. The JavaScript application that Cline wrote communicates with new RestController in our application to get its work done.
Click the button below to download an archive containing the final version of the project.
When you run the application you can access the survey builder application by entering the URL
http://localhost:8085/builder
in your browser. To access the survey taker application, use the URL
http://localhost:8085/