DEV Community

valerialistratova
valerialistratova

Posted on • Updated on

MVC Web Application with ActiveJ

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>
Enter fullscreen mode Exit fullscreen mode

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 + '}';
    }
}
Enter fullscreen mode Exit fullscreen mode

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 + '\'' + '}';
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • An AsyncServlet will be used as a controller. We will also add RoutingServlet 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")
    );
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Next, let’s provide a ContactDAOImpl factory method:

@Provides
ContactDAO dao() {
    return new ContactDAOImpl();
}
Enter fullscreen mode Exit fullscreen mode

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));
                    }));
}
Enter fullscreen mode Exit fullscreen mode

Here we provide an AsyncServlet, which receives HttpRequests from clients, creates HttpResponses 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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)