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:
<?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:
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:
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).
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
:
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
:
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
<!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 (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
<!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> </#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 (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
:
<!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 (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:
GET
request to show/render the page, represented by functionsshowAddNote(...)
andshowEditNote(...)
POST
request to save the note data to the server, represented by functionsaddNote(...)
andupdateNote(...)
@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
:
<!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 (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 (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:
GET
request to show/render the page, represented by functionsshowDeleteNoteById(...)
to confirm deletionPOST
request to delete note from the server, represented by functionsdeleteNoteById(...)
@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 (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
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:
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() {
}
}
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!