Spring Boot + FreeMarker CRUD Example

Apache FreeMarker™ is a free Java-based template engine. It's is a general purpose template engine, with no dependency on servlets or HTTP or HTML. It used for generating HTML web pages (dynamic web page generation), e-mails, configuration files, source code, etc

Templates are written in the FreeMarker Template Language (FTL), which is a simple and specialized language. Java is used to prepare the data (issue database queries, do business calculations). Then, FreeMarker displays that prepared data using templates. In the template you are focusing on how to present the data, and outside the template you are focusing on what data to present. Similar like most of MVC approach.

In this tutorial, we will learn on how to build a simple CRUD Spring Boot application with FreeMarker as server side templating engine.

Start a Spring Boot Project

Refer to Scaffolding Spring Boot Application to generate your Spring Boot application via Spring Initializr (https://start.spring.io/ ) or Spring Boot CLI. Add (at least) these five dependencies:

  • Spring Web
    Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Spring Data JPA
    Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.
  • Apache Freemarker
    Java library to generate text output (HTML web pages, e-mails, configuration files, source code, etc.) based on templates and changing data.
  • MySQL Driver
    MySQL JDBC and R2DBC driver.
  • Lombok
    Java annotation library which helps to reduce boilerplate code.

Here the final pom.xml:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-freemarker-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-freemarker-example</name>
    <description>Demo project for Spring Boot with FreeMarker template</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>no.api.freemarker</groupId>
            <artifactId>freemarker-java8</artifactId>
            <version>1.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
                    

We will check about "extra dependencies" later on.

From the pom.xml, you should aware that for this project, we will use MySQL as data source.

Prepare MySQL

The application that we will create is a simple notes application. The application able to create, retrieve, update and delete a Note. A Note is a simple class with a title and content.

Assuming we will use a database called "dariawan", here the DDL for table notes:

CREATE TABLE `notes` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(255) DEFAULT NULL, `content` varchar(4000) DEFAULT NULL, `created_on` datetime NOT NULL, `updated_on` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;

For initial data, let's insert one record in this table:

insert into notes (title, content, created_on, updated_on) values ('Hello World!', 'First note in notes application', '2019-11-07 08:00:00', '2019-11-07 18:30:45')

Now, let’s configure Spring Boot to use MySQL as our data source by adding MySQL database url, username, and password in the src/main/resources/application.properties file:

# Connection url for the database "dariawan" spring.datasource.url = jdbc:mysql://localhost:3306/dariawan?serverTimezone=Asia/Singapore # Username and password spring.datasource.username = barista spring.datasource.password = cappuccino # Allows Hibernate to generate SQL optimized for a particular DBMS spring.jpa.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.ImprovedNamingStrategy

Spring Boot Application

NotesApplication is the main entry point of our Spring Boot application:

NotesApplication.java
package com.dariawan.notesapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class NotesApplication {

    public static void main(String[] args) {
        SpringApplication.run(NotesApplication.class, args);
    }
}
                    

The main() method calls Spring Boot’s SpringApplication.run() method to launch the application.

Refer to @SpringBootApplication section in Spring Boot Web Application Example for explanation about SpringBootApplication annotation. But, we see something new in above code; EnableJpaAuditing annotation:

  • EnableJpaAuditing: Annotation to enable auditing in JPA via annotation configuration.

What the benefit of this annotation, let's move to the next section - Domain Model

Create Domain Model

For table notes, we will create a Note class:

Note.java
package com.dariawan.notesapp.domain;

import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.validation.annotation.Validated;

@Validated
@Entity
@Table(name = "notes")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
public class Note implements Serializable {

    private static final long serialVersionUID = 5057388942388599423L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String title;

    @NotBlank
    @Column(length = 4000)
    private String content;

    @Column(nullable = false, updatable = false)
    @CreatedDate
    private LocalDateTime createdOn;

    @Column(nullable = false)
    @LastModifiedDate
    private LocalDateTime updatedOn;
    
    public String getShortContent() {
        return StringUtils.left(this.content, 200);
    }
}
                    

property shortContent will be used to show content in brief (max 200 characters). To use function left of StringUtils, we are using Apache commons-lang3 dependency

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency>

Refer to Create Domain Models section of Spring Boot + JPA/Hibernate + PostgreSQL RESTful CRUD API Example article to understand more about annotations used for our entities/models. Now, I'll only focusing on @EntityListeners(AuditingEntityListener.class).

  • EntityListeners: Specifies the callback listener classes to be used for an entity or mapped superclass. This annotation may be applied to an entity class or mapped superclass.
  • AuditingEntityListener: JPA entity listener to capture auditing information on persisting and updating entities. To get this one flying be sure you configure it as entity listener (example: in your orm.xml)

JPA provides the @EntityListeners annotation to specify callback listener classes.and Spring Data created its own JPA entity listener class, AuditingEntityListener to capture audit information. Using @CreatedDate and @LastModifiedDate; created date and last modified date is captured automatically by the listener when persisting (save or update) a Note.

Creating Repository to Access Data from Data Source

Next thing we will create a repository to access Note’s data from notes table (in our MySQL database).

NoteRepository.java
package com.dariawan.notesapp.repository;

import com.dariawan.notesapp.domain.Note;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface NoteRepository extends PagingAndSortingRepository<Note, Long>, 
        JpaSpecificationExecutor<Note> {
}
                    

NoteRepository extends PagingAndSortingRepository, an interface that provides generic CRUD operations and add methods to retrieve entities using the pagination and sorting abstraction.

Create Business Service

Here our NoteService:

NoteService.java
package com.dariawan.notesapp.service;

import com.dariawan.notesapp.domain.Note;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.dariawan.notesapp.repository.NoteRepository;

@Service
public class NoteService {
    
    @Autowired
    private NoteRepository noteRepository;
    
    private boolean existsById(Long id) {
        return noteRepository.existsById(id);
    }
    
    public Note findById(Long id) {
        return noteRepository.findById(id).orElse(null);
    }
    
    public List<Note> findAll(int pageNumber, int rowPerPage) {
        List<Note> notes = new ArrayList<>();
        Pageable sortedByLastUpdateDesc = PageRequest.of(pageNumber - 1, rowPerPage, 
                Sort.by("updatedOn").descending());
        noteRepository.findAll(sortedByLastUpdateDesc).forEach(notes::add);
        return notes;
    }
    
    public Note save(Note note) throws Exception {
        if (StringUtils.isEmpty(note.getTitle())) {
            throw new Exception("Title is required");
        }
        if (StringUtils.isEmpty(note.getContent())) {
            throw new Exception("Content is required");
        }
        if (note.getId() != null && existsById(note.getId())) { 
            throw new Exception("Note with id: " + note.getId() + " already exists");
        }
        return noteRepository.save(note);
    }
    
    public void update(Note note) throws Exception {
        if (StringUtils.isEmpty(note.getTitle())) {
            throw new Exception("Title is required");
        }
        if (StringUtils.isEmpty(note.getContent())) {
            throw new Exception("Content is required");
        }
        if (!existsById(note.getId())) {
            throw new Exception("Cannot find Note with id: " + note.getId());
        }
        noteRepository.save(note);
    }
    
    public void deleteById(Long id) throws Exception {
        if (!existsById(id)) { 
            throw new Exception("Cannot find Note with id: " + id);
        }
        else {
            noteRepository.deleteById(id);
        }
    }
    
    public Long count() {
        return noteRepository.count();
    }
}
                    

NoteService is the place where we place our business logic and calculations, also bridging the communication between controller and repository.

Adding Controller

Next, one of the main part of this tutorial - NoteController:

NoteController.java
package com.dariawan.notesapp.controller;

import com.dariawan.notesapp.domain.Note;
import com.dariawan.notesapp.service.NoteService;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class NoteController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final int ROW_PER_PAGE = 5;

    @Autowired
    private NoteService noteService;

    @Value("${msg.title}")
    private String title;

    @GetMapping(value = {"/", "/index"})
    public String index(Model model) { ... }

    @GetMapping(value = "/notes")
    public String getNotes(Model model,
            @RequestParam(value = "page", defaultValue = "1") int pageNumber) { ... }

    @GetMapping(value = "/notes/{noteId}")
    public String getNoteById(Model model, @PathVariable long noteId) { ... }

    @GetMapping(value = {"/notes/add"})
    public String showAddNote(Model model) { ... }

    @PostMapping(value = "/notes/add")
    public String addNote(Model model,
            @ModelAttribute("note") Note note) { ... }

    @GetMapping(value = {"/notes/{noteId}/edit"})
    public String showEditNote(Model model, @PathVariable long noteId) { ... }

    @PostMapping(value = {"/notes/{noteId}/edit"})
    public String updateNote(Model model,
            @PathVariable long noteId,
            @ModelAttribute("note") Note note) { ... }

    @GetMapping(value = {"/notes/{noteId}/delete"})
    public String showDeleteNoteById(
            Model model, @PathVariable long noteId) { ... }

    @PostMapping(value = {"/notes/{noteId}/delete"})
    public String deleteNoteById(
            Model model, @PathVariable long noteId) { ... }
}
                    

Now, we will go through our controller item by item:

Index Page

The index page or welcome page is a simple page with the title of application and link to notes page.

@Value("${msg.title}")
private String title;

@GetMapping(value = {"/", "/index"})
public String index(Model model) {
    model.addAttribute("title", title);
    return "index";
}
                    

The function returning String, which is the template name which will be used to render the response. The template that will be rendered in this function is index.ftl which is available in FreeMarker default templates location for Spring Boot in src/main/resources/templates/. The default suffix for FreeMarker in Spring Boot is .ftl

index.ftl
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>${title}</title>
        <link rel="stylesheet" type="text/css" href="/css/style.css"/>
    </head>
    <body>
        <h1>${title}</h1>
        <a href="/notes">Notes</a>  
        <br/><br/>
        <div>Copyright © dariawan.com</div>
    </body>
</html>
                    

The title attribute is extracted from property file with the @Value annotation of msg.title. Here the value in application.properties:

msg.title=Spring Boot + FreeMarker CRUD Example

Result in http://localhost:8080:

http://localhost:8080

http://localhost:8080 (Index Page)

Clicking the "Notes" link will bring us to notes page.

Notes Page

Notes page will show list of notes in paged mode, in our example is every five records (but we are not focusing on that, although codes for paging are available in our simple example)

@GetMapping(value = "/notes")
public String getNotes(Model model,
        @RequestParam(value = "page", defaultValue = "1") int pageNumber) {
    List<Note> notes = noteService.findAll(pageNumber, ROW_PER_PAGE);

    long count = noteService.count();
    boolean hasPrev = pageNumber > 1;
    boolean hasNext = (pageNumber * ROW_PER_PAGE) < count;
    model.addAttribute("notes", notes);
    model.addAttribute("hasPrev", hasPrev);
    model.addAttribute("prev", pageNumber - 1);
    model.addAttribute("hasNext", hasNext);
    model.addAttribute("next", pageNumber + 1);
    return "note-list";
}
                    

The controller then will render note-list.ftl

note-list.ftl
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Note List</title>
        <link rel="stylesheet" type="text/css" href="/css/style.css"/>
    </head>
    <body>
        <h1>Note List</h1>
        
        <div>
            <nobr>
                <a href="/notes/add">Add Note</a> |
                <a href="/">Back to Index</a>
            </nobr>
        </div>
        <br/><br/>
        <div>
            <table border="1">
                <tr>
                    <th>Id</th>
                    <th>Title</th>
                    <th>Content</th>
                    <th>Created On</th>
                    <th>Updated On</th>
                    <th>Edit</th>                    
                </tr>
                <#list notes as note>
                    <tr>
                        <td><a href="${'notes/' + note.id}">${note.id}</a></td>
                        <td><a href="${'notes/' + note.id}">${note.title}</a></td>
                        <td>${note.shortContent}</td>
                        <td>${(note.createdOn).format('yyyy-MM-dd HH:mm:ss')}</td>
                        <td>${(note.updatedOn).format('yyyy-MM-dd HH:mm:ss')}</td>
                        <td><a href="${'notes/' + note.id + '/edit'}">Edit</a></td>
                    </tr>
                </#list>
            </table>          
        </div>
        <br/><br/>
        <div>
            <nobr>
                <#if hasPrev><a href="${'notes?page=' + prev}">Prev</a>&nbsp;&nbsp;&nbsp;</#if>
                <#if hasNext><a href="${'notes?page=' + next}">Next</a></#if>
            </nobr>
        </div>
    </body>
</html>
                    

Result of http://localhost:8080/notes:

http://localhost:8080/notes

http://localhost:8080/notes (Notes Page)

Clicking the "id" and "title" link will lead us to Note Page, and clicking the "edit" link will lead to Edit Note Page. Add new Note Page is available by clicking "Add Note" link.

Note Page

Note Page used to show note in read-only mode. From this page, user can decide to "Edit" or "Delete" note.

@GetMapping(value = "/notes/{noteId}")
public String getNoteById(Model model, @PathVariable long noteId) {
    Note note = null;
    try {
        note = noteService.findById(noteId);
        model.addAttribute("allowDelete", false);
    } catch (Exception ex) {
        model.addAttribute("errorMessage", ex.getMessage());
    }
    model.addAttribute("note", note);        
    return "note";
}
                    

The controller then will render note.ftl:

note.ftl
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>View Note</title>
        <link rel="stylesheet" type="text/css" href="/css/style.css"/>
    </head>
    <body>
        <h1>View Note</h1>
        <a href="/notes">Back to Note List</a>
        <br/><br/>
        <#if note??>
            <table border="0">
                <tr>
                    <td>ID</td>
                    <td>:</td>
                    <td>${note.id}</td>          
                </tr>
                <tr>
                    <td>Title</td>
                    <td>:</td>
                    <td>${note.title}</td>             
                </tr>
                <tr>
                    <td>Content</td>
                    <td>:</td>
                    <td>${note.content}</td>              
                </tr>
                <tr>
                    <td>Created On</td>
                    <td>:</td>
                    <td>${(note.createdOn).format('yyyy-MM-dd HH:mm:ss')}</td>              
                </tr>
                <tr>
                    <td>Updated On</td>
                    <td>:</td>
                    <td>${(note.updatedOn).format('yyyy-MM-dd HH:mm:ss')}</td>              
                </tr>
            </table>
            <br/><br/>
            <#if allowDelete>
                <form action="${'/notes/' + note.id + '/delete'}" method="POST">
                    Delete this note? <input type="submit" value="Yes" />
                </form>
            <#else>
                <div>
                    <a href="${'/notes/' + note.id + '/edit'}">Edit</a> |
                    <a href="${'/notes/' + note.id + '/delete'}">Delete</a>
                </div>
            </#if>
        </#if>
        <#if errorMessage?has_content>
            <div class="error">${errorMessage}</div>
        </#if>
    </body>
</html>
                    

http://localhost:8080/notes/1 rendered in browser:

http://localhost:8080/notes/1

http://localhost:8080/notes/1 (Note Page)

If user choose Edit, it will go to Edit Note Page. If user choose Delete, it will lead to Delete Note Page.

Edit and Add Note Page

For Add and Edit Note Page, we will use following flow:

  1. GET request to show/render the page, represented by functions showAddNote(...) and showEditNote(...)
  2. POST request to save the note data to the server, represented by functions addNote(...) and updateNote(...)
@GetMapping(value = {"/notes/add"})
public String showAddNote(Model model) {
    Note note = new Note();
    model.addAttribute("add", true);
    model.addAttribute("note", note);

    return "note-edit";
}

@PostMapping(value = "/notes/add")
public String addNote(Model model,
        @ModelAttribute("note") Note note) {        
    try {
        Note newNote = noteService.save(note);
        return "redirect:/notes/" + String.valueOf(newNote.getId());
    } catch (Exception ex) {
        // log exception first, 
        // then show error
        String errorMessage = ex.getMessage();            
        logger.error(errorMessage);
        model.addAttribute("errorMessage", errorMessage);

        model.addAttribute("add", true);
        return "note-edit";
    }        
}

@GetMapping(value = {"/notes/{noteId}/edit"})
public String showEditNote(Model model, @PathVariable long noteId) {
    Note note = null;
    try {
        note = noteService.findById(noteId);
    } catch (Exception ex) {
        model.addAttribute("errorMessage", ex.getMessage());
    }
    model.addAttribute("add", false);
    model.addAttribute("note", note);
    return "note-edit";
}

@PostMapping(value = {"/notes/{noteId}/edit"})
public String updateNote(Model model,
        @PathVariable long noteId,
        @ModelAttribute("note") Note note) {
    try {
        note.setId(noteId);
        noteService.update(note);
        return "redirect:/notes/" + String.valueOf(note.getId());
    } catch (Exception ex) {
        // log exception first, 
        // then show error
        String errorMessage = ex.getMessage();            
        logger.error(errorMessage);
        model.addAttribute("errorMessage", errorMessage);

        model.addAttribute("add", false);
        return "note-edit";
    }
}
                    

For GET request, both functions will render note-edit.ftl:

note-edit.ftl
<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8" />
        <title><#if add>Create a Note<#else>Edit a Note</#if></title>
        <link rel="stylesheet" type="text/css" href="/css/style.css"/>
    </head>
    <body>
        <h1><#if add>Create a Note:<#else>Edit a Note:</#if></h1>
        <a href="/notes">Back to Note List</a>
        <br/><br/>
        <#if add>
            <#assign urlAction>/notes/add</#assign>
            <#assign submitTitle>Create</#assign>
        <#else>
            <#assign urlAction>${'/notes/' + note.id + '/edit'}</#assign>
            <#assign submitTitle>Update</#assign>
        </#if>
        <form action="${urlAction}" name="note" method="POST">
            <table border="0">
                <#if note.id??>
                <tr>
                    <td>ID</td>
                    <td>:</td>
                    <td>${note.id}</td>             
                </tr>
                </#if>
                <tr>
                    <td>Title</td>
                    <td>:</td>
                    <td><input type="text" name="title" value="${(note.title)!''}" /></td>              
                </tr>
                <tr>
                    <td>Content</td>
                    <td>:</td>
                    <td><textarea name="content" rows="4" cols="50">${(note.content)!""}</textarea></td>
                </tr>
                <#if note.createdOn??>
                <tr>
                    <td>Created On</td>
                    <td>:</td>
                    <td>${(note.createdOn).format('yyyy-MM-dd HH:mm:ss')}</td>              
                </tr>
                <tr>
                    <td>Updated On</td>
                    <td>:</td>
                    <td>${(note.updatedOn).format('yyyy-MM-dd HH:mm:ss')}</td>              
                </tr>
                </#if>
            </table>
            <input type="submit" value="${submitTitle}" />
        </form>

        <br/>
        <!-- Check if errorMessage is not null and not empty -->       
        <#if errorMessage?has_content>
            <div class="error">${errorMessage}</div>
        </#if>       
    </body>
</html>
                    

Attribute add is used to control if the page is in "add mode" or "edit mode". Below is screenshot of Add Note Page that available in http://localhost:8080/notes/add :

http://localhost:8080/notes/add

http://localhost:8080/notes/add (Add Note Page)

Upon successful add, the controller will redirect to Note Page to view new created note.

Following screenshot is the example of Edit Note Page (for Note with id=1) which is available in http://localhost:8080/notes/1/edit:

http://localhost:8080/notes/1/edit

http://localhost:8080/notes/1/edit (Edit Note Page)

Upon successful update, the controller also will redirect to Note Page to view updated note.

Delete Note Page

Delete Note Page using same scenario as Add/Edit Note Page:

  1. GET request to show/render the page, represented by functions showDeleteNoteById(...) to confirm deletion
  2. POST request to delete note from the server, represented by functions deleteNoteById(...)
@GetMapping(value = {"/notes/{noteId}/delete"})
public String showDeleteNoteById(
        Model model, @PathVariable long noteId) {
    Note note = null;
    try {
        note = noteService.findById(noteId);
    } catch (Exception ex) {
        model.addAttribute("errorMessage", ex.getMessage());
    }
    model.addAttribute("allowDelete", true);
    model.addAttribute("note", note);
    return "note";
}

@PostMapping(value = {"/notes/{noteId}/delete"})
public String deleteNoteById(
        Model model, @PathVariable long noteId) {
    try {
        noteService.deleteById(noteId);
        return "redirect:/notes";
    } catch (Exception ex) {
        String errorMessage = ex.getMessage();
        logger.error(errorMessage);
        model.addAttribute("errorMessage", errorMessage);
        return "note";
    }
}
                    

Confirm deletion in http://localhost:8080/notes/1/delete:

http://localhost:8080/notes/1/delete

http://localhost:8080/notes/1/delete (Delete Note Page)

Static Files

Static resources default folder is in \src\main\resources\static\. The css used for this example is available in css\style.css

style.css
h1, h2 {
    color:#ff4f57;
}

a {
    color: #ff4f57;
}

table {
    border-collapse: collapse;
}
 
table th, table td {
    padding: 5px;
}

.error {
    color: red;
    font-weight: bold;
}
                    

Test Files

There are two test files for our project:

NotesApplicationTests
package com.dariawan.notesapp;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class NotesApplicationTests {

    @Test
    public void contextLoads() {
    }
}
                    

NoteServiceJPATest.java
package com.dariawan.notesapp.service;

import com.dariawan.notesapp.domain.Note;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;
import javax.sql.DataSource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class NoteServiceJPATest {

    @Autowired 
    private DataSource dataSource;
    
    @Autowired 
    private NoteService noteService;
    
    @Before
    public void cleanTestData() throws Exception {
        try (Connection conn = dataSource.getConnection()) {
            String sql = "delete from notes where title <> ?";
            PreparedStatement ps = conn.prepareStatement(sql);
            ps.setString(1, "Hello World!");
            ps.executeUpdate();
        }
    }
    
    
    @Test
    public void testFindAllNotes() {
        List<Note> notes = noteService.findAll(1, 20);
        assertNotNull(notes);
        assertTrue(notes.size() == 1);
        for (Note user : notes) {
            assertNotNull(user.getId());
            assertNotNull(user.getTitle());
            assertNotNull(user.getContent());
        }
    }
    
    @Test
    public void testSaveUpdateDeleteNote() throws Exception{
        Note n = new Note();
        n.setTitle("Spring Boot + FreeMarker");
        n.setContent("Spring Boot with FreeMarker Tutorial");
        
        noteService.save(n);
        assertNotNull(n.getId());
        
        Note findNote = noteService.findById(n.getId());
        assertEquals(n.getTitle(), findNote.getTitle());
        assertEquals(n.getContent(), findNote.getContent());
        
        // update record
        n.setTitle("Spring Boot + FreeMarker Example");
        noteService.update(n);
        
        // test after update
        findNote = noteService.findById(n.getId());
        assertEquals(n.getTitle(), findNote.getTitle());
        
        // test delete
        noteService.deleteById(n.getId());
        
        // query after delete
        Note nDel = noteService.findById(n.getId());
        assertNull(nDel);
    }
}
                    

Final Project Structure

At the end, our project structure will be similar like this:

spring-boot-freemarker-example │ .gitignore │ HELP.md │ mvnw │ mvnw.cmd │ pom.xml │ ├───.mvn │ └───wrapper │ maven-wrapper.jar │ maven-wrapper.properties │ MavenWrapperDownloader.java │ └───src ├───main │ ├───java │ │ └───com │ │ └───dariawan │ │ └───notesapp │ │ │ NotesApplication.java │ │ │ │ │ ├───config │ │ │ FreemarkerConfig.java │ │ │ │ │ ├───controller │ │ │ NoteController.java │ │ │ │ │ ├───domain │ │ │ Note.java │ │ │ │ │ ├───repository │ │ │ NoteRepository.java │ │ │ │ │ └───service │ │ NoteService.java │ │ │ └───resources │ │ application.properties │ │ │ ├───static │ │ └───css │ │ style.css │ │ │ └───templates │ index.ftl │ note-edit.ftl │ note-list.ftl │ note.ftl │ ├───sql │ notes.sql │ └───test └───java └───com └───dariawan └───notesapp │ NotesApplicationTests.java │ └───service NoteServiceJPATest.java

What Next?

From the structure above we see one file that we not checked yet, FreemarkerConfig.java. What this file for? Check my next article!