Form Validation Using HTTP Decoder
In this example we will create an async servlet that adds contacts to a list, parses requests and processes form
validation with the help of Decoder
. Consider this example as a concise representation of 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. - To simplify the example, we will use an
ArrayList
to store theContact
objects.ContactDAO
interface and its implementation are used for this purpose. - To build a view we will use a single HTML file, compiled with the help of the Mustache template engine.
- An
AsyncServlet
will be used as a controller. We will also addRoutingServlet
for routing a concrete request to a particular endpoint. - Decoder provides you with tools for parsing requests.
Here we will consider only HttpDecoderExample
class with AsyncServlet
as it contains ActiveJ-specific features.
Creating HttpDecoderExample
Class
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 HTTP Decoder
- ADDRESS_DECODER
and CONTACT_DECODER
- which will be used for validation.
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(Mapper.ofEx(Integer::valueOf, "Cannot decode age"))
.validate(age -> age >= 18, "Age must not be less than 18"),
ADDRESS_DECODER.withId("contact-address")
);
If you want to learn more about template engines integration, check out this example
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 AsyncServlet
to handle requests:
@Provides
AsyncServlet mainServlet(Reactor reactor, ContactDAO contactDAO) {
Mustache contactListView = new DefaultMustacheFactory().compile("static/contactList.html");
return RoutingServlet.builder(reactor)
.with("/", request -> HttpResponse.ok200()
.withBody(applyTemplate(contactListView, Map.of("contacts", contactDAO.list())))
.toPromise())
.with(POST, "/add", request -> request.loadBody()
.then($ -> {
//[START REGION_3]
Either<Contact, DecodeErrors> decodedUser = CONTACT_DECODER.decode(request);
//[END REGION_3]
if (decodedUser.isLeft()) {
contactDAO.add(decodedUser.getLeft());
}
Map<String, Object> scopes = new HashMap<>();
scopes.put("contacts", contactDAO.list());
if (decodedUser.isRight()) {
//noinspection ConstantConditions - is 'right', hence not 'null'
scopes.put("errors", decodedUser.getRight().toMap(SEPARATOR));
}
return HttpResponse.ok200()
.withBody(applyTemplate(contactListView, scopes))
.toPromise();
}))
.build();
}
- Here we provide an
AsyncServlet
, which receivesHttpRequests
from clients, createsHttpResponses
depending on 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 HTTPPOST
method that adds or declines adding new users. We will process this request parsing with the help of the aforementioned HTTPDecoder
by usingdecode(request)
method:
Either<Contact, DecodeErrors> decodedUser = CONTACT_DECODER.decode(request);
- Either represents a value of two possible data types (
Contact
,DecodeErrors
). Either is either Left(Contact
) or Right(DecodeErrors). In order to determine whether a parse was successful or not, we check it's value by using theisLeft()
andisRight()
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);
}
Running the application
If you want to run the example, you need to clone ActiveJ from GitHub and import it as a Maven project. Check out branch v6.0-beta2. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).
Then open HttpDecoderExample
class, which is located at activej/examples/tutorials/decoder
and run its main()
method. Open your favourite browser and go to localhost:8080