Você está na página 1de 79

First Edition

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.

Thymeleaf’s official site is at: http://www.thymeleaf.org/

Spring’s official site is at: https://spring.io/

Thanks to my family for all their support in my life.

Lastly, thank you for reading this short book and I hope it is help-
ful. If you have any questions, please contact me at
michael@michaelcgood.com

ii
Getting Started

1
Here we will be reviewing
the basics of getting
Thymeleaf set up
appropriately with a Spring
Boot project.
Section 1

Overview What is Thymeleaf?


Thymeleaf is a Java library. It is an XML/XHTML/HTML5 tem-
plate engine capable of applying a set of transformations to tem-
plate files in order to display data produced by your applica-
tions.

Thymeleaf is most suited for serving XHTML/HTML5 in web ap-


plications, but it can process any XML file in web or standalone
Lorem Ipsum applications.

The primary goal of Thymeleaf is to provide an elegant and


1. Overview of Thymeleaf
structured way of creating templates.  In order to achieve this, it
2. How to use this book is based on XML tags and attributes that define the execution of
predefined logic on the DOM (Document Object Model), instead
of explicitly writing that logic as code inside the template.

Its architecture allows a fast processing of templates, relying on


intelligent caching of parsed files in order to use the least possi-
ble amount of I/O operations during execution.

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?

• Thymeleaf has full HTML5 compatiblity

How to Use This Book


Just a small portion of this book is a reference manual and it is
not as comprehensive as the Thymeleaf manual. The heart of
the book is chapters 6 through 11, where you build real working
applications that can hopefully help you with your own work. So
for this section, it is best if you have at your computer with your
IDE open. If you have any problems following along or just
have a question, please email me at
michael@michaelcgood.com and I will help you ASAP! It is my
goal that this book is of some help to you.

6
Section 2

application.properti Overview
Spring Boot applies it’s typical convention over configuration ap-

es proach to property files. This means that we can simply put an


“application.properties” file in our “src/main/resources” directory,
and it will be auto-detected. We can then inject any loaded prop-
erties from it as normal.

So, by using this file, we don’t have to explicitly register a Prop-


Goals ertySource, or even provide a path to a property file.

1. Review various application.properties settings in


regards to Thymeleaf Common Thymeleaf Properties
# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.cache=true # Enable template cach-
ing.
spring.thymeleaf.check-template=true # Check that the
template exists before rendering it.
spring.thymeleaf.check-template-location=true # Check
that the templates location exists.
spring.thymeleaf.content-type=text/html # Content-
Type value.
spring.thymeleaf.enabled=true # Enable MVC Thymeleaf
view resolution.
spring.thymeleaf.encoding=UTF-8 # Template encoding.

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

Hello World 1. Hello World with Model

To use Model, you can return the name of the template as a


String:

@GetMapping("helloworldM")
public String helloworldM(Model model){
// add some attributes using model
Topics
return "helloM";
}
1. Model

2. ModelAndView 2. Hello World with ModelAndView


Here we assign a template name to ModelAndView this way,
3. Thymeleaf but it can be done with other methods like setView or set-
ViewName:

@GetMapping("helloworldMV")
public ModelAndView helloWorldMV(){
ModelAndView modelAndView = new ModelAnd-
View("helloMV");

return modelAndView;

Objects can be added to to ModelAndView, sort of like how at-


tributes are added to Model.

10
private String name;
3. Thymeleaf // basic getters and setters

Make a simple repository:


A very basic Thymeleaf template we could use to say “Hello
World”: @Repository
public interface PlanetDAO extends JpaReposito-
<html xmlns="http://www.w3.org/1999/xhtml" ry<Planet, Long> {
xmlns:th="http://www.thymeleaf.org">
Planet findByname(String name);
<head>
}
</head>
<body>
<h1> Hello world with ModelAndView</h1> Now we can do this:

@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);

// call repository and get list for attribute


Let’s add some attributes to our Model from step 1. addPlanets();
Iterable<Planet> planetList =
First I make a entity: planetDAO.findAll();
model.addAttribute("planetList", planetList);
// favorite planets
@Entity
List<Planet> favoritePlanets = new Ar-
public class Planet {
rayList<>();

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>

return "helloM"; </tr>


} </thead>
<tbody>
<tr th:each="planetList : ${planet-
5. Attributes In Thymeleaf List}">
<td
th:text="${planetList.id}">Text ...</td>
<html xmlns="http://www.w3.org/1999/xhtml" <td
xmlns:th="http://www.thymeleaf.org"> th:text="${planetList.name}">Text ...</td>
</tr>
<head> </tbody>
<!-- CSS INCLUDE --> </table>
<link rel="stylesheet" </div>
<br />
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7 <h2>th:if example</h2>
/css/bootstrap.min.css" <table class="table datatable"
th:if="${#lists.size(favoritePlanets)}>1">
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7 <thead>
on3RYdg4Va+PmSTsz/K68vbdEjh4u" <tr>
crossorigin="anonymous"></link> <th>No.</th>
<!-- EOF CSS INCLUDE --> <th>planet</th>
</head>
<body> </tr>
<h1>Hello world with Model</h1> </thead>
<tbody>
<h2>th:text example</h2> <tr th:each="favoritePlanets : ${favor-
<span th:text="${neptune.name}">here is text </ itePlanets}">
span> <td
th:text="${favoritePlanets.id}">Text ...</td>
<h2>th:each example</h2> <td
<div class="planetList" th:text="${favoritePlanets.name}">Text ...</td>
th:unless="${#lists.isEmpty(planetList)}"> </tr>
<table class="table datatable"> </tbody>
<thead> </table>
12
</body>
</html>

13
Section 2

Basic Concepts xmlns:th


This is the thymeleaf namespace being declared
for th:* attributes

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:

• If value is not null:


◦ If value is a boolean and is true.
◦ If value is a number and is non-zero
◦ If value is a character and is non-zero
◦ If value is a String and is not “false”, “off” or “no”
◦ If value is not a boolean, a number, a character or a
String.

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).

Contrary to the ones in th:object, these expressions can in-


clude property navigation (in fact any expression allowed for the
path attribute of a <form:input> JSP tag will be allowed
Inputs here).
17
Note that th:field also understands the new types of <in- <input type="checkbox" th:field="*{features}"
put> element introduced by HTML5 like <input th:value="${feat}" />
<label th:for="${#ids.prev('features')}"
type="datetime" ... />, <input type="color" ... th:text="#{${'storeGuide.feature.' +
/>, etc., effectively adding complete HTML5 support to Spring feat}}">Heating</label>
MVC. </li>
</ul>

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">
&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.

A big advantage of this approach to fragments is that you can


write your fragments’ code in pages that are perfectly display-
able by a browser, with a complete and even validating XHTML
structure, while still retaining the ability to make Thymeleaf in-
clude them into other templates.

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>

<?xml version="1.0" encoding="UTF-8"?> <dependencies>


<project xmlns="http://maven.apache.org/POM/4.0.0" <dependency>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <groupId>org.springframework.boot</groupId>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 <artifactId>spring-boot-starter-thymeleaf</ar
http://maven.apache.org/xsd/maven-4.0.0.xsd"> tifactId>
<modelVersion>4.0.0</modelVersion> </dependency>

<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); }

} else if (currentPage + halfPagesToShow > total- public int getEndPage() {


Pages) { return endPage;
setStartPage(totalPages - getButtonsToShow() }
+ 1);
setEndPage(totalPages); public void setEndPage(int endPage) {
this.endPage = endPage;
} else { }
setStartPage(currentPage - halfPagesToShow);
setEndPage(currentPage + halfPagesToShow); @Override
} public String toString() {
return "Pager [startPage=" + startPage + ", end-
} Page=" + endPage + "]";
}
public int getButtonsToShow() {
return buttonsToShow; }
}

public void setButtonsToShow(int buttonsToShow) {


4 – Repository
if (buttonsToShow % 2 != 0) {

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;

import com.michaelcgood.model.ClientModel; import java.util.Optional;

@Repository import org.springframework.stereotype.Controller;


public interface ClientRepository extends PagingAndSortin- import
gRepository<ClientModel,Long> { org.springframework.web.bind.annotation.GetMapping;
import
} org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.michaelcgood.model.PagerModel;
5 – Controller import
org.springframework.beans.factory.annotation.Autowired;
We define some variables in the beginning of the class. We only import org.springframework.data.domain.Page;
want to show 3 page buttons at time. The initial page is the first import org.springframework.data.domain.PageRequest;
page of results, the initial amount of items on the page is 5, and
import com.michaelcgood.dao.ClientRepository;
the user has the ability to have either 5 or 10 results per page.
import com.michaelcgood.model.ClientModel;

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">&nbsp;</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)}">&laquo;</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})}">&larr;</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})}">&rarr;</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})}">&raquo;</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>

In dealing with null values, we touch on use <name>michaelcgood-validation-thymeleaf</name>


of java.util.Optional which was introduced in Java 1.8. <description>Michael C Good - Validation in Thyme-
leaf Example Application</description>

0 – Spring Boot + Thymeleaf Example <parent>


Form Validation Application <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</
artifactId>
We are building a web application for a university that allows <version>1.5.7.RELEASE</version>
potential students to request information on their programs. <relativePath/> <!-- lookup parent from reposi-
tory -->
</parent>
View and Download the code from Github
<properties>
1 – Project Dependencies <project.build.sourceEncoding>UTF-8</project.buil
d.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.
Besides our typical Spring Boot dependencies, we are using an reporting.outputEncoding>
embedded HSQLDB database and nekohtml for LEGA- <java.version>1.8</java.version>
CYHTML5 mode. </properties>

<?xml version="1.0" encoding="UTF-8"?> <dependencies>


<project xmlns="http://maven.apache.org/POM/4.0.0" <dependency>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <groupId>org.springframework.boot</groupId>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 <artifactId>spring-boot-starter-data-jpa</art
http://maven.apache.org/xsd/maven-4.0.0.xsd"> ifactId>

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

<build> package com.michaelcgood.model;


<plugins>
<plugin> import javax.persistence.Entity;

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;

We register StringTrimmerEditor to convert empty Strings to


import javax.validation.Valid;
null values automatically.
import
When a user sends a POST request, we want to receive the value org.springframework.beans.factory.annotation.Autowired;
of that Student object, so we use @ModelAttribute to do just import
org.springframework.beans.propertyeditors.StringTrimmerEd
that.
itor;
import org.springframework.stereotype.Controller;
To ensure that the user is sending values that are valid, we use import org.springframework.ui.Model;
the appropriately named @Valid annotation next. import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import
org.springframework.web.bind.annotation.GetMapping;
42
import String comments =
org.springframework.web.bind.annotation.InitBinder; checkNullString(newStudent.getComments());
import if (comments != "None") {
org.springframework.web.bind.annotation.ModelAttribute; System.out.println("nothing
import changes");
org.springframework.web.bind.annotation.PostMapping; } else {
import com.michaelcgood.dao.StudentRepository; newStudent.setComments(comments);
import com.michaelcgood.model.Student; }
} catch (Exception e) {
@Controller
public class StudentController { System.out.println(e);
@InitBinder
public void initBinder(WebDataBinder binder) { }
binder.registerCustomEditor(String.class, new studentRepository.save(newStudent);
StringTrimmerEditor(true)); System.out.println("new student added: "
} + newStudent);
public String finalString = null; }
@Autowired
private StudentRepository studentRepository; return "thanks";
@PostMapping(value="/") }
public String addAStudent(@ModelAttribute @Valid Stu- }
dent newStudent, BindingResult bindingResult, Model
model){ @GetMapping(value="thanks")
if (bindingResult.hasErrors()) { public String thankYou(@ModelAttribute Student newStu-
System.out.println("BINDING RESULT ERROR"); dent, Model model){
return "index"; model.addAttribute("student",newStudent);
} else {
model.addAttribute("student", newStudent); return "thanks";
}
if (newStudent.getName() != null) {
try { @GetMapping(value="/")
// check for comments and if not pre- public String viewTheForm(Model model){
sent set to 'none' Student newStudent = new Student();
model.addAttribute("student",newStudent);

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;

} The above conditional statement means if the user inputs data


into that field that doesn’t match the requirement we put for
} that field in our Student model and then submits the form,
show the input requirements when the user is returned to this
page.
Optional.ofNullable(str); means that the String will become the
data type Optional, but the String may be a null value. index.html

<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

This demo application demonstrated how to valid user input in


a Thymeleaf form.

In my opinion, Spring and Thymeleaf work well
with javax.validation.constraints for validating user input.

The source code is on Github

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:

<table class="table datatable">


5. File Upload and Processing <thead>
<tr>
<th>Artist</th>
As this article’s focus is on CKEditor and AJAX, we aren’t going <th>Song Title</th>
to go into detail on the file upload and processing with Spring <th>Load</th>
Batch. We have reviewed this process in depth in these prior </tr>
posts, however: </thead>
<tbody>
<tr th:each="songList : ${songList}">
• Spring Batch CSV Processing
<td th:text="${songList.artist}">Text ...</td>
• Converting XML to JSON + Raw Use in MongoDB + <td th:text="${songList.songTitle}">Text ...</td>
Spring Batch <td><button th:onclick="|getSong('${songList.id}')|"
6. Setting Data to CKEditor – GET Re- id="button" class="btn btn-primary btn-condensed">
quest <i class="glyphicon glyphicon-folder-open"></i>
</button></td>
</tr>
It is our goal to retrieve the data for an individual song and dis- </tbody>
play that data in CKEditor. There are two issues to tackle: re- </table>
trieving the data for an individual song and displaying it in CKE-
ditor.

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:

In the getSong function, we use a deferred promise to ensure @GetMapping(value={"/show/","/show/{sid}"})


that data is set after the GET request: public ResponseEntity<?> getSong(@RequestParam String
sid, Model model){
ResponseModel response = new ResponseModel();
function getSong(song) { System.out.println("SID :::::" + sid);
$.ajax({ ArrayList<String> musicText = new ArrayList<S-
url : "/api/show/?sid=" + song, tring>();
type : 'GET', if(sid!=null){
dataType : 'text' String sidString = sid;
}).then(function(data) { SongModel songModel = songDAO.findOne(sidString);
var length = data.length-2; System.out.println("get status of boolean during
var datacut = data.slice(9,length); get ::::::" + songModel.getUpdated());
CKEDITOR.instances.content.setData(datacut); if(songModel.getUpdated()==false ){

}); 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);
}

We check if the Song has been modified and if so we retrieve the }


associated UpdatedSong entity. If not, we treat the Song differ-
53
model.addAttribute("response", response); 7.1 Client Side Code
return ResponseEntity.ok(response);
}
As the CKEditor instance is a textarea within a form, we can trig-
ger a function on a form submit:

ResponseModel is a very simple POJO as mentioned: $(document)


.ready(
public class ResponseModel { function() {
private String data;
// SUBMIT FORM
public ResponseModel(){ $("#form").submit(function(event) {
// Prevent the form from submitting via the browser.
} event.preventDefault();
ajaxPost();
public ResponseModel(String data){ });
this.data = data;
}
ajaxPost() retrieves the current data in the CKEditor and sets it
public String getData() {
to the variable formData:
return data;
function ajaxPost() {
}

// PREPARE FORM DATA


public void setData(String data) {
var formData = CKEDITOR.instances.content
this.data = data;
.getData();
}
}
// DO POST
$
7. Saving CKEditor Data – POST Request .ajax({
type : "POST",
contentType : "text/html",
POSTing the data isn’t much of a challenge; however, ensuring
url : $("#form").attr("action"),
that the data is handled appropriately can be. data : formData,
dataType : '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.
"

" Areas for us to note include:


+ "Post Successfully! "
+ "
• @RequestBody String body is responsible for setting the
"); variable body to the content of our request
• We once again return ResponseModel, the simple POJO
console.log(result); described earlier that contains our data
}, • We treat a previously modified SongModel different than
error : function(e) { one that has not been modified before
alert("Error!") Also, like the GET request, the sid allows us to deal with the cor-
console.log("ERROR: ", e);
rect Song:
}
});
@PostMapping(value={"/save/","/save/[sid]"}, consumes =
} MediaType.TEXT_HTML_VALUE)
public @ResponseBody ResponseModel saveSong( @Request-
}) Body String body, @RequestParam String sid){
ResponseModel response = new ResponseModel();
response.setData(body);
It is important to note:
SongModel oldSong = songDAO.findOne(sid);
String songTitle = oldSong.getSongTitle();
• contentType is “text/html” String artistName = oldSong.getArtist();
• dataType is “text” if(oldSong.getUpdated() == false){
Having the incorrect contentType or dataType can lead to er- UpdatedSong updatedSong = new UpdatedSong();
updatedSong.setArtist(artistName);
rors or malformed data.
updatedSong.setSongTitle(songTitle);
updatedSong.setHtml(body);
7.2 Server Side Code updatedSong.setSid(sid);

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:

We upload the provided music-example.xml file:

We click “Load” for a song:

We add content and click “Save”:

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

Let’s start with the configuration bean definitions:


10699:M 23 Nov 08:35:58.312 # Server initialized
10699:M 23 Nov 08:35:58.312 * Ready to accept connections
@Bean
JedisConnectionFactory jedisConnectionFactory() {
4. Maven Dependencies return new JedisConnectionFactory();
}

Let’s declare the necessary dependencies in our pom.xml for the


example application we are building: @Bean
public RedisTemplate<String, Object> redisTemplate() {
final RedisTemplate<String, Object> template = new Re-
<dependency> disTemplate<String, Object>();
<groupId>org.springframework.boot</groupId> template.setConnectionFactory(jedisConnectionFactory(
<artifactId>spring-boot-starter-data-redis</ ));
artifactId> template.setValueSerializer(new
</dependency> GenericToStringSerializer<Object>(Object.class));
<dependency> return template;
<groupId>org.springframework.boot</groupId> }
<artifactId>spring-boot-starter-thymeleaf</
artifactId>
</dependency>

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:

We also define this as a bean in RedisConfig:


public interface MessagePublisher {
@Bean
void publish(final String message);
MessagePublisher redisPublisher() {
}
return new MessagePublisherImpl(redisTemplate(),
topic());
We implement the MessagePublisher interface to use the high- }
level RedisTemplate to publish the message since the RedisTem-
plate allows arbitrary objects to be passed in as messages:
@Service 6. RedisRepository
public class MessagePublisherImpl implements MessagePub-
lisher { Now that we have configured the application to interact with
the Redis server, we are going to prepare the application to take
@Autowired example data.
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ChannelTopic topic; 6.1 Model

public MessagePublisherImpl() { For this example, we defining a Movie model with two fields:
}

public MessagePublisherImpl(final RedisTemplate<S- private String id;


tring, Object> redisTemplate, final ChannelTopic topic) { private String name;
this.redisTemplate = redisTemplate; //standard getters and setters
this.topic = topic;

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;

So, unfortunately, we need to write our own interface and @Autowired


then define the methods: public RedisRepositoryImpl(RedisTemplate<String, Ob-
ject> redisTemplate){
this.redisTemplate = redisTemplate;
public interface RedisRepository {
}

Map<Object, Object> findAllMovies();


@PostConstruct
private void init(){
void add(Movie movie);
hashOperations = redisTemplate.opsForHash();
}
void delete(String id);

public void add(final Movie movie) {


Movie findMovie(String id);
hashOperations.put(KEY, movie.getId(),
movie.getName());
}
}

6.3 Repository implementation public void delete(final String id) {


hashOperations.delete(KEY, id);
}

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();
});
}

We request the key and delete the object in the redisReposito-


ry based on this key:

@RequestMapping(value = "/delete", method =


RequestMethod.POST)
public ResponseEntity<String> delete(@RequestParam String
key) {
redisRepository.delete(key);
return new ResponseEntity<>(HttpStatus.OK);
}

8. Demo

Visit localhost:8080 and provide values for Movie Id and and


Movie Name. You will see it is added on the right side of the
page and then you can delete the entrees too.

9. Conclusion

In this tutorial, we introduced Spring Data Redis and one way


of connecting it to a web application to perform CRUD opera-
tions.

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); }
}

Sheet sheet = wb.createSheet(tabName); // Create


8. Demo
new sheet w/ Tab name
We visit localhost:8080.
sheet.setZoom(85); // Set sheet zoom: 85%

We input some text with some HTML:


// content rich text
RichTextString contentRich = null; We open up our excel file and see the RichText we created:
if (content != null) {
contentRich =
htmlToExcel.fromHtmlToCellValue(content, wb); 9. Conclusion
}
We can see it is not trivial to convert HTML to Apache POI’s
RichTextString class; however, for business applications con-
// begin insertion of values into cells verting HTML to RichTextString can be essential because read-
Row dataRow = sheet.createRow(0);
ability is important in Microsoft Excel files. There’s likely room
Cell A = dataRow.createCell(0); // Row Number
to improve upon the performance of the application we build,
A.setCellValue(contentRich);
but we covered the foundation of building such an application .
75
The full source code is available on Github

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.

For now, to stay up-to-date on Spring Boot, Thymeleaf, and


Java related topics in general:

• First, I am shamelessly promoting myself. Check out my blog


at www.michaelcgood.com and my Twitter at
https://twitter.com/michaelcgood

• The official Spring blog: https://spring.io/blog

• The official Thymeleaf Twitter account:


https://twitter.com/thymeleaf

• The official Thymeleaf site: http://www.thymeleaf.org/

• Baeldung - a good website for Java tutorials


http://www.baeldung.com/

• DZone - Java has many educational articles:


https://dzone.com/java-jdk-development-tutorials-tools-news

Lastly, once again if you have any questions or just want to con-
tact me, send me an email at michael@michaelcgood.com .

78

Você também pode gostar