Spring - Thymeleaf
Overview
Spring Boot MVC application with Thymeleaf template & basic spring security support, uses bootstrap for CSS and chart.js for rendering charts. Creates uber jar to deploy.
Github: https://github.com/gitorko/project79
Quick Overview
To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run
1git clone https://github.com/gitorko/project79
2cd project79
3docker-compose -f docker/docker-compose.yml up
Features
A Spring Web MVC application that renders thymeleaf templates as HTML. Supports basic integration with spring security and provides login logout support. Uses Spring Data to persist data into the HSQL db. A file based HSQL server db is used so that data persists across restarts. This can easily be changed to in-memory HSQL db. Spring dev tools allow seamless reload on any changes for html and java files so you can view the changes in the browser as soon as you edit them.
- Supports basic login via spring security
- Bootstrap 5
- Login screen
- CRUD UI for adding and removing customer
- HSQL db
- Spring JPA
- Thymeleaf template
- Chart.js charts for bar,pie,stack charts with data from rest api
Design
Code
On Intellij to allow spring dev tools to reload on change you need to enable 'Update classes and resources' as shown below
Spring MVC controller renders the HTML.
1package com.demo.project79.controller;
2
3import java.security.Principal;
4import java.util.Date;
5
6import javax.websocket.server.PathParam;
7
8import com.demo.project79.domain.Customer;
9import com.demo.project79.repo.CustomerRepository;
10import lombok.RequiredArgsConstructor;
11import lombok.extern.slf4j.Slf4j;
12import org.springframework.beans.factory.annotation.Autowired;
13import org.springframework.stereotype.Controller;
14import org.springframework.ui.Model;
15import org.springframework.web.bind.annotation.GetMapping;
16import org.springframework.web.bind.annotation.PathVariable;
17import org.springframework.web.bind.annotation.PostMapping;
18import org.springframework.web.bind.annotation.RequestParam;
19import org.springframework.web.servlet.mvc.support.RedirectAttributes;
20
21@Controller
22@Slf4j
23@RequiredArgsConstructor
24public class HomeController {
25
26 @Autowired
27 CustomerRepository customerRepo;
28
29 @GetMapping(value = "/")
30 public String home(Model model) {
31 Iterable<Customer> customerLst = customerRepo.findAll();
32 model.addAttribute("customerLst", customerLst);
33 model.addAttribute("serverTime", new Date());
34 return "home";
35 }
36
37 @PostMapping(value = "/save")
38 public String customerSave(@RequestParam(value = "firstName") String firstName, @RequestParam(value = "lastName") String lastName,
39 Model model, RedirectAttributes redirAttrs, Principal principal) {
40 log.info("Name: " + firstName);
41 Customer customer = new Customer();
42 customer.setFirstName(firstName);
43 customer.setLastName(lastName);
44 customerRepo.save(customer);
45 redirAttrs.addFlashAttribute("successMsg", "Successfully added user by: " + principal.getName());
46 return "redirect:/";
47 }
48
49 @GetMapping(value = "/delete/{id}")
50 public String customerSave(@PathVariable Long id, RedirectAttributes redirAttrs, Principal principal) {
51 log.info("User {} deleted by {}", id, principal.getName());
52 customerRepo.deleteById(id);
53 redirAttrs.addFlashAttribute("successMsg", "Deleted user: " + id);
54 return "redirect:/";
55 }
56
57 @GetMapping(value = "/charts")
58 public String chartsHome(Model model) {
59 return "charts";
60 }
61
62}
Spring Security is configured for BASIC authentication
1package com.demo.project79.config;
2
3import org.springframework.context.annotation.Configuration;
4import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
5import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
8import org.springframework.security.crypto.factory.PasswordEncoderFactories;
9import org.springframework.security.crypto.password.PasswordEncoder;
10
11@Configuration
12@EnableWebSecurity
13public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
14
15 public static final String USER_ROLE = "ADMIN";
16 public static final String USER_NAME = "admin";
17 public static final String USER_PASSWORD = "admin@123";
18
19 @Override
20 protected void configure(HttpSecurity http) throws Exception {
21 http
22 .csrf().disable()
23 .authorizeRequests()
24 .antMatchers("/", "/home", "/rest/**").permitAll()
25 .antMatchers("/js/**", "/css/**", "/images/**").permitAll()
26 .anyRequest().authenticated()
27 .and()
28 .formLogin()
29 .loginPage("/login")
30 .permitAll()
31 .and()
32 .logout()
33 .permitAll();
34 }
35
36 @Override
37 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
38 PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
39 auth.inMemoryAuthentication()
40 .withUser(USER_NAME)
41 .password(encoder.encode(USER_PASSWORD))
42 .roles(USER_ROLE);
43 }
44}
chart.js is a library that provides various charts, the project renders charts and the data is fetched from Rest API.
1<!doctype html>
2<html lang="en" xmlns:th="https://www.thymeleaf.org">
3<head>
4 <div th:replace="fragments/general :: include-frag"/>
5 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6 <script src="js/charts.js"></script>
7</head>
8<body>
9
10<div th:replace="fragments/general :: menu-frag"/>
11
12<div class="container">
13 <div th:replace="fragments/general :: flash-message-frag"/>
14 <br/>
15 <br/>
16 <div class="row">
17 <div class="col">
18 <canvas id="piechartContainer" width="200" height="300"></canvas>
19 </div>
20 <div class="col">
21 <canvas id="barchartContainer" width="200" height="300"></canvas>
22 </div>
23 </div>
24 <br/>
25 <br/>
26 <div class="row">
27 <div class="col">
28 <canvas id="linechartContainer" width="200" height="300"></canvas>
29 </div>
30 <div class="col">
31 <canvas id="stackchartContainer" width="200" height="300"></canvas>
32 </div>
33 </div>
34</div>
35
36</body>
37</html>
1$(function() {
2
3 var backgroundColors = [
4 'rgba(255, 99, 132, 0.2)',
5 'rgba(255, 159, 64, 0.2)',
6 'rgba(255, 205, 86, 0.2)',
7 'rgba(75, 192, 192, 0.2)',
8 'rgba(54, 162, 235, 0.2)',
9 'rgba(153, 102, 255, 0.2)',
10 'rgba(201, 203, 207, 0.2)'
11 ];
12
13 var borderColors = [
14 'rgb(255, 99, 132)',
15 'rgb(255, 159, 64)',
16 'rgb(255, 205, 86)',
17 'rgb(75, 192, 192)',
18 'rgb(54, 162, 235)',
19 'rgb(153, 102, 255)',
20 'rgb(201, 203, 207)'
21 ];
22
23 $.getJSON("/rest/pie-data", function(json) {
24 new Chart(document.getElementById("piechartContainer"), {
25 type: 'pie',
26 data: {
27 labels: json[0],
28 datasets: [{
29 backgroundColor: backgroundColors,
30 borderColor: borderColors,
31 borderWidth: 1,
32 hoverOffset: 4,
33 data: json[1]
34 }]
35 },
36 options: {
37 title: {
38 display: true,
39 text: 'Pie Chart'
40 },
41 responsive: true,
42 maintainAspectRatio: false,
43 scales: {
44 yAxes: [{
45 ticks: {
46 beginAtZero:true
47 }
48 }]
49 }
50 }
51 });
52 });
53
54 $.getJSON("/rest/pie-data", function(json) {
55 new Chart(document.getElementById("barchartContainer"), {
56 type: 'bar',
57 data: {
58 labels: json[0],
59 datasets: [{
60 label: 'My First Dataset',
61 backgroundColor: backgroundColors,
62 borderColor: borderColors,
63 borderWidth: 1,
64 hoverOffset: 4,
65 data: json[1]
66 }]
67 },
68 options: {
69 title: {
70 display: true,
71 text: 'Bar Chart'
72 },
73 responsive: true,
74 maintainAspectRatio: false,
75 scales: {
76 yAxes: [{
77 ticks: {
78 beginAtZero:true
79 }
80 }]
81 }
82 }
83 });
84 });
85
86 $.getJSON("/rest/pie-data", function(json) {
87 new Chart(document.getElementById("linechartContainer"), {
88 type: 'line',
89 data: {
90 labels: json[0],
91 datasets: [{
92 label: 'My First Dataset',
93 backgroundColor: backgroundColors,
94 borderColor: borderColors,
95 fill: false,
96 borderWidth: 1,
97 hoverOffset: 4,
98 data: json[1]
99 }]
100 },
101 options: {
102 title: {
103 display: true,
104 text: 'Line Chart'
105 },
106 responsive: true,
107 maintainAspectRatio: false,
108 scales: {
109 yAxes: [{
110 ticks: {
111 beginAtZero:true
112 }
113 }]
114 }
115 }
116 });
117 });
118
119 $.getJSON("/rest/column-data", function(json) {
120 new Chart(document.getElementById("stackchartContainer"), {
121 type: 'bar',
122 data: {
123 labels: json[0]["data"],
124 datasets: [{
125 label: json[1]["name"],
126 backgroundColor: backgroundColors[0],
127 borderColor: borderColors[0],
128 fill: false,
129 borderWidth: 1,
130 hoverOffset: 4,
131 data: json[1]["data"]
132 },{
133 label: json[2]["name"],
134 backgroundColor: backgroundColors[1],
135 borderColor: borderColors[1],
136 fill: false,
137 borderWidth: 1,
138 hoverOffset: 4,
139 data: json[2]["data"]
140 },{
141 label: json[3]["name"],
142 backgroundColor: backgroundColors[2],
143 borderColor: borderColors[2],
144 fill: false,
145 borderWidth: 1,
146 hoverOffset: 4,
147 data: json[3]["data"]
148 }]
149 },
150 options: {
151 title: {
152 display: true,
153 text: 'Stack Chart'
154 },
155 responsive: true,
156 maintainAspectRatio: false,
157 scales: {
158 x: {
159 stacked: true,
160 },
161 y: {
162 stacked: true
163 }
164 }
165 }
166 });
167 });
168
169});
1<!doctype html>
2<html lang="en" xmlns:th="https://www.thymeleaf.org">
3<head>
4 <div th:replace="fragments/general :: include-frag"/>
5 <script src="js/home.js"></script>
6</head>
7<body>
8
9<div th:replace="fragments/general :: menu-frag"/>
10
11<div class="container">
12 <div th:replace="fragments/general :: flash-message-frag"/>
13
14 <form method="post" th:action="@{/save}" role="form" class="form-horizontal">
15 <div class="row">
16 <div class="col text-center">
17 <p class="text-end" th:inline="text">Current Date : [[${serverTime}]]</p>
18 </div>
19 </div>
20 <br/>
21
22 <div class="row">
23 <div class="col text-center">
24 <h2>Customers</h2>
25 </div>
26 </div>
27 <br/>
28
29 <div class="row">
30 <div class="col-4">
31 <form>
32 <div class="mb-3">
33 <label for="firstName" class="form-label">First Name</label>
34 <input type="text" name="firstName" class="form-control" id="firstName" aria-describedby="nameHelp">
35 <div id="nameHelp" class="form-text">Enter your name!</div>
36 </div>
37 <div class="mb-3">
38 <label for="lastName" class="form-label">Last Name</label>
39 <input type="text" name="lastName" class="form-control" id="lastName">
40 </div>
41 <button type="submit" class="btn btn-primary">Submit</button>
42 </form>
43 </div>
44 <div class="col-8">
45 <table id="customer-table" class="table table-striped table-bordered" style="width:100%">
46 <thead>
47 <tr>
48 <th>
49 First Name
50 </th>
51 <th>
52 Last Name
53 </th>
54 <th>
55 Action
56 </th>
57 </tr>
58 </thead>
59 <tbody>
60 <tr th:each="customer: ${customerLst}">
61 <td th:inline="text">[[${customer.firstName}]]</td>
62 <td th:inline="text">[[${customer.lastName}]]</td>
63 <td><a th:href="@{'/delete/' + ${customer.id}}"<i class="bi bi-trash"></i></a></td>
64 </tr>
65 </tbody>
66
67 </table>
68 </div>
69 </div>
70
71 </form>
72</div>
73
74</body>
75</html>
1<!doctype html>
2<html lang="en" xmlns:th="https://www.thymeleaf.org">
3<head>
4 <div th:replace="fragments/general :: include-frag"/>
5</head>
6<body>
7
8<div th:replace="fragments/general :: login-menu-frag"/>
9
10<div class="container">
11 <form method="post" th:action="@{/login}" role="form" class="form-horizontal" style="max-width: 400px; margin: auto">
12 <br/>
13 <br/>
14 <h2>Login</h2>
15
16 <div class="mb-3">
17 <label for="exampleInputEmail1" class="form-label">Username</label>
18 <input type="text" name="username" class="form-control" id="exampleInputEmail1" aria-describedby="usernameHelp">
19 <div id="usernameHelp" class="form-text">Enter ldap username.</div>
20 </div>
21 <div class="mb-3">
22 <label for="exampleInputPassword1" class="form-label">Password</label>
23 <input type="password" name="password" class="form-control" id="exampleInputPassword1">
24 </div>
25 <button type="submit" class="btn btn-primary" style="width:100%">Submit</button>
26
27 <br/>
28 <br/>
29
30 <div th:if="${param.logout}" class="alert alert-success" role="alert">
31 You have been logged out
32 </div>
33 <div th:if="${param.error}" class="alert alert-danger" role="alert">
34 Invalid username and password!
35 </div>
36 </form>
37
38</div>
39
40</body>
41</html>
Setup
Project 79
Spring Boot MVC Web project Thymeleaf, Login, Charts
https://gitorko.github.io/spring-boot-thymeleaf/
Version
Check version
1$java --version
2openjdk 17.0.3 2022-04-19 LTS
Postgres DB
1docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
2docker ps
3docker exec -it pg-container psql -U postgres -W postgres
4CREATE USER test WITH PASSWORD 'test@123';
5CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
6grant all PRIVILEGES ON DATABASE "test-db" to test;
7
8docker stop pg-container
9docker start pg-container
Dev
To run the code.
1./gradlew clean build
2./gradlew bootRun
Prod
To build the uber jar & run the jar.
1./gradlew clean build
2cd build/libs
3java -jar project79-1.0.0.jar
1user: admin
2pwd: admin@123
Docker
1./gradlew clean build
2docker build -f docker/Dockerfile --force-rm -t project79:1.0.0 .
3docker images |grep project79
4docker tag project79:1.0.0 gitorko/project79:1.0.0
5docker push gitorko/project79:1.0.0
6docker-compose -f docker/docker-compose.yml up