Form Validation Using HTTP Decoder

Introduction

In this example we created an async servlet that adds contacts to the list, parse requests and process form validation with the help of HttpDecoder. You can find full example sources on GitHub.

Here we will consider only HttpDecoderExample class with AsyncServlet as it contains DataKernel-specific features.

Consider this example as a concise presentation of 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 example, we will use an ArrayList to store the Contact 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 add RoutingServlet to determine respond to a particular endpoint.
  • HttpDecoder provides you with tools for parsing requests.

Creating HttpDecoderExample Class

Let’s create HttpDecoderExample class which extends HttpServerLauncher. HttpServerLauncher will take care of the lifecycle of our server, will start and stop needed services. Next, provide two custom parsers which will be used for validation - ADDRESS_DECODER and CONTACT_DECODER:

public final class HttpDecoderExample extends HttpServerLauncher {
	private final static String SEPARATOR = "-";

	private final static Decoder<Address> ADDRESS_DECODER = Decoder.of(Address::new,
			ofPost("title", "")
					.validate(param -> !param.isEmpty(), "Title cannot be empty")
	);

	private final static 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 be greater than 18"),
			ADDRESS_DECODER.withId("contact-address")
	);

Also, we need to create applyTemplate(Mustache mustache, Map<String, Object> scopes) method, which will 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();
}

And 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(ContactDAO contactDAO) {
	Mustache contactListView = new DefaultMustacheFactory().compile("static/contactList.html");
	return RoutingServlet.create()
			.map("/", request -> Promise.of(
					HttpResponse.ok200()
							.withBody(applyTemplate(contactListView, map("contacts", contactDAO.list())))))
			.map(POST, "/add", AsyncServletDecorator.loadBody()
					.serve(request -> {
						//[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 = map("contacts", contactDAO.list());
						if (decodedUser.isRight()) {
							scopes.put("errors", decodedUser.getRight().toMap(SEPARATOR));
						}
						return Promise.of(HttpResponse.ok200()
								.withBody(applyTemplate(contactListView, scopes)));
					}));
}
  • Here we provide an AsyncServlet, which receives HttpRequest from clients, creates HttpResponse depending on route path and sends it.
  • Inside the RoutingServlet two route paths are defined. First one matches requests to the root route "/" - it simply displays a contact list. The second one, "/add" - is an HTTP POST method which adds or dismisses new users. We will process this request parsing with the help of aforementioned HttpDecoder, using decode(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 or Right. We can check if Either contain only Left(Contact) or Right(DecodeErrors) using 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);
}

Running the application

If you want to run the example, you need to clone DataKernel and import it as a Maven project. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).

Then open HttpDecoderExample class, which is located at datakernel -> examples -> tutorials -> decoder and run its main() method. Open your favourite browser and go to localhost:8080.