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
Implementation
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 com.demo.project79.domain.Customer;
7import com.demo.project79.repo.CustomerRepository;
8import lombok.RequiredArgsConstructor;
9import lombok.extern.slf4j.Slf4j;
10import org.springframework.beans.factory.annotation.Autowired;
11import org.springframework.stereotype.Controller;
12import org.springframework.ui.Model;
13import org.springframework.web.bind.annotation.GetMapping;
14import org.springframework.web.bind.annotation.PathVariable;
15import org.springframework.web.bind.annotation.PostMapping;
16import org.springframework.web.bind.annotation.RequestParam;
17import org.springframework.web.servlet.mvc.support.RedirectAttributes;
18
19@Controller
20@Slf4j
21@RequiredArgsConstructor
22public class HomeController {
23
24 @Autowired
25 CustomerRepository customerRepo;
26
27 @GetMapping(value = "/")
28 public String home(Model model) {
29 Iterable<Customer> customerLst = customerRepo.findAll();
30 model.addAttribute("customerLst", customerLst);
31 model.addAttribute("serverTime", new Date());
32 return "home";
33 }
34
35 @PostMapping(value = "/save")
36 public String customerSave(@RequestParam(value = "firstName") String firstName, @RequestParam(value = "lastName") String lastName,
37 Model model, RedirectAttributes redirAttrs, Principal principal) {
38 log.info("Name: " + firstName);
39 Customer customer = new Customer();
40 customer.setFirstName(firstName);
41 customer.setLastName(lastName);
42 customerRepo.save(customer);
43 redirAttrs.addFlashAttribute("successMsg", "Successfully added user by: " + principal.getName());
44 return "redirect:/";
45 }
46
47 @GetMapping(value = "/delete/{id}")
48 public String customerSave(@PathVariable Long id, RedirectAttributes redirAttrs, Principal principal) {
49 log.info("User {} deleted by {}", id, principal.getName());
50 customerRepo.deleteById(id);
51 redirAttrs.addFlashAttribute("successMsg", "Deleted user: " + id);
52 return "redirect:/";
53 }
54
55 @GetMapping(value = "/charts")
56 public String chartsHome(Model model) {
57 return "charts";
58 }
59
60}
Spring Security is configured for BASIC authentication
1package com.demo.project79.config;
2
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7import org.springframework.security.core.userdetails.User;
8import org.springframework.security.crypto.factory.PasswordEncoderFactories;
9import org.springframework.security.crypto.password.PasswordEncoder;
10import org.springframework.security.provisioning.InMemoryUserDetailsManager;
11import org.springframework.security.web.SecurityFilterChain;
12
13@Configuration
14@EnableWebSecurity
15public class WebSecurityConfig {
16
17 public static final String USER_ROLE = "ADMIN";
18 public static final String USER_NAME = "admin";
19 public static final String USER_PASSWORD = "admin@123";
20
21 @Bean
22 SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
23 return http
24 .csrf(csrf -> csrf.disable())
25 .authorizeHttpRequests(authorize -> authorize
26 .requestMatchers("/", "/home", "/rest/**").permitAll()
27 .requestMatchers("/js/**", "/css/**", "/images/**").permitAll()
28 .anyRequest().authenticated()
29 )
30 .formLogin(form -> form
31 .loginPage("/login")
32 .permitAll()
33 )
34 .logout(logout -> logout
35 .permitAll()
36 ).build();
37 }
38
39 @Bean
40 public InMemoryUserDetailsManager userDetailsService() {
41 PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
42 return new InMemoryUserDetailsManager(
43 User.withUsername(USER_NAME)
44 .password(encoder.encode(USER_PASSWORD))
45 .roles(USER_ROLE)
46 .build()
47 );
48 }
49}
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
1# Project 79
2
3Spring Boot MVC Web project Thymeleaf, Login, Charts
4
5[https://gitorko.github.io/spring-boot-thymeleaf/](https://gitorko.github.io/spring-boot-thymeleaf/)
6
7### Version
8
9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14```
15
16### Postgres DB
17
18```
19docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
20docker ps
21docker exec -it pg-container psql -U postgres -W postgres
22CREATE USER test WITH PASSWORD 'test@123';
23CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
24grant all PRIVILEGES ON DATABASE "test-db" to test;
25
26docker stop pg-container
27docker start pg-container
28```
29
30### Dev
31
32To run the code.
33
34```bash
35./gradlew clean build
36./gradlew bootRun
37```
38
39### Prod
40
41To build the uber jar & run the jar.
42
43```bash
44./gradlew clean build
45cd build/libs
46java -jar project79-1.0.0.jar
47```
48
49Open [http://localhost:8080/](http://localhost:8080/)
50
51```
52user: admin
53pwd: admin@123
54```
55
56### Docker
57
58```bash
59./gradlew clean build
60docker build -f docker/Dockerfile --force-rm -t project79:1.0.0 .
61docker images |grep project79
62docker tag project79:1.0.0 gitorko/project79:1.0.0
63docker push gitorko/project79:1.0.0
64docker-compose -f docker/docker-compose.yml up
65```
66