Thymeleaf Spring Boot
Thymeleaf Spring Boot
Thymeleaf
with
Spring Boot
by Michael Good
Preface
Thank you to everyone that participates in the Thymeleaf and
Spring projects. Both technologies are such a pleasure to use
and it is amazing that they are open source.
Lastly, thank you for reading this short book and I hope it is help-
ful. If you have any questions, please contact me at
[email protected]
ii
Getting Started
1
Here we will be reviewing
the basics of getting
Thymeleaf set up
appropriately with a Spring
Boot project.
Section 1
And last but not least, Thymeleaf has been designed from the
beginning with XML and Web standards in mind, allowing you
to create fully validating templates if that is a need for you.
4
" •" Velocity
How does Thymeleaf work with
Spring? A Brief Overview of Thymeleaf VS JSP
The Model-View-Controller (MVC) software design pattern is a
method for separating concerns within a software application. In
principal, the application logic, or controller, is separated from •" Thymeleaf looks more HTML-ish than the JSP version –
the technology used to display information to the user, or the no strange tags, just some meaningful attributes.
view layer. The model is a communications vehicle between the •" Variable expressions (${...}) are Spring EL and exe-
controller and view layers.
cute on model attributes, asterisk expressions (*{...}) exe-
Within an application, the view layer may use one or more differ- cute on the form backing bean, hash expressions (#{...})
ent technologies to render the view. Spring web-based applica- are for internationalization and link expressions (@{...}) re-
tions support a variety of view options, often referred to as view write URLs. (
templates. These technologies are described as "templates" be-
• We are allowed to have prototype code in Thymeleaf.
cause they provide a markup language to expose model attrib-
utes within the view during server-side rendering. • For changing styles when developing, working with JSP takes
more complexity, effort and time than Thymeleaf because you
The following view template libraries, among others, are com-
have to deploy and start the whole application every time you
patible with Spring:
change CSS. Think of how this difference would be even more
" •" JSP/JSTL noticeable if our development server was not local but remote,
changes didn’t involve only CSS but also adding and/or remov-
" •" Thymeleaf ing some HTML code, and we still hadn’t implemented the re-
" •" Tiles quired logic in our application to reach our desired page. This
last point is especially important. What if our application was
" •" Freemarker still being developed, the Java logic needed to show this or
other previous pages wasn’t working, and we had to new
5
styles to our customer? Or perhaps the customer wanted us
to show new styles on-the-go?
6
Section 2
application.properti Overview
Spring Boot applies it’s typical convention over configuration ap-
7
spring.thymeleaf.excluded-view-names= # Comma-
separated list of view names that should be excluded
from resolution.
spring.thymeleaf.mode=HTML5 # Template mode to be ap-
plied to templates. See also StandardTemplateModeHan-
dlers.
spring.thymeleaf.prefix=classpath:/templates/ # Pre-
fix that gets prepended to view names when building a
URL.
spring.thymeleaf.suffix=.html # Suffix that gets ap-
pended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of
the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list
of view names that can be resolved.
8
Basic Content
2
Here we cover how to
connect a controller to a
Thymeleaf template and the
basic properties used in a
template.
Section 1
@GetMapping("helloworldM")
public String helloworldM(Model model){
// add some attributes using model
Topics
return "helloM";
}
1. Model
@GetMapping("helloworldMV")
public ModelAndView helloWorldMV(){
ModelAndView modelAndView = new ModelAnd-
View("helloMV");
return modelAndView;
10
private String name;
3. Thymeleaf // basic getters and setters
@GetMapping("helloworldM")
</body> public String helloworldM(Model model){
</html> // add neptune here to demonstrate adding it
as attribute
Planet Neptune = new Planet();
Neptune.setName("neptune");
planetDAO.save(Neptune);
4. Adding Attributes to Model model.addAttribute("neptune", Neptune);
favoritePlanets.add(planetDAO.findByname("earth"));
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
favoritePlanets.add(planetDAO.findByname("mars"));
private Long id;
11
model.addAttribute("favoritePlanets", favor- <tr>
itePlanets); <th>No.</th>
<th>planet</th>
13
Section 2
th:text
The th:text attribute evaluates its value expression and sets
the result of this evaluation as the body of the tag it is in, substi-
We Are Reviewing: tuting the current content between the tags with what the ex-
pression evaluates to.
1. xmlns:th th:each
2. th:text th:each is used for iteration. These objects that are consid-
ered iterable by a th:each attribute
3. th:each
4. th:unless • java.util.List objects
• Any object implementing java.util.Iterable
5. th:if • Any object implementing java.util.Map. When iterating
maps, iter variables will be of
class java.util.Map.Entry.
• Any array
• Any other object will be treated as if it were a single-
valued list containing the object itself.
th:if
This is used if you need a fragment of your template only to ap-
pear in the result if a certain condition is met.
14
Note that the th:if attribute will not only evaluate boolean con-
ditions. It will evaluate the specified expression as true follow-
ing these rules:
th:unless
This is the negative counterpart of th:if .
15
Forms
3
Here we cover how forms
work in Thymeleaf and the
typical elements of a form
like radio buttons and
checkboxes.
Let’s see now how to add an input to our form:
Command Object
Command object is the name Spring MVC gives to form- <input type="text" th:field="*{dateAcquired}" />
backing beans, this is, to objects that model a form’s fields and As you can see, we are introducing a new attribute
provide getter and setter methods that will be used by the frame- here: th:field. This is a very important feature for Spring
work for establishing and obtaining the values input by the user MVC integration because it does all the heavy work of binding
at the browser side. your input with a property in the form-backing bean. You can
see it as an equivalent of the path attribute in a tag from Spring
Thymeleaf requires you to specify the command object by using MVC’s JSP tag library.
a th:object attribute in your <form> tag:
The th:field attribute behaves differently depending on
<form action="#" th:action="@{/storemanager}" whether it is attached to an <input>, <select> or <tex-
th:object="${storeGuide}" method="post">
tarea> tag (and also depending on the specific type of <in-
...
</form> put> tag). In this case (input[type=text]), the above line
This is consistent with other uses of th:object, but in fact of code is similar to:
this specific scenario adds some limitations in order to correctly <input type="text" id="dateAcquired" name="dateA-
integrate with Spring MVC’s infrastructure: cquired" th:value="*{dateAcquired}" />
…but in fact it is a little bit more than that,
• Values for th:object attributes in form tags must be vari- because th:field will also apply the registered Spring Con-
able expressions (${...}) specifying only the name of a version Service, including the DateFormatter we saw before
model attribute, without property navigation. This means (even if the field expression is not double-bracketed). Thanks to
that an expression like ${storeGuide} is valid, this, the date will be shown correctly formatted.
but ${storeGuide.data} would not be.
• Once inside the <form> tag, no Values for th:field attributes must be selection expressions
other th:object attribute can be specified. This is consis- (*{...}), which makes sense given the fact that they will be
tent with the fact that HTML forms cannot be nested. evaluated on the form-backing bean and not on the context vari-
ables (or model attributes in Spring MVC jargon).
Checkbox Fields Note that we’ve added a th:value attribute this time, because
the features field is not a boolean like covered was, but instead
th:field also allows us to define checkbox inputs. Let’s see
is an array of values.
an example from our HTML page:
<div> Let’s see the HTML output generated by this code:
<label th:for="${#ids.next('covered')}"
th:text="#{storeGuide.covered}">Covered</label> <ul>
<input type="checkbox" th:field="*{covered}" /> <li>
</div> <input id="features1" name="features" type="chec-
kbox" value="features-one" />
Note there’s some fine stuff here besides the checkbox itself, <input name="_features" type="hidden" value="on"
like an externalized label and also the use of />
the #ids.next('covered') function for obtaining the value <label for="features1">Features 1</label>
that will be applied to the id attribute of the checkbox input. </li>
<li>
Why do we need this dynamic generation of an id attribute for <input id="features2" name="features" type="chec-
kbox" value="features-two" />
this field? Because checkboxes are potentially multi-valued, <input name="_features" type="hidden" value="on"
and thus their id values will always be suffixed a sequence num- />
ber (by internally using the #ids.seq(...)function) in order <label for="features2">Features 2</label>
to ensure that each of the checkbox inputs for the same prop- </li>
erty has a different id value. <li>
<input id="features3" name="features" type="chec-
kbox" value="features-three" />
We can see this more easily if we look at such a multi-valued <input name="_features" type="hidden" value="on"
checkbox field: />
<label for="features3">Features 3</label>
<ul> </li>
<li th:each="feat : ${allFeatures}"> </ul>
18
We can see here how a sequence suffix is added to each in- the <select> tag has to include a th:field attribute, but
put’s id attribute, and how the #ids.prev(...) function al- the th:value attributes in the nested <option> tags will be
lows us to retrieve the last sequence value generated for a spe- very important because they will provide the means of knowing
cific input id. which is the currently selected option (in a similar way to non-
boolean checkboxes and radio buttons).
Don’t worry about those hidden inputs with name="_fe-
atures": they are automatically added in order to avoid prob- Let’s re-build the type field as a dropdown select:
lems with browsers not sending unchecked checkbox values to
the server upon form submission.
<select th:field="*{type}">
Also note that if our features property contained some selected <option th:each="type : ${allTypes}"
values in our form-backing bean, th:field would have taken th:value="${type}"
care of that and would have added a checked="checked" a- th:text="#{${'storeGuide.type.' +
ttribute to the corresponding input tags. type}}">Wireframe</option>
</select>
Radio Buttons At this point, understanding this piece of code is quite easy.
Radio button fields are specified in a similar way to non- Just notice how attribute precedence allows us to set
boolean (multi-valued) checkboxes —except that they are not the th:each attribute in the <option> tag itself.
multivalued, of course:
<ul>
<li th:each="ty : ${allTypes}">
<input type="radio" th:field="*{type}"
th:value="${ty}" />
<label th:for="${#ids.prev('type')}"
th:text="#{${'storeGuide.type.' + ty}}">Wireframe</
label>
</li>
</ul>
Dropdown/List Selectors
Select fields have two parts: the <select> tag and its
nested <option> tags. When creating this kind of field, only
19
Fragments
4
Learn how to use
fragments, reusable pieces
of code.
We will often want to include in our templates fragments from <div th:include="footer :: copy"></div>
other templates. Common uses for this are footers, headers, </body>
menus…
The syntax for both these inclusion attributes is quite straightfor-
In order to do this, Thymeleaf needs us to define the fragments ward. There are three different formats:
available for inclusion, which we can do by using
the th:fragment attribute. • "templatename::domselector" or the
equivalent templatename::[domselector] Includes
Now let’s say we want to add a standard copyright footer to all the fragment resulting from executing the specified DOM
our grocery pages, and for that we define Selector on the template named templatename.
a /WEB-INF/templates/footer.html file containing this ◦ Note that domselector can be a mere fragment
code: name, so you could specify something as simple
<!DOCTYPE html SYSTEM as templatename::fragmentname like in
"http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.d the footer :: copy above.
td"> DOM Selector syntax is similar to XPath expressions and CSS
<html xmlns="http://www.w3.org/1999/xhtml"
selectors, see the Appendix C for more info on this syntax.
xmlns:th="http://www.thymeleaf.org">
• "templatename" Includes the complete template
<body> named templatename.
Note that the template name you use
<div th:fragment="copy">
© 2018 Some fictional company in th:include/th:replace tags will have to be resolvable
</div> by the Template Resolver currently being used by the Template
Engine.
</body>
</html> • ::domselector" or "this::domselector" Includes
a fragment from the same template.
The code above defines a fragment called copy that we can Both templatename and domselector in the above exam-
easily include in our home page using one of ples can be fully-featured expressions (even conditionals!) like:
the th:include or th:replace attributes:
<body> <div th:include="footer :: (${user.isAdmin}?
#{footer.admin} : #{footer.normaluser})"></div>
...
21
Fragments can include any th:* attributes. These attrib-
utes will be evaluated once the fragment is included into the tar-
get template (the one with
the th:include/th:replace attribute), and they will be able
to reference any context variables defined in this target tem-
plate.
22
URLs
5
Here we cover the basics of
URLs in Thymeleaf.
Absolute URLs
Completely written URLs like http://www.thymeleaf.org <!-- Will produce
'http://localhost:8080/some/order/details?orderId=3'
(plus rewriting) -->
<a href="details.html"
Page-Relative URLs
th:href="@{http://localhost:8080/some/order/details(o
rderId=${o.id})}">view</a>
Page-relative, like shop/login.html
<!-- Will produce '/some/order/details?orderId=3'
(plus rewriting) -->
<a href="details.html"
Context-Relative URLs th:href="@{/order/details(orderId=${o.id})}">view</a>
URL based on the current context,a URL would be like / <!-- Will produce '/some/order/3/details' (plus re-
writing) -->
item?id=5 (context name in server will be added automati- <a href="details.html"
cally) th:href="@{/order/{orderId}/details(orderId=${o.id})}
">view</a>
Server-Relatve URLs
Relative to server’s address, a URL would be like ~/account/
viewInvoice (allows calling URLs in another context (= appli-
cation) in the same server.
th:href
So, th:href is used for URLs and it is an attribute modifier at-
tribute: once processed, it will compute the link URL to be used
and set the href attribute of the <a> tag to this URL.
24
PagingAndSorting
Example
6
For this tutorial, I will
demonstrate how to
display a list of a business’
clients in Thymeleaf with
pagination.
For this tutorial, I will demonstrate how to display a list of a
business’ clients in Thymeleaf with pagination. <properties>
<project.build.sourceEncoding>UTF-8</project.buil
d.sourceEncoding>
View and Download the code from Github
<project.reporting.outputEncoding>UTF-8</project.
reporting.outputEncoding>
<java.version>1.8</java.version>
1 – Project Dependencies </properties>
<groupId>com.michaelcgood</groupId> <dependency>
<artifactId>michaelcgood-pagingandsorting</ <groupId>org.springframework.boot</groupId>
artifactId> <artifactId>spring-boot-starter-test</
<version>0.0.1</version> artifactId>
<packaging>jar</packaging> <scope>test</scope>
</dependency>
<name>PagingAndSortingRepositoryExample</name> <dependency>
<description>Michael C Good - <groupId>org.hsqldb</groupId>
PagingAndSortingRepository</description> <artifactId>hsqldb</artifactId>
<scope>runtime</scope>
<parent> </dependency>
<groupId>org.springframework.boot</groupId> <dependency>
<artifactId>spring-boot-starter-parent</ <groupId>org.springframework.boot</groupId>
artifactId> <artifactId>spring-boot-starter-data-jpa</art
<version>1.5.6.RELEASE</version> ifactId>
<relativePath /> <!-- lookup parent from reposi- </dependency>
tory --> <dependency>
</parent> <groupId>org.springframework.boot</groupId>
26
artifactId>
<artifactId>spring-boot-starter-web</
2 – Project Dependencies
</dependency>
</dependencies> Besides the normal Spring dependencies, we add Thymeleaf
and hsqldb because we are using an embedded database.
<build>
<plugins>
<plugin>
<?xml version="1.0" encoding="UTF-8"?>
<groupId>org.springframework.boot</groupI
<project xmlns="http://maven.apache.org/POM/4.0.0"
d>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<artifactId>spring-boot-maven-plugin</art
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
ifactId>
http://maven.apache.org/xsd/maven-4.0.0.xsd">
</plugin>
<modelVersion>4.0.0</modelVersion>
</plugins>
</build>
<groupId>com.michaelcgood</groupId>
<artifactId>michaelcgood-pagingandsorting</
artifactId>
</project>
<version>0.0.1</version>
<packaging>jar</packaging>
<name>PagingAndSortingRepositoryExample</name>
<description>Michael C Good -
For this tutorial, I will demonstrate how to display a list of a PagingAndSortingRepository</description>
business’ clients in Thymeleaf with pagination.
<parent>
View and Download the code from Github <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</
artifactId>
1 – Project Structure <version>1.5.6.RELEASE</version>
<relativePath /> <!-- lookup parent from reposi-
We have a normal Maven project structure.
tory -->
</parent>
<properties>
27
<project.build.sourceEncoding>UTF-8</project.buil </dependency>
d.sourceEncoding> </dependencies>
<project.reporting.outputEncoding>UTF-8</project.
reporting.outputEncoding> <build>
<java.version>1.8</java.version> <plugins>
</properties> <plugin>
<groupId>org.springframework.boot</groupI
<dependencies> d>
<dependency> <artifactId>spring-boot-maven-plugin</art
<groupId>org.springframework.boot</groupId> ifactId>
<artifactId>spring-boot-starter-thymeleaf</ar </plugin>
tifactId> </plugins>
</dependency> </build>
<dependency>
<groupId>org.springframework.boot</groupId> </project>
artifactId>
<artifactId>spring-boot-starter-test</
3 – Models
<scope>test</scope>
</dependency> We define the following fields for a client:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
• a unique identifier
<scope>runtime</scope> • name of the client
</dependency> • an address of the client
<dependency> • the amount owed on the current invoice
<groupId>org.springframework.boot</groupId> The getters and setters are quickly generated in Spring Tool
<artifactId>spring-boot-starter-data-jpa</art Suite.
ifactId> The @Entity annotation is needed for registering this model to
</dependency> @SpringBootApplication.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</ ClientModel.java
artifactId>
28
package com.michaelcgood.model; public void setCurrentInvoice(Integer currentInvoice)
{
import javax.persistence.Entity; this.currentInvoice = currentInvoice;
import javax.persistence.GeneratedValue; }
import javax.persistence.GenerationType; private String name;
import javax.persistence.Id; private String address;
private Integer currentInvoice;
@Entity
public class ClientModel { }
@Id
@GeneratedValue(strategy = GenerationType.AUTO) The PagerModel is just a POJO (Plain Old Java Object), unlike
private Long id; the ClientModel. There are no imports, hence no annotations.
public Long getId() { This PagerModel is purely just used for helping with the pagina-
return id; tion on our webpage. Revisit this model once you read the Thy-
} meleaf template and see the demo pictures. The PagerModel
public void setId(Long id) {
makes more sense when you think about it in context.
this.id = id;
}
public String getName() {
PagerModel.java
return name;
} package com.michaelcgood.model;
public void setName(String name) {
this.name = name; public class PagerModel {
} private int buttonsToShow = 5;
public String getAddress() {
return address; private int startPage;
}
public void setAddress(String address) { private int endPage;
this.address = address;
} public PagerModel(int totalPages, int currentPage,
public Integer getCurrentInvoice() { int buttonsToShow) {
return currentInvoice;
} setButtonsToShow(buttonsToShow);
29
int halfPagesToShow = getButtonsToShow() / 2; this.buttonsToShow = buttonsToShow;
} else {
if (totalPages <= getButtonsToShow()) { throw new IllegalArgumentException("Must be
setStartPage(1); an odd value!");
setEndPage(totalPages); }
}
} else if (currentPage - halfPagesToShow <= 0) {
setStartPage(1); public int getStartPage() {
setEndPage(getButtonsToShow()); return startPage;
}
} else if (currentPage + halfPagesToShow == total-
Pages) { public void setStartPage(int startPage) {
setStartPage(currentPage - halfPagesToShow); this.startPage = startPage;
setEndPage(totalPages); }
30
The PagingAndSortingRepository is an extension of the CrudRe- With the addtorepository method(), we add several “clients” to
pository. The only difference is that it allows you to do pagina- our repository, and many of them are hat companies because I
tion of entities. Notice that we annotate the interface with @Re- ran out of ideas.
pository to make it visible to @SpringBootApplication.
ModelAndView is used here rather than Model. ModelAndView
ClientRepository.java is used instead because it is a container for both a ModelMap
and a view object. It allows the controller to return both as a sin-
package com.michaelcgood.dao; gle value. This is desired for what we are doing.
import
org.springframework.data.repository.PagingAndSortingRepos ClientController.java
itory;
import org.springframework.stereotype.Repository; package com.michaelcgood.controller;
We add some example values to our repository with the addtore- @Controller
pository() method, which is defined further below in this class. public class ClientController {
31
int evalPage = (page.orElse(0) < 1) ? INITI-
private static final int BUTTONS_TO_SHOW = 3; AL_PAGE : page.get() - 1;
private static final int INITIAL_PAGE = 0; // print repo
private static final int INITIAL_PAGE_SIZE = 5; System.out.println("here is client repo " +
private static final int[] PAGE_SIZES = { 5, 10}; clientrepository.findAll());
@Autowired Page<ClientModel> clientlist =
ClientRepository clientrepository; clientrepository.findAll(new PageRequest(evalPage, eval-
PageSize));
@GetMapping("/") System.out.println("client list get total pages"
public ModelAndView homepage(@RequestParam("page- + clientlist.getTotalPages() + "client list get number "
Size") Optional<Integer> pageSize, + clientlist.getNumber());
@RequestParam("page") Optional<Integer> PagerModel pager = new
page){ PagerModel(clientlist.getTotalPages(),clientlist.getNumbe
r(),BUTTONS_TO_SHOW);
if(clientrepository.count()!=0){ // add clientmodel
;//pass modelAndView.addObject("clientlist",clientlist);
}else{ // evaluate page size
addtorepository(); modelAndView.addObject("selectedPageSize", eval-
} PageSize);
// add page sizes
ModelAndView modelAndView = new ModelAndView("in- modelAndView.addObject("pageSizes", PAGE_SIZES);
dex"); // add pager
// modelAndView.addObject("pager", pager);
// Evaluate page size. If requested parameter is return modelAndView;
null, return initial
// page size }
int evalPageSize =
pageSize.orElse(INITIAL_PAGE_SIZE); public void addtorepository(){
// Evaluate page. If requested parameter is null
or less than 0 (to //below we are adding clients to our repository
// prevent exception), return initial size. Other- for the sake of this example
wise, return value of ClientModel widget = new ClientModel();
// param. decreased by 1. widget.setAddress("123 Fake Street");
widget.setCurrentInvoice(10000);
32
widget.setName("Widget Inc"); ClientModel hat = new ClientModel();
hat.setAddress("444 Hat Drive");
clientrepository.save(widget); hat.setCurrentInvoice(60000);
hat.setName("The Hat Shop");
//next client clientrepository.save(hat);
ClientModel foo = new ClientModel();
foo.setAddress("456 Attorney Drive"); //next client
foo.setCurrentInvoice(20000); ClientModel hatB = new ClientModel();
foo.setName("Foo LLP"); hatB.setAddress("445 Hat Drive");
hatB.setCurrentInvoice(60000);
clientrepository.save(foo); hatB.setName("The Hat Shop B");
clientrepository.save(hatB);
//next client
ClientModel bar = new ClientModel(); //next client
bar.setAddress("111 Bar Street"); ClientModel hatC = new ClientModel();
bar.setCurrentInvoice(30000); hatC.setAddress("446 Hat Drive");
bar.setName("Bar and Food"); hatC.setCurrentInvoice(60000);
clientrepository.save(bar); hatC.setName("The Hat Shop C");
clientrepository.save(hatC);
//next client
ClientModel dog = new ClientModel(); //next client
dog.setAddress("222 Dog Drive"); ClientModel hatD = new ClientModel();
dog.setCurrentInvoice(40000); hatD.setAddress("446 Hat Drive");
dog.setName("Dog Food and Accessories"); hatD.setCurrentInvoice(60000);
clientrepository.save(dog); hatD.setName("The Hat Shop D");
clientrepository.save(hatD);
//next client
ClientModel cat = new ClientModel(); //next client
cat.setAddress("333 Cat Court"); ClientModel hatE = new ClientModel();
cat.setCurrentInvoice(50000); hatE.setAddress("447 Hat Drive");
cat.setName("Cat Food"); hatE.setCurrentInvoice(60000);
clientrepository.save(cat); hatE.setName("The Hat Shop E");
clientrepository.save(hatE);
//next client
33
//next client The changePageAndSize() function is the JavaScript function
ClientModel hatF = new ClientModel(); that will update the page size when the user changes it.
hatF.setAddress("448 Hat Drive");
hatF.setCurrentInvoice(60000);
hatF.setName("The Hat Shop F"); <html xmlns="http://www.w3.org/1999/xhtml"
clientrepository.save(hatF); xmlns:th="http://www.thymeleaf.org">
} <head>
<!-- CSS INCLUDE -->
} <link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7
/css/bootstrap.min.css"
6 – Thymeleaf Template integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7
on3RYdg4Va+PmSTsz/K68vbdEjh4u"
In Thymeleaf template, the two most important things to note crossorigin="anonymous"></link>
are:
<!-- EOF CSS INCLUDE -->
<style>
• Thymeleaf Standard Dialect .pagination-centered {
• Javascript text-align: center;
Like in a CrudRepository, we iterate through the PagingAnd- }
SortingRepository with th:each=”clientlist : ${clientlist}”. Ex-
cept instead of each item in the repository being an Iterable, the .disabled {
pointer-events: none;
item is a Page.
opacity: 0.5;
}
With select class=”form-control pagination” id=”pageSizeS-
elect”, we are allowing the user to pick their page size of either 5 .pointer-disabled {
or 10. We defined these values in our Controller. pointer-events: none;
}
</style>
Next is the code that allows the user to browse the various
pages. This is where our PagerModel comes in to use. </head>
<body>
34
<!-- START PAGE CONTAINER --> <i class="glyphicon
<div class="container-fluid"> glyphicon-folder-open"></i>
<!-- START PAGE SIDEBAR --> </button></td>
<!-- commented out <div </tr>
th:replace="fragments/header :: header"> </div> --> </tbody>
<!-- END PAGE SIDEBAR --> </table>
<!-- PAGE TITLE --> <div class="row">
<div class="page-title"> <div class="form-group col-md-1">
<h2> <select class="form-control pagina-
<span class="fa fa-arrow-circle-o- tion" id="pageSizeSelect">
left"></span> Client Viewer <option th:each="pageSize :
</h2> ${pageSizes}" th:text="${pageSize}"
</div> th:value="${pageSize}"
<!-- END PAGE TITLE --> th:selected="${pageSize} ==
<div class="row"> ${selectedPageSize}"></option>
<table class="table datatable"> </select>
<thead> </div>
<tr> <div th:if="${clientlist.totalPages !=
<th>Name</th> 1}"
<th>Address</th> class="form-group col-md-11
<th>Load</th> pagination-centered">
</tr> <ul class="pagination">
</thead> <li th:class="${clientlist.number
<tbody> == 0} ? disabled"><a
<tr th:each="clientlist : ${cli- class="pageLink"
entlist}"> th:href="@{/(pageSize=${selec
<td tedPageSize}, page=1)}">«</a>
th:text="${clientlist.name}">Text ...</td> </li>
<td <li th:class="${clientlist.number
th:text="${clientlist.address}">Text ...</td> == 0} ? disabled"><a
<td><button type="button" class="pageLink"
class="btn btn-primary th:href="@{/(pageSize=${selec
btn-condensed"> tedPageSize}, page=${clientlist.number})}">←</a>
</li>
35
<li integrity="sha256-VAvG3sHdS5LqTT+5A/aeq/bZGa/Uj04xKxY8K
th:class="${clientlist.number M/w9EE="
== (page - 1)} ? 'active pointer-disabled'" crossorigin="anonymous"></script>
th:each="page :
${#numbers.sequence(pager.startPage, pager.endPage)}">
<a class="pageLink" <script
th:href="@{/(pageSize=${selec src="https://maxcdn.bootstrapcdn.com/bootstrap/3.
tedPageSize}, page=${page})}" 3.7/js/bootstrap.min.js"
th:text="${page}"></a> integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZx
</li> UPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
<li crossorigin="anonymous"></script>
th:class="${clientlist.number <script th:inline="javascript">
+ 1 == clientlist.totalPages} ? disabled"> /*<![CDATA[*/
<a class="pageLink" $(document).ready(function() {
th:href="@{/(pageSize=${selec changePageAndSize();
tedPageSize}, page=${clientlist.number + 2})}">→</a> });
</li>
<li function changePageAndSize() {
th:class="${clientlist.number $('#pageSizeSelect').change(function(evt) {
+ 1 == clientlist.totalPages} ? disabled"> window.location.replace("/?pageSize=" +
<a class="pageLink" this.value + "&page=1");
th:href="@{/(pageSize=${selec });
tedPageSize}, }
page=${clientlist.totalPages})}">»</a> /*]]>*/
</li> </script>
</ul>
</div> </body>
</div> </html>
</div>
<!-- END PAGE CONTENT -->
<!-- END PAGE CONTAINER -->
7 – Configuration
</div>
<script The below properties can be changed based on your preferences
src="https://code.jquery.com/jquery-1.11.1.min.js" but were what I wanted for my environment.
36
application.properties
#==================================
# = Thymeleaf configurations
#==================================
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.content-type=text/html
spring.thymeleaf.cache=false
server.contextPath=/
8 – Demo
Visit localhost:8080 and see how you can change page size and
page number :-).
37
Validation in
Thymeleaf Example
7
Here we build an example
application that validates
input.
Overview <modelVersion>4.0.0</modelVersion>
<groupId>com.michaelcgood</groupId>
Important topics we will be discussing are dealing with null val- <artifactId>michaelcgood-validation-thymeleaf</
ues, empty strings, and validation of input so we do not enter artifactId>
invalid data into our database. <version>0.0.1</version>
<packaging>jar</packaging>
39
</dependency> <groupId>org.springframework.boot</groupI
<dependency> d>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</art
<artifactId>spring-boot-starter-thymeleaf</ar ifactId>
tifactId> </plugin>
</dependency> </plugins>
<dependency> </build>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</
artifactId> </project>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId> 3 – Model
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
In our model we define:
<dependency>
<groupId>org.springframework.boot</groupId> • An autogenerated id field
<artifactId>spring-boot-starter-test</ • A name field that cannot be null
artifactId>
• That the name must be between 2 and 40 characters
<scope>test</scope>
• An email field that is validated by the @Email annotation
</dependency>
<!-- legacy html allow -->
• A boolean field “openhouse” that allows a potential stu-
<dependency> dent to indicate if she wants to attend an open house
<groupId>net.sourceforge.nekohtml</groupId> • A boolean field “subscribe” for subscribing to email up-
<artifactId>nekohtml</artifactId> dates
<version>1.9.21</version> • A comments field that is optional, so there is no minimum
</dependency> character requirement but there is a maximum character
</dependencies> requirement
40
import javax.persistence.GeneratedValue; public void setName(String name) {
import javax.persistence.GenerationType; this.name = name;
import javax.persistence.Id; }
import javax.validation.constraints.NotNull; public String getEmail() {
import javax.validation.constraints.Size; return email;
}
import org.hibernate.validator.constraints.Email; public void setEmail(String email) {
this.email = email;
@Entity }
public class Student { public Boolean getOpenhouse() {
return openhouse;
@Id }
@GeneratedValue(strategy = GenerationType.AUTO) public void setOpenhouse(Boolean openhouse) {
private Long id; this.openhouse = openhouse;
@NotNull }
@Size(min=2, max=40) public Boolean getSubscribe() {
private String name; return subscribe;
@NotNull }
@Email public void setSubscribe(Boolean subscribe) {
private String email; this.subscribe = subscribe;
private Boolean openhouse; }
private Boolean subscribe; public String getComments() {
@Size(min=0, max=300) return comments;
private String comments; }
public void setComments(String comments) {
public Long getId() { this.comments = comments;
return id; }
}
public void setId(Long id) {
this.id = id; }
}
public String getName() {
return name;
} 4 – Repository
41
We define a repository. BindingResult must follow next, or else the user is given an er-
ror page when submitting invalid data instead of remaining on
package com.michaelcgood.dao; the form page.
import
We use if…else to control what happens when a user submits a
org.springframework.data.jpa.repository.JpaRepository;
form. If the user submits invalid data, the user will remain on
import org.springframework.stereotype.Repository;
the current page and nothing more will occur on the server side.
import com.michaelcgood.model.Student; Otherwise, the application will consume the user’s data and the
user can proceed.
@Repository
public interface StudentRepository extends JpaReposito-
At this point, it is kind of redundant to check if the student’s
ry<Student,Long> {
name is null, but we do. Then, we call the method checkNull-
}
String, which is defined below, to see if the comment field is an
empty String or null.
package com.michaelcgood.controller;
5 – Controller
import java.util.Optional;
43
}
return "index";
6 – Thymeleaf Templates
public String checkNullString(String str){ As you saw in our Controller’s mapping above, there are two
String endString = null; pages. The index.html is our main page that has the form for po-
if(str == null || str.isEmpty()){ tential University students.
System.out.println("yes it is empty");
str = null;
Optional<String> opt = Our main object is Student, so of course our th:object refers to
Optional.ofNullable(str); that. Our model’s fields respectively go into th:field.
endString = opt.orElse("None");
System.out.println("endString : " + end-
We wrap our form’s inputs inside a table for formatting pur-
String);
}
poses.
else{
; //do nothing Below each table cell (td) we have a conditional statement like
} this one: […]
th:if=”${#fields.hasErrors(‘name’)}” th:errors=”*{name}”
[…]
return endString;
<html xmlns="http://www.w3.org/1999/xhtml"
endString = opt.orElse(“None”); sets the String value to “None”
xmlns:th="http://www.thymeleaf.org">
if the variable opt is null.
<head>
44
<!-- CSS INCLUDE --> <td
<link rel="stylesheet" th:if="${#fields.hasErrors('name')}"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7 th:errors="*{name}">Name
/css/bootstrap.min.css" Error</td>
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7 </tr>
on3RYdg4Va+PmSTsz/K68vbdEjh4u" <tr>
crossorigin="anonymous"></link> <td>Email:</td>
<td><input type="text"
<!-- EOF CSS INCLUDE --> th:field="*{email}"></input></td>
</head> <td
<body> th:if="${#fields.hasErrors('email')}"
th:errors="*{email}">Email
<!-- START PAGE CONTAINER --> Error</td>
<div class="container-fluid"> </tr>
<!-- PAGE TITLE --> <tr>
<div class="page-title"> <td>Comments:</td>
<h2> <td><input type="text"
<span class="fa fa-arrow-circle-o- th:field="*{comments}"></input></td>
left"></span> Request University </tr>
Info <tr>
</h2> <td>Open House:</td>
</div> <td><input type="checkbox"
<!-- END PAGE TITLE --> th:field="*{openhouse}"></input></td>
<div class="column">
<form action="#" th:action="@{/}" </tr>
th:object="${student}" <tr>
method="post"> <td>Subscribe to updates:</td>
<table> <td><input type="checkbox"
<tr> th:field="*{subscribe}"></input></td>
<td>Name:</td>
<td><input type="text" </tr>
th:field="*{name}"></input></td> <tr>
<td>
45
<button type="submit"
class="btn btn-primary">Submit</button>
thanks.html
</td>
</tr>
<html xmlns="http://www.w3.org/1999/xhtml"
</table>
xmlns:th="http://www.thymeleaf.org">
</form>
<head>
</div>
<!-- CSS INCLUDE -->
<!-- END PAGE CONTENT -->
<link rel="stylesheet"
<!-- END PAGE CONTAINER -->
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7
</div>
/css/bootstrap.min.css"
<script
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7
src="https://code.jquery.com/jquery-1.11.1.min.js"
on3RYdg4Va+PmSTsz/K68vbdEjh4u"
integrity="sha256-VAvG3sHdS5LqTT+5A/aeq/bZGa/Uj04
crossorigin="anonymous"></link>
xKxY8KM/w9EE="
crossorigin="anonymous"></script>
<!-- EOF CSS INCLUDE -->
<script
</head>
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.
<body>
3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZx
<!-- START PAGE CONTAINER -->
UPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
<div class="container-fluid">
crossorigin="anonymous"></script>
<!-- PAGE TITLE -->
<div class="page-title">
</body>
<h2>
</html>
<span class="fa fa-arrow-circle-o-
left"></span> Thank you
</h2>
</div>
Here we have the page that a user sees when they have success-
<!-- END PAGE TITLE -->
fully completed the form. We use th:text to show the user the <div class="column">
text he or she input for that field. <table class="table datatable">
46
<thead>
<tr> <script
<th>Name</th> src="https://maxcdn.bootstrapcdn.com/bootstrap/3.
<th>Email</th> 3.7/js/bootstrap.min.js"
<th>Open House</th> integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZx
<th>Subscribe</th> UPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
<th>Comments</th> crossorigin="anonymous"></script>
</tr>
</thead> </body>
<tbody> </html>
<tr th:each="student : ${student}">
<td
th:text="${student.name}">Text ...</td>
7 – Configuration
<td
th:text="${student.email}">Text ...</td> Using Spring Boot Starter and including Thymeleaf dependen-
<td cies, you will automatically have a templates location of /
th:text="${student.openhouse}">Text ...</td> templates/, and Thymeleaf just works out of the box. So most of
<td these settings aren’t needed.
th:text="${student.subscribe}">Text ...</td>
<td
th:text="${student.comments}">Text ...</td> The one setting to take note of is LEGACYHTM5 which is pro-
</tr> vided by nekohtml. This allows us to use more casual HTML5
</tbody> tags if we want to. Otherwise, Thymeleaf will be very strict and
</table> may not parse your HTML. For instance, if you do not close
</div> an input tag, Thymeleaf will not parse your HTML.
</div>
<!-- END PAGE CONTAINER -->
</div> application.properties
<script
src="https://code.jquery.com/jquery-1.11.1.min.js" #==================================
integrity="sha256-VAvG3sHdS5LqTT+5A/aeq/bZGa/Uj04xKxY8K # = Thymeleaf configurations
M/w9EE=" #==================================
crossorigin="anonymous"></script> spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
47
spring.thymeleaf.suffix=.html Java 8’s Optional was sort of forced into this application for
spring.thymeleaf.content-type=text/html demonstration purposes, and I want to note it works more or-
spring.thymeleaf.cache=false
ganically when using @RequestParam as shown in my Pagin-
spring.thymeleaf.mode=LEGACYHTML5
gAndSortingRepository tutorial.
server.contextPath=/
However, if you were not using Thymeleaf, you could have possi-
8 – Demo bly made our not required fields Optional. Here Vlad Mihalcea
discusses the best way to map Optional entity attribute with
JPA and Hibernate.
Visit localhost:8080 to get to the homepage.
I input invalid data into the name field and email field.
Now I put valid data in all fields, but do not provide a comment.
It is not required to provide a comment. In our controller, we
made all empty Strings null values. If the user did not provide a
comment, the String value is made “None”.
9 – Conclusion
Wrap up
Notes
48
AJAX with
CKEditor Example
8
Here we build an
application that uses
CKEditor and in the
process do AJAX with
Thymeleaf and Spring
Boot.
1. Overview 3. The XML Document
In this article, we will cover how to use CKEditor with As mentioned, we are uploading an XML document in this appli-
Spring Boot. In this tutorial, we will be importing an XML cation. The XML data will be inserted into the database and
document with numerous data, program the ability to load a set used for the rest of the tutorial.
of data to the CKEditor instance with a GET request, and do a
POST request to save the CKEditor’s data. <?xml version="1.0"?>
<Music
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Technologies we will be using include MongoDB, Thymeleaf,
id="MUS-1" style="1.1">
and Spring Batch.
<status date="2017-11-07">draft</status>
<title xmlns:xhtml="http://www.w3.org/1999/xhtml" >Guide
The full source code for this tutorial is available on Github. to Music I Like - No Specific Genre</title>
<description xmlns:xhtml="http://www.w3.org/1999/xhtml"
>This guide presents a catalog of music that can be found
2. What is CKEditor? on Spotify.
<html:br xmlns:html="http://www.w3.org/1999/xhtml"/>
<html:br xmlns:html="http://www.w3.org/1999/xhtml"/>
CKEditor is a browser-based What-You-See-Is-What-
This is a very small sample of music found on Spotify
You-Get (WYSIWYG) content editor. CKEditor aims to and is no way to be considered comprehensive.
bring to a web interface common word processor features found </description>
in desktop editing applications like Microsoft Word and <songs>
OpenOffice. <song>
<artist>
Run the Jewels
CKEditor has numerous features for end users in regards to the
</artist>
user interface, inserting content, authoring content, and more. <song-title>Legend Has It</song-title>
</song>
There are different versions of CKEditor, but for this tutorial we <song>
are using CKEditor 4. To see a demo, <artist>
Kendrick Lamar
visit: https://ckeditor.com/ckeditor-4/
</artist>
<song-title>ELEMENT.</song-title>
50
</song> Shadowboxin'
<song> </song-title>
<artist> </song>
Weird Al Yankovic </songs>
</artist> </Music>
<song-title>NOW That's What I Call Polka!</song-
title>
</song> 4. Model
<song>
<artist>
Eiffel 65 For the above XML code, we can model a Song like this:
</artist>
<song-title>Blue (Da Ba Dee) - DJ Ponte Ice Pop public class SongModel {
Radio</song-title> @Id
</song> private String id;
<song> @Indexed
<artist> private String artist;
YTCracker @Indexed
</artist> private String songTitle;
<song-title>Hacker Music</song-title> @Indexed
</song> private Boolean updated;
<song>
<artist> // standard getters and setters
MAN WITH A MISSION
</artist>
<song-title> For our application, we will be differentiating between an un-
Raise Your Flag modified song and a song that has been modified in CKEditor
</song-title> with a separate Model and Repository.
</song>
<song>
<artist> Let’s now define what an Updated Song is:
GZA, Method Man
</artist>
public class UpdatedSong {
<song-title>
51
@Id 6.1 Client Side Code
private String id;
@Indexed
private String artist;
In view.html, we use a table in Thymeleaf to iter-
@Indexed ate through each Song in the Song repository. To be able to re-
private String songTitle; trieve the data from the server for a specific song, we pass in the
@Indexed Song’s id to a function.
private String html;
@Indexed
Here’s the snippet of code that is responsible for calling the
private String sid;
function that retrieves the data from the server and subsequent-
// standard getters and setters ly sets the data to the CKEditor instance:
52
As we can see, the id of Song is essential for us to be able to re- ently. Ultimately, we return a simple POJO with a String for
trieve the data. data named ResponseModel, however:
}); musicText.add(songModel.getArtist());
musicText.add(songModel.getSongTitle());
$("#form").attr("action", "/api/save/?sid=" + song); String filterText =
format.changeJsonToHTML(musicText);
}; response.setData(filterText);
} else if(songModel.getUpdated()==true){
6.2 Server Side Code UpdatedSong updated =
updatedDAO.findBysid(sidString);
getSong accepts a parameter named sid, which stands for Song String text = updated.getHtml();
id. sid is also a path variable in the @GetMapping. We System.out.println("getting the updated text
treat sid as a String because this is the id of the Song from Mon- ::::::::" + text);
goDB. response.setData(text);
}
54
success : function(result) { We stated in our contentType for the POST request that the me-
diatype is “text/html”. We need to specify in our mapping that
$("#postResultDiv")
this will be consumed. Therefore, we add consumes =
.html(
MediaType.TEXT_HTML_VALUE with our @PostMapping.
"
55
oldSong.setUpdated(true); In this tutorial, we covered how to load data using a GET re-
songDAO.save(oldSong); quest with the object’s id, set the data to the CKEditor instance,
updatedDAO.insert(updatedSong);
and save the CKEditor’s data back to the database with a POST
System.out.println("get status of boolean dur-
request. There’s extra code, such as using two different entities
ing post :::::" + oldSong.getUpdated());
}else{
for the data (an original and a modified version), that isn’t nec-
UpdatedSong currentSong = essary, but hopefully is instructive.
updatedDAO.findBysid(sid);
currentSong.setHtml(body); The full code can be found on Github.
updatedDAO.save(currentSong);
}
return response;
}
8. Demo
We visit localhost:8080:
If you return to the saved content, you may see “\n”for line
breaks. For the time being, discussing this is out of the scope for
the tutorial.
9. Conclusion
56
Redis with Spring
Boot Example
9
Here we build an
application that uses
Redis, Thymeleaf and
Spring Boot.
1. Overview brew install redis
In this article, we will review the basics of how to use Redis Then start the server:
with Spring Boot through the Spring Data Redis library. mikes-MacBook-Air:~ mike$ redis-server
10699:C 23 Nov 08:35:58.306 # oO0OoO0OoO0Oo Redis is
starting oO0OoO0OoO0Oo
We will build an application that demonstrates how to per-
10699:C 23 Nov 08:35:58.307 # Redis version=4.0.2,
form CRUD operations Redis through a web interface. The bits=64, commit=00000000, modified=0, pid=10699, just
full source code for this project is available on Github. started
10699:C 23 Nov 08:35:58.307 # Warning: no config file
specified, using the default config. In order to specify
2. What is Redis? a config file use redis-server /path/to/redis.conf
10699:M 23 Nov 08:35:58.309 * Increased maximum number of
Redis is an open source, in-memory key-value data store, used open files to 10032 (it was originally set to 256).
as a database, cache and message broker. In terms of implemen- _._
tation, Key Value stores represent one of the largest and oldest
members in the NoSQL space. Redis supports data structures _.-``__
such as strings, hashes, lists, sets, and sorted sets with range ''-._
_.-`` `. `_. ''-._ Redis 4.0.2
queries.
(00000000/0) 64 bit
.-`` .-```. ```\/ _.,_
The Spring Data Redis framework makes it easy to write Spring ''-._
applications that use the Redis key value store by providing an ( ' , .-` | `, ) Running in standa-
abstraction to the data store. lone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 10699
3. Setting Up A Redis Server `-._ `-._ `-./ _.-'
_.-'
|`-._`-._ `-.__.-'
The server is available for free at http://redis.io/download. _.-'_.-'|
| `-._`-._ _.-'_.-' |
If you use a Mac, you can install it with homebrew: http://redis.io
58
`-._ `-._`-.__.-'_.-' <dependency>
_.-' <groupId>org.springframework.boot</groupId>
|`-._`-._ `-.__.-' <artifactId>spring-boot-starter-web</artifactId>
_.-'_.-'| </dependency>
|
| `-._`-._ _.-'_.-'
5. Redis Configuration
`-._ `-._`-.__.-'_.-'
_.-' We need to connect our application with the Redis server. To es-
`-._ `-.__.-' tablish this connection, we are using Jedis, a Redis client imple-
_.-' mentation.
`-._
_.-'
`-.__.-' 5.1 Config
59
The JedisConnectionFactory is made into a bean so we can cre- }
ate a RedisTemplate to query data.
public void publish(final String message) {
redisTemplate.convertAndSend(topic.getTopic(),
5.2 Message Publisher message);
}
Following the principles of SOLID, we create a MessagePublish-
}
er interface:
public MessagePublisherImpl() { For this example, we defining a Movie model with two fields:
}
60
6.2 Repository interface Our implementation class uses the redisTemplate defined in
our configuration class RedisConfig.
Unlike other Spring Data projects, Spring Data Redis does
offer any features to build on top of the other Spring We use the HashOperations template that Spring Data Redis
Data interfaces. This is odd for us who have experience with offers:
the other Spring Data projects.
@Repository
Often there is no need to write an implementation of a reposi- public class RedisRepositoryImpl implements RedisReposi-
tory interface with Spring Data projects. We simply just interact tory {
with the interface. Spring Data JPA provides numerous reposi- private static final String KEY = "Movie";
tory interfaces that can be extended to get features such as
CRUD operations, derived queries, and paging. private RedisTemplate<String, Object> redisTemplate;
private HashOperations hashOperations;
61
<form id="addForm">
public Movie findMovie(final String id){ <div class="form-group">
return (Movie) hashOperations.get(KEY, id); <label for="keyInput">Movie ID
} (key)</label>
<input name="keyInput" id="keyInput"
public Map<Object, Object> findAllMovies(){ class="form-control"/>
return hashOperations.entries(KEY); </div>
} <div class="form-group">
<label for="valueInput">Movie Name
} (field of Movie object value)</label>
Let’s take note of the init() method. In this method, we use a <input name="valueInput" id="valueI-
function named opsForHash(), which returns the operations nput" class="form-control"/>
</div>
performed on hash values bound to the given key. We then use
<button class="btn btn-default"
the hashOps, which was defined in init(), for all our CRUD op-
id="addButton">Add</button>
erations. </form>
7. Web interface Now we use JavaScript to persist the values on form submis-
sion:
$(document).ready(function() {
In this section, we will review adding Redis CRUD operations var keyInput = $('#keyInput'),
capabilities to a web interface. valueInput = $('#valueInput');
refreshTable();
7.1 Add A Movie
$('#addForm').on('submit', function(event) {
var data = {
We want to be able to add a Movie in our web page. The Key is key: keyInput.val(),
the is the Movie id and the Value is the actual object. However, value: valueInput.val()
we will later address this so only the Movie name is shown as };
the value.
$.post('/add', data, function() {
refreshTable();
So, let’s add a form to a HTML document and assign appropri- keyInput.val('');
ate names and ids : valueInput.val('');
62
keyInput.focus(); var attr,
}); mainTable = $('#mainTable tbody');
event.preventDefault(); mainTable.empty();
}); for (attr in data) {
if (data.hasOwnProperty(attr)) {
keyInput.focus(); mainTable.append(row(attr, data[attr]));
}); }
}
We assign the @RequestMapping value for the POST request, });}
request the key and value, create a Movie object, and save it to The GET request is processed by a method named findAll() that
the repository: retrieves all the Movie objects stored in the repository and then
@RequestMapping(value = "/add", method = converts the datatype from Map<Object, Object> to Map<S-
RequestMethod.POST) tring, String>:
public ResponseEntity<String> add(
@RequestParam String key, @RequestMapping("/values")
@RequestParam String value) { public @ResponseBody Map<String, String> findAll() {
Map<Object, Object> aa =
Movie movie = new Movie(key, value); redisRepository.findAllMovies();
Map<String, String> map = new HashMap<String,
redisRepository.add(movie); String>();
return new ResponseEntity<>(HttpStatus.OK); for(Map.Entry<Object, Object> entry : aa.entrySet()){
} String key = (String) entry.getKey();
map.put(key, aa.get(key).toString());
7.2 Viewing the content }
return map;
}
Once a Movie object is added, we refresh the table to display an
updated table. In our JavaScript code block for section 7.1, we
called a JavaScript function called refreshTable(). This function 7.3 Delete a Movie
performs a GET request to retrieve the current data in the re-
pository: We write Javascript to do a POST request to /delete, refresh the
table, and set keyboard focus to key input:
function refreshTable() {
$.get('/values', function(data) { function deleteKey(key) {
63
$.post('/delete', {key: key}, function() { The source code for the example application is on Github.
refreshTable();
$('#keyInput').focus();
});
}
8. Demo
9. Conclusion
64
HTML to Microsoft
Excel Example
10
Here we build an
application that has a
Thymeleaf interface, takes
input, and converts HTML
to rich text for Microsoft
Excel.
1. Overview use Java 9. This is because of a java.util.regex appendReplace-
ment method we use that has only been available since Java 9.
In this tutorial, we will be building an application that takes
HTML as an input and creates a Microsoft Excel Workbook <parent>
with a RichText representation of the HTML that was pro- <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
vided. To generate the Microsoft Excel Workbook, we will be us-
<version>1.5.9.RELEASE</version>
ing Apache POI. To analyze the HTML, we will be using Jeri-
<relativePath /> <!-- lookup parent from repository
cho. -->
</parent>
The full source code for this tutorial is available on Github.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.so
2. What is Jericho? urceEncoding>
<project.reporting.outputEncoding>UTF-8</project.repo
rting.outputEncoding>
Jericho is a java library that allows analysis and manipulation <java.version>9</java.version>
of parts of an HTML document, including server-side tags, </properties>
while reproducing verbatim any unrecognized or invalid HTML.
It also provides high-level HTML form manipulation <dependencies>
functions. It is an open source library released under the follow- <dependency>
ing licenses: Eclipse Public License (EPL), GNU Lesser General <groupId>org.springframework.boot</groupId>
Public License (LGPL), and Apache License. <artifactId>spring-boot-starter-batch</
artifactId>
</dependency>
I found Jericho to be very easy to use for achieving my goal of <dependency>
converting HTML to RichText. <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</
artifactId>
3. pom.xml </dependency>
Here are the required dependencies for the application we are <dependency>
building. Please take note that for this application we have to <groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
66
<scope>runtime</scope> <dependency>
</dependency> <groupId>net.htmlparser.jericho</groupId>
<dependency> <artifactId>jericho-html</artifactId>
<groupId>org.springframework.boot</groupId> <version>3.4</version>
<artifactId>spring-boot-starter-test</artifactId> </dependency>
<scope>test</scope> <dependency>
</dependency> <groupId>org.springframework.boot</groupId>
<!-- <artifactId>spring-boot-configuration-processor</
https://mvnrepository.com/artifact/org.apache.commons/com artifactId>
mons-lang3 --> <optional>true</optional>
<dependency> </dependency>
<groupId>org.apache.commons</groupId> <!-- legacy html allow -->
<artifactId>commons-lang3</artifactId> <dependency>
<version>3.7</version> <groupId>net.sourceforge.nekohtml</groupId>
</dependency> <artifactId>nekohtml</artifactId>
<dependency> </dependency>
<groupId>org.springframework.batch</groupId> </dependencies>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency> Web Page: Thymeleaf
<dependency>
<groupId>org.apache.poi</groupId> We use Thymeleaf to create a basic webpage that has a form
<artifactId>poi</artifactId>
with a textarea. The source code for Thymeleaf page is available
<version>3.15</version>
</dependency> here on GitHub. This textarea could be replaced with a
RichText Editor if we like, such as CKEditor. We just must be
<dependency> mindful to make the data for AJAX correct, using an appropri-
<groupId>org.apache.poi</groupId> ate setData method. There is a previous tutorial about CKeditor
<artifactId>poi-ooxml</artifactId>
titled AJAX with CKEditor in Spring Boot.
<version>3.15</version>
</dependency>
<!-- Controller
https://mvnrepository.com/artifact/net.htmlparser.jericho
/jericho-html -->
67
In our controller, we Autowire JobLauncher and a Spring Batch
}
job we are going to create called GenerateExcel. Autowiring
these two classes allow us to run the Spring Batch Job Generate-
Excel on demand when a POST request is sent to “/export”. @PostMapping("/export")
public String postTheFile(@RequestBody String body,
Another thing to note is that to ensure that the Spring Batch job RedirectAttributes redirectAttributes, Model model)
will run more than once we include unique parameters with this throws IOException, JobExecutionAlreadyRunningEx-
code: addLong(“uniqueness”, ception, JobRestartException, JobInstanceAlreadyComplete-
Exception, JobParametersInvalidException {
System.nanoTime()).toJobParameters(). An error may occur if
we do not include unique parameters because only
unique JobInstances may be created and executed, and Spring setCurrentContent(body);
Batch has no way of distinguishing between the first and sec-
ond JobInstance otherwise. Job job = exceljob.ExcelGenerator();
jobLauncher.run(job, new
JobParametersBuilder().addLong("uniqueness",
System.nanoTime()).toJobParameters()
);
@Controller
public class WebController { return "redirect:/";
}
private String currentContent;
//standard getters and setters
@Autowired
JobLauncher jobLauncher; }
@Autowired
GenerateExcel exceljob;
6. Batch Job
@GetMapping("/")
public ModelAndView getHome() { In Step1 of our Batch job, we call the getCurrentContent()
ModelAndView modelAndView = new ModelAndView("in- method to get the content that was passed into the Thymeleaf
dex"); form, create a new XSSFWorkbook, specify an arbitrary Micro-
return modelAndView;
68
soft Excel Sheet tab name, and then pass all three variables into public RepeatStatus execute(StepContribu-
the createWorksheet method that we will be making in the next tion stepContribution, ChunkContext chunkContext) throws
Exception, JSONException {
step of our tutorial :
String content =
webcontroller.getCurrentContent();
System.out.println("content is ::" +
@Configuration content);
@EnableBatchProcessing Workbook wb = new XSSFWorkbook();
@Lazy String tabName = "some";
public class GenerateExcel { createexcel.createWorkSheet(wb, con-
tent, tabName);
List<String> docIds = new ArrayList<String>();
return RepeatStatus.FINISHED;
@Autowired }
private JobBuilderFactory jobBuilderFactory; })
.build();
@Autowired }
private StepBuilderFactory stepBuilderFactory;
@Bean
@Autowired public Job ExcelGenerator() {
WebController webcontroller; return jobBuilderFactory.get("ExcelGenerator")
.start(step1())
@Autowired .build();
CreateWorksheet createexcel;
}
@Bean
public Step step1() { }
return stepBuilderFactory.get("step1")
.tasklet(new Tasklet() {
@Override We have covered Spring Batch in other tutorials such as Con-
verting XML to JSON + Spring Batch and Spring Batch CSV
Processing.
69
public class RichTextInfo {
private int startIndex;
7. Excel Creation Service private int endIndex;
private STYLES fontStyle;
We use a variety of classes to create our Microsoft Excel file. Or- private String fontValue;
der matters when dealing with converting HTML to RichText, // standard getters and setters, and the like
so this will be a focus.
7.3 Styles
7.1 RichTextDetails
A enum that contains HTML tags that we want to process. We
A class with two parameters: a String that will have our con- can add to this as necessary:
tents that will become RichText and a font map.
public enum STYLES {
BOLD("b"),
public class RichTextDetails {
EM("em"),
private String richText;
STRONG("strong"),
private Map<Integer, Font> fontMap;
COLOR("color"),
//standard getters and setters
UNDERLINE("u"),
@Override
SPAN("span"),
public int hashCode() {
ITALLICS("i"),
UNKNOWN("unknown"),
// The goal is to have a more efficient hashcode
PRE("pre");
than standard one.
// standard getters and setters
return richText.hashCode();
}
7.4 TagInfo
7.2 RichTextInfo
A POJO to keep track of tag info:
A POJO that will keep track of the location of the RichText and
what not: public class TagInfo {
private String tagName;
private String style;
private int tagType;
70
// standard getters and setters RichTextString cellValue = mergeTextDetails(cellVal-
ues);
7.5 HTML to RichText
return cellValue;
This is not a small class, so let’s break it down by method. }
Essentially, we are surrounding any arbitrary HTML with As we saw above, we pass an ArrayList of RichTextDetails in
a div tag, so we know what we are looking for. Then we look for this method. Jericho has a setting that takes boolean value to
all elements within the div tag, add each to an ArrayList of recognize empty tag elements such as <br/>
RichTextDetails , and then pass the whole ArrayList to the mer- : Config.IsHTMLEmptyElementTagRecognised. This can be im-
geTextDetails method. mergeTextDetails returns Richtext- portant when dealing with online rich text editors, so we set this
String, which is what we need to set a cell value: to true. Because we need to keep track of the order of the ele-
ments, we use a LinkedHashMap instead of a HashMap.
public RichTextString fromHtmlToCellValue(String html,
Workbook workBook){
Config.IsHTMLEmptyElementTagRecognised = true; private static RichTextString mergeTextDetails(L-
ist<RichTextDetails> cellValues) {
Matcher m = HEAVY_REGEX.matcher(html); Config.IsHTMLEmptyElementTagRecognised = true;
String replacedhtml = m.replaceAll(""); StringBuilder textBuffer = new StringBuilder();
StringBuilder sb = new StringBuilder(); Map<Integer, Font> mergedMap = new LinkedHashMap<Inte-
sb.insert(0, "<div>"); ger, Font>(550, .95f);
sb.append(replacedhtml); int currentIndex = 0;
sb.append("</div>"); for (RichTextDetails richTextDetail : cellValues) {
String newhtml = sb.toString(); //textBuffer.append(BULLET_CHARACTER + " ");
Source source = new Source(newhtml); currentIndex = textBuffer.length();
List<RichTextDetails> cellValues = new Ar- for (Entry<Integer, Font> entry :
rayList<RichTextDetails>(); richTextDetail.getFontMap()
for(Element el : source.getAllElements("div")){ .entrySet()) {
cellValues.add(createCellValue(el.toString(), mergedMap.put(entry.getKey() + currentIndex,
workBook)); entry.getValue());
} }
textBuffer.append(richTextDetail.getRichText())
71
.append(NEW_LINE); Map<String, TagInfo> tagMap = new LinkedHashMap<S-
} tring, TagInfo>(550, .95f);
for (Element e : source.getChildElements()) {
RichTextString richText = new getInfo(e, tagMap);
XSSFRichTextString(textBuffer.toString()); }
for (int i = 0; i < textBuffer.length(); i++) {
Font currentFont = mergedMap.get(i); StringBuilder sbPatt = new StringBuilder();
if (currentFont != null) { sbPatt.append("(").append(StringUtils.join(tagMap.key
richText.applyFont(i, i + 1, currentFont); Set(), "|")).append(")");
} String patternString = sbPatt.toString();
} Pattern pattern = Pattern.compile(patternString);
return richText; Matcher matcher = pattern.matcher(html);
}
StringBuilder textBuffer = new StringBuilder();
List<RichTextInfo> textInfos = new ArrayList<RichTex-
tInfo>();
ArrayDeque<RichTextInfo> richTextBuffer = new ArrayDe-
que<RichTextInfo>();
As mentioned above, we are using Java 9 in order to use String- while (matcher.find()) {
Builder with the java.util.regex.Matcher.appendReplacement. matcher.appendReplacement(textBuffer, "");
Why? Well that’s because StringBuffer slower than String- TagInfo currentTag =
Builder for operations. StringBuffer functions are synchronized tagMap.get(matcher.group(1));
for thread safety and thus slower. if (START_TAG == currentTag.getTagType()) {
richTextBuffer.push(getRichTextInfo(currentTa
g, textBuffer.length(), workBook));
We are using Deque instead of Stack because a more complete } else {
and consistent set of LIFO stack operations is provided by the if (!richTextBuffer.isEmpty()) {
Deque interface: RichTextInfo info = richTextBuffer.pop();
if (info != null) {
static RichTextDetails createCellValue(String html, Work- info.setEndIndex(textBuffer.length())
book workBook) { ;
Config.IsHTMLEmptyElementTagRecognised = true; textInfos.add(info);
Source source = new Source(html); }
}
72
} if (font == null) {
} font = workBook.createFont();
matcher.appendTail(textBuffer); }
Map<Integer, Font> fontMap = buildFontMap(textInfos,
workBook); switch (fontStyle) {
case BOLD:
return new RichTextDetails(textBuffer.toString(), case EM:
fontMap); case STRONG:
} font.setBoldweight(Font.BOLDWEIGHT_BOLD);
break;
We can see where RichTextInfo comes in to use here: case UNDERLINE:
font.setUnderline(Font.U_SINGLE);
private static Map<Integer, Font> buildFontMap(L- break;
ist<RichTextInfo> textInfos, Workbook workBook) { case ITALLICS:
Map<Integer, Font> fontMap = new LinkedHashMap<Inte- font.setItalic(true);
ger, Font>(550, .95f); break;
case PRE:
for (RichTextInfo richTextInfo : textInfos) { font.setFontName("Courier New");
if (richTextInfo.isValid()) { case COLOR:
for (int i = richTextInfo.getStartIndex(); i if (!isEmpty(fontValue)) {
< richTextInfo.getEndIndex(); i++) {
fontMap.put(i, mergeFont(fontMap.get(i), font.setColor(IndexedColors.BLACK.getIndex());
richTextInfo.getFontStyle(), richTextInfo.getFontValue(), }
workBook)); break;
} default:
} break;
} }
return fontMap; return font;
} }
Where we use STYLES enum: We are making use of the TagInfo class to track the current tag:
private static Font mergeFont(Font font, STYLES fontStyle, String fontValue, private static RichTextInfo getRichTextInfo(TagInfo cur-
Workbook workBook) { rentTag, int startIndex, Workbook workBook) {
73
RichTextInfo info = null; if (e.getChildElements()
switch (STYLES.fromValue(currentTag.getTagName())) { .size() > 0) {
case SPAN: List<Element> children = e.getChildElements();
if (!isEmpty(currentTag.getStyle())) { for (Element child : children) {
for (String style : currentTag.getStyle() getInfo(child, tagMap);
.split(";")) { }
String[] styleDetails = style.split(":"); }
if (styleDetails != null && if (e.getEndTag() != null) {
styleDetails.length > 1) { tagMap.put(e.getEndTag()
if .toString(),
("COLOR".equalsIgnoreCase(styleDetails[0].trim())) { new TagInfo(e.getEndTag()
info = new RichTextInfo(startIn- .getName(), END_TAG));
dex, -1, STYLES.COLOR, styleDetails[1]); } else {
} // Handling self closing tags
} tagMap.put(e.getStartTag()
} .toString(),
} new TagInfo(e.getStartTag()
break; .getName(), END_TAG));
default: }
info = new RichTextInfo(startIndex, -1, }
STYLES.fromValue(currentTag.getTagName()));
break;
7.6 Create Worksheet
}
return info;
} Using StringBuilder, I create a String that is going to written to
FileOutPutStream. In a real application this should be user de-
fined. I appended my folder path and filename on two different
We process the HTML tags:
private static void getInfo(Element e, Map<String, TagIn- lines. Please change the file path to your own.
fo> tagMap) {
tagMap.put(e.getStartTag()
sheet.createRow(0) creates a row on the very first line
.toString(),
and dataRow.createCell(0) creates a cell in column A of the
new TagInfo(e.getStartTag()
.getName(), e.getAttributeValue("style"),
row.
START_TAG));
74
public void createWorkSheet(Workbook wb, String content, sheet.autoSizeColumn(0);
String tabName) {
StringBuilder sbFileName = new StringBuilder();
sbFileName.append("/Users/mike/javaSTS/michaelcgo try {
od-apache-poi-richtext/"); /////////////////////////////////
sbFileName.append("myfile.xlsx"); // Write the output to a file
String fileMacTest = sbFileName.toString(); wb.write(fileOut);
try { fileOut.close();
this.fileOut = new FileOutputStream(fileMacT- } catch (IOException ex) {
est); Logger.getLogger(CreateWorksheet.class.getNam
} catch (FileNotFoundException ex) { e())
Logger.getLogger(CreateWorksheet.class.getNam .log(Level.SEVERE, null, ex);
e()) }
.log(Level.SEVERE, null, ex); }
}
76
Conclusion
11
Thanks for reading. Let’s
cover what’s next.
Thanks for reading. I plan to make further revisions and addi-
tions to this book as time goes on. If you have the current book,
then you will be getting any future versions free of charge.
Lastly, once again if you have any questions or just want to con-
tact me, send me an email at [email protected] .
78