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

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

Open http://localhost:8080/

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 

References

https://getbootstrap.com/

https://www.chartjs.org/

comments powered by Disqus