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
Contactrepresentation, 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
ArrayListto store theContactobjects.ContactDAOinterface 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
AsyncServletwill be used as a controller. We will also addRoutingServletfor 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 receivesHttpRequestsfrom clients, createsHttpResponsesdepending on route path and sends it. - Inside the
RoutingServlettwo 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 HTTPPOSTmethod that adds or declines adding new users. We will process this request parsing with the help of the aforementioned HTTPDecoderby 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