Intro
In this tutorial we will create an asynchronous servlet that adds contacts to a list, parses requests and processes form validation with the help of ActiveJ framework. You can find the source code of this tutorial on GitHub.
Setting up the project
We'll need the following dependencies:
<dependencies>
<dependency>
<groupId>io.activej</groupId>
<artifactId>activej-launchers-http</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.4</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>
We'll use the following ActiveJ technologies:
- ActiveInject - lightweight and powerful dependency injection library with great performance and no third-party dependencies.
- ActiveJ HTTP - high-performance asynchronous HTTP clients and servers.
- ActiveJ Launcher - takes care of full application lifecycle, service management, and logging. Perfectly compatible with ActiveInject.
Time to code!
This tutorial represents the MVC pattern:
- To model a
Contact
representation, we will create a plain Java class with fields (name
,age
,address
), constructor, and accessors to the fields:
class Contact {
private final String name;
private final Integer age;
private final Address address;
public Contact(String name, Integer age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Address getAddress() {
return address;
}
@Override
public String toString() {
return "Contact{name='" + name + '\'' + ", age=" + age + ", address=" + address + '}';
}
}
The Address
class is pretty simple:
class Address {
private final String title;
public Address(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
@Override
public String toString() {
return "Address{title='" + title + '\'' + '}';
}
}
To simplify the tutorial, we will use an ArrayList to store the Contact
objects. ContactDAO
interface and its implementation are used for this purpose:
interface ContactDAO {
List<Contact> list();
void add(Contact user);
}
class ContactDAOImpl implements ContactDAO {
private final List<Contact> userList = new ArrayList<>();
@Override
public List<Contact> list() {
return userList;
}
@Override
public void add(Contact user) {
userList.add(user);
}
}
- To build a view we will use a single HTML file, compiled with the help of the Mustache template engine:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Decoder example</title>
<style>
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td, th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #dddddd;
}
div {
text-align: center;
}
span {
color: red;
}
</style>
</head>
<body>
<div>
<form action="/add" method="post">
<table>
<tr>
<th>Name: <input type="text" name="name"> <span>{{errors.name}}</span></th>
<th>Age: <input type="text" name="age"> <span>{{errors.age}}</span></th>
<th>Address: <input type="text" name="title"> <span>{{errors.contact-address-title}}</span>
</th>
</tr>
</table>
<input type="submit" value="Submit">
</form>
</div>
<br>
<table>
<tr>
<th>Name</th>
<th>Age</th>
<th>Address</th>
</tr>
{{#contacts}}
<tr>
<th>{{name}}</th>
<th>{{age}}</th>
<th>{{address.title}}</th>
</tr>
{{/contacts}}
</table>
</body>
</html>
- An
AsyncServlet
will be used as a controller. We will also addRoutingServlet
for routing requests to the needed endpoints. We'll get to this in a moment.
Let’s create HttpDecoderExample
class which extends HttpServerLauncher
. By extending HttpServerLauncher
we will take care of the server’s lifecycle and service management. Next, we provide two custom parsers based on Decoder
- ADDRESS_DECODER
and CONTACT_DECODER
- which will be used for validation. Decoder
class provides you with tools for parsing requests.
public final class HttpDecoderExample extends HttpServerLauncher {
private static final String SEPARATOR = "-";
private static final Decoder<Address> ADDRESS_DECODER = Decoder.of(Address::new,
ofPost("title", "")
.validate(param -> !param.isEmpty(), "Title cannot be empty")
);
private static final Decoder<Contact> CONTACT_DECODER = Decoder.of(Contact::new,
ofPost("name")
.validate(name -> !name.isEmpty(), "Name cannot be empty"),
ofPost("age")
.map(Integer::valueOf, "Cannot parse age")
.validate(age -> age >= 18, "Age must not be less than 18"),
ADDRESS_DECODER.withId("contact-address")
);
Also, we need to create applyTemplate(Mustache mustache, Map<String, Object> scopes)
method to fill the provided Mustache template with the given data:
private static ByteBuf applyTemplate(Mustache mustache, Map<String, Object> scopes) {
ByteBufWriter writer = new ByteBufWriter();
mustache.execute(writer, scopes);
return writer.getBuf();
}
Next, let’s provide a ContactDAOImpl
factory method:
@Provides
ContactDAO dao() {
return new ContactDAOImpl();
}
Now we have everything needed to create the controller AsyncServlet
to handle requests:
@Provides
AsyncServlet mainServlet(ContactDAO contactDAO) {
Mustache contactListView = new DefaultMustacheFactory().compile("static/contactList.html");
return RoutingServlet.create()
.map("/", request ->
HttpResponse.ok200()
.withBody(applyTemplate(contactListView, map("contacts", contactDAO.list()))))
.map(POST, "/add", AsyncServletDecorator.loadBody()
.serve(request -> {
Either<Contact, DecodeErrors> decodedUser = CONTACT_DECODER.decode(request);
if (decodedUser.isLeft()) {
contactDAO.add(decodedUser.getLeft());
}
Map<String, Object> scopes = map("contacts", contactDAO.list());
if (decodedUser.isRight()) {
scopes.put("errors", decodedUser.getRight().toMap(SEPARATOR));
}
return HttpResponse.ok200()
.withBody(applyTemplate(contactListView, scopes));
}));
}
Here we provide an AsyncServlet
, which receives HttpRequest
s from clients, creates HttpResponse
s depending on the route path and sends it.
Inside the RoutingServlet
two route paths are defined. The first one matches requests to the root route /
- it simply displays a contact list. The second one, /add
- is an HTTP POST method that adds or declines adding new users. We will process this request parsing with the help of the aforementioned Decoder
by using decode
method:
Either<Contact, DecodeErrors> decodedUser = CONTACT_DECODER.decode(request);
Either
represents a value of two possible data types which are either Left
(Contact
) or Right
(DecodeErrors
). In order to determine whether a parse was successful or not, we check it’s value by using the isLeft()
and isRight()
methods.
Finally, write down the main
method which will launch our application:
public static void main(String[] args) throws Exception {
Launcher launcher = new HttpDecoderExample();
launcher.launch(args);
}
Time to test!
You've just created and launched an MVC web application with asynchronous and high-performance server! Now you can test the app by visiting localhost:8080.
Top comments (0)