Javalin Cookbook
Begin creating a new project from scratch using this tutorial. Then you might need to know how to …
Javalin
- How to organize files
- How to add more routes
- How to add a database
- How to export and import a Postgres database
Thymeleaf
- How to re-use fragments in templates
- How to use fragments with parameters
- How to add an image
- How to iterate through a list (each ….)
- How to use conditionals (if …)
- How to add a link
Javalin & Thymeleaf
Javalin
1. How to organize files
It’s generally a good idea to organize the Java App into meaningful packages. Suggested packages are:
- config (configuration files, Thymeleaf etc)
- controllers (first stop for each route, these can call service methods, facade DB methods etc)
- services (domain / business logic and methods, can also call facade DB methods)
- entities (domain classes that mirrors the DB)
- exceptions (custom exceptions)
- persistence (database related mapper classes and database facade classes)
- resources
- public
- css
- images
- templates
- public
2. How to add more routes
GET og POST request can be added like this:
app.get("/users", ctx -> UserController.showUserList(ctx, connectionPool));
app.post("/login", ctx -> UserController.login(ctx, connectionPool));
In this example a Class UserController
has been created to organize the code. The methods showUserList
and login
are made static for ease of use.
3. How to add a database
Add a thread-safe connection pool with 2-10 JDBC Postgresql connections. Create a package called persistence
and drop the ConnectionPool class and the rest of DB classes into it:
package app.persistence;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
/***
* Singleton pattern applied to handling a Hikari ConnectionPool
*/
public class ConnectionPool {
private static volatile ConnectionPool instance = null;
private static HikariDataSource ds = null;
private static final Logger LOGGER = Logger.getLogger(ConnectionPool.class.getName());
/***
* Private constructor to enforce Singleton pattern.
*/
private ConnectionPool() {
// Prevent instantiation
}
/***
* Getting a singleton instance of a Hikari Connection Pool with specific credentials
* and connection string. If an environment variable "DEPLOYED" exists, then environment variables
* will be used instead of provided parameters.
* @param user Database username
* @param password Database password
* @param url Database connection URL
* @param db Database name
* @return Singleton instance of ConnectionPool
*/
public static ConnectionPool getInstance(String user, String password, String url, String db) {
if (instance == null) {
synchronized (ConnectionPool.class) {
if (instance == null) { // Double-checked locking
if (System.getenv("DEPLOYED") != null) {
ds = createHikariConnectionPool(
System.getenv("JDBC_USER"),
System.getenv("JDBC_PASSWORD"),
System.getenv("JDBC_CONNECTION_STRING"),
System.getenv("JDBC_DB"));
} else {
ds = createHikariConnectionPool(user, password, url, db);
}
instance = new ConnectionPool();
}
}
}
return instance;
}
/***
* Getting a live connection from the Hikari Connection Pool
* @return a database connection
* @throws SQLException if connection fails
*/
public Connection getConnection() throws SQLException {
if (ds == null) {
throw new SQLException("DataSource is not initialized. Call getInstance() first.");
}
return ds.getConnection();
}
/***
* Closing the Hikari Connection Pool
*/
public void close() {
if (ds != null) {
LOGGER.log(Level.INFO, "Shutting down connection pool...");
ds.close();
ds = null;
instance = null;
}
}
/***
* Configuring a Hikari DataSource ConnectionPool
* @param user Database username
* @param password Database password
* @param url Database connection URL
* @param db Database name
* @return Configured HikariDataSource
*/
private static HikariDataSource createHikariConnectionPool(String user, String password, String url, String db) {
LOGGER.log(Level.INFO, "Initializing Connection Pool for database: {0}", db);
HikariConfig config = new HikariConfig();
config.setDriverClassName("org.postgresql.Driver");
config.setJdbcUrl(String.format(url, db));
config.setUsername(user);
config.setPassword(password);
// Connection Pool Configurations
config.setMaximumPoolSize(10); // Default is 10
config.setMinimumIdle(2); // Ensures some connections are always available
config.setIdleTimeout(30000); // 30 seconds idle timeout
config.setConnectionTimeout(30000); // Max wait time for a connection
config.setPoolName("Postgresql-Pool");
// Optimizations
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
return new HikariDataSource(config);
}
}
Add to beginning of Main-class:
private static final Logger LOGGER = Logger.getLogger(Main.class.getName());
private static final String USER = "postgres";
private static final String PASSWORD = "postgres";
private static final String URL = "jdbc:postgresql://localhost:5432/%s?currentSchema=public";
private static final String DB = "databasename";
private static final ConnectionPool connectionPool = ConnectionPool.getInstance(USER, PASSWORD, URL, DB);
Then the database can be accessed like this:
public static List<User> getAllUsers(ConnectionPool connectionPool) throws DatabaseException
{
List<User> userList = new ArrayList<>();
String sql = "select * from users";
try (Connection connection = connectionPool.getConnection())
{
try (PreparedStatement ps = connection.prepareStatement(sql))
{
ResultSet rs = ps.executeQuery();
while (rs.next())
{
String userName = rs.getString("username");
String password = rs.getString("password");
String role = rs.getString("role");
User user = new User(userName, password, role);
userList.add(user);
}
}
}
catch (SQLException ex)
{
throw new DatabaseException(ex, "Could not get users from database");
}
return userList;
}
4. How to export and import a database
This is how you share a database between team members (when not having a shared database in the cloud):
- Right-click your database in PG-Admin and choose
Backup ...
- Find a filename. Use same name as database
cupcake
- Format: Custom (the most efficient)
- Encoding: UTF8
- Data Option Tab: select
Pre-data
,Data
, andPost-data
. These three ensure that everything is copied. - Once the backup is done. Go to the topmenu, and choose:
Tools -> Store manager ...
- Pick the backup file you just created in click the download button.
- Find the backup file in your
download
folder on disk, and copy it to your team members.
This is how you import / restore the file on another machine
- Create a new database in PG-Admin with the same name as the back-up’ed version
- Right-click the new database and choose
Restore ...
- Choose
Custom or tar
as Format - Click the folder icon in the
Filename
to open the file picker - Click the 3 small dots in the upper right corner to open a local menu, and then click
Upload
- Drop the cupcake file into the file picker to upload - cancel to get back to the file list, and then mark the
cupcake
file - Click
Restore
to import - Refresh the database to see the newly imported database and data
Thymeleaf
1. How to re-use fragments in templates
Re-use html fragments like this:
- Create a html-file in the
templates
-folder to contain all the fragments. One file to rule them all. Call itfragments.html
This is an example with a re-usable<head>
fragment, a header-fragment, and a footer-fragment:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
<title th:text="${title}"></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="../public/css/styles.css" th:href="@{/css/styles.css}" rel="stylesheet"/>
</head>
<body>
<header th:fragment="header" style="width:100%;border-bottom:1px solid black;">
<img src="../public/images/fourthingsplus.png" th:src="@{/images/fourthingsplus.png}" width="100%"/>
</header>
<footer th:fragment="footer" style="width:100%;border-top:1px solid black;">
Dette er så en footer.
</footer>
</body>
</html>
These fragments can replace html-elements in another document like this:
<div th:replace="~{fragments :: head('Frontpage')}"></div>
<div th:replace="~{fragments :: header}"></div>
<div th:replace="~{fragments :: footer}"></div>
OBS! Use the fragments wisely. The upside of fragments is adhering to the DRY principle. The downside is destroying the Thymeleaf principle of Natural Templating. Natural Templating means that you can show your template in a normal browser. The browser cannot interpret fragments, so they spoil the party. You need to run the template through the Thymeleaf engine for fragments to work. And you need to compile and run your Javalin project to make that happen. This takes extra time, and makes it hard to delegate template building to frontend staff. A compromise could be to keep the
<head>
section on each template and then stuff menus, footers etc in fragments. If you put the<head>
section in a fragment, you will miss importing stylesheets etc. Think about it. You might also reade this comment by one of the developers of Thymeleaf.
2. How to use fragments with parameters
In the fragment-example above - notice how the text ‘Frontpage’ is passed as an argument to the fragment. Also notice how the parameter is used in the fragment: <title th:text="${title}"></title>
3. How to add an image
To insert an image do like this:
<img src="../public/images/fourthingsplus.png" th:src="@{/images/fourthingsplus.png}" width="100%"/>
Note: place the image file in the /public/images
folder. The th:src="@{/images/fourthingsplus.png}"
attribute is used by Thymeleaf. The other attribute: src="../public/images/fourthingsplus.png"
will be applied in case you open the file directly in a browser instead of through the Thymeleaf template engine.
4. How to iterate through a list (each ….)
This example assumed that a list of user objects are passed from Javalin:
<h1>Users</h1>
<table>
<thead>
<tr>
<th>username</th>
<th>password</th>
<th>role</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${userList}">
<td th:text="${user.username}">username</td>
<td th:text="${user.password}">password</td>
<td th:text="${user.role}">role</td>
</tr>
</tbody>
</table>
That’s how the th:each
attribute works. It’s is important that you spell the attribute names correctly. username
, password
, and role
should be attributes in the Java User class.
5. How to use conditionals (if …)
Check for details in the Thymeleaf docs.
An example of how to apply if
:
<p th:if="${not #lists.isEmpty(userList)}">Listen er ikke tom</p>
Another way is to use the ternary operator If-then-else: (if) ? (then) : (else)
like this:
<p th:text="${not #lists.isEmpty(userList) ? userList.size + ' in list' : 'Den er tom' }">Ikke tom</p>
6. How to add a link
The link (url/route) should be given in the th:href attribute. In this example as th:href="@{/createuser}"
. It means that the navigation will be forwarded to /createuser. However, if you want to open the html document in a browser directly, then the first link will work: href="createuser.html"
.
<a href="createuser.html" th:href="@{/createuser}">Create account</a>
Javalin & Thymeleaf
1. How to transfer data between frontend and backend
To make the website dynamic means to adapt the html pages to user-input.
1.a From backend to frontend
Use a hashmap to add objects you want to transfer to a Thymeleaf template. Add to the built-in maps in the ctx
object:
ctx
maps:
- Session Scope
ctx.sessionAttribute("userList", userList);
- Request Scope
ctx.attribute("totalSum", 1025);
And then render the template including your attributes:
ctx.render("index.html");
From HTML and Thymeleaf, the hashmaps are accessible in various ways.
- Session Scope
<p th:text="${session.email}"></p>
- Request Scope
<p th:text="${name}"></p>
Note: unfortunately, IntelliJ is not able to provide autocompletion and name resolution for scope and model variables. Luckily it works anyway. A work-a-round is to define the variables as Thymeleaf variables in the beginning of the body block:
<body>
<!--@thymesVar id="hello" type="String"-->
<div th:text="${hello}"></div>
</body>
In this way we get autocompletion. But it’s probably not worth it.
1.b From frontend to backend
There are two ways to send values from a html page to the Javalin backend:
- As query parameters (GET link)
In the html page:
<a href="/users?usergroup=1&page=2">view users</a>
The two query parameters usergroup
and page
is sent to the backend route /users
. When arrived you can retrieve the parameters like this:
int usergroup = Integer.parseInt(ctx.queryParam("usergroup"));
int page = Integer.parseInt(ctx.queryParam("page"));
Note: In an ideal world we should wrap the conversions in a try-catch block, since ctx.queryParam("usergroup")
and ctx.queryParam("page")
return a String
- hopefully with a value.
- As form parameters (GET or POST )
In the html page:
<form method="post">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<button formaction="/login" type="submit" value="login">Login</button>
</form>
The two form parameters username
and password
are sent along the http POST request to Javalin and lands at the route “/login”. Then the values can be retrieved like this:
String userName = ctx.formParam("username");
String password = ctx.formParam("password");
Note: it is easy to spell the name attribute wrong in the form element. In the <input type="text" name="username" placeholder="username">
element name="username"
is the form parameter that we expect on the Javalin side.