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 

Open http://localhost:8080/

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.

  1. Supports basic login via spring security
  2. Bootstrap 5
  3. Login screen
  4. CRUD UI for adding and removing customer
  5. HSQL db
  6. Spring JPA
  7. Thymeleaf template
  8. 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

References

https://getbootstrap.com/

https://www.chartjs.org/

comments powered by Disqus