Jakarta EE: Generation IV - a new hope

Otavio Santana
Developer Relations
30 Jul 2019

Java Enterprise Edition (Java EE) is an umbrella that holds specifications and APIs with enterprise features like distributed computing and web services. Widely used in Java, Java EE runs on reference runtimes that can be microservices or application servers that handle transactions, security, scalability, concurrency, and management for the components it’s deploying.

Now that Enterprise Java has been standardized under the Eclipse Foundation—with the brand-new name Jakarta EE—there’s a new hope for Enterprise Java. This post will show a similar solution to Spring MVC with MongoDB on the Jakarta EE community side, with full support on Platform.sh.

For this project, we’ll need some assistance. To triumph over the imperial forces, these three projects can help us:

  • Eclipse Krazo. An implementation of action-based MVC specified by MVC 1.0 (JSR-371). Eclipse Krazo builds on top of JAX-RS and currently contains support for RESTEasy, Jersey, and CXF with a well-defined SPI for other implementations.
  • Jakarta NoSQL. A framework to help developers create enterprise-grade applications using Java and NoSQL technologies. Jakarta NoSQL enables devs to create scalable applications while maintaining low coupling with the underlying NoSQL technology.
  • Bean Validation, JSR 380. A specification that creates its own annotations and validation, and ensures that the properties of a Class match specific criteria, using annotations such as @NotNull, @Min, and @Max.

Show me the force code

The first step, as usual, is to create a Maven project, where we define the dependencies in the pom.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
     	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>org.superbiz</groupId>
	<artifactId>mvc-cxf</artifactId>
	<packaging>war</packaging>

	<name>OpenEJB :: Examples :: MVC (CXF-based)</name>
	<description>OpenEJB :: Web Examples :: MVC 1.0 - Jakarta NoSQL</description>
	<version>0.0.1-SNAPSHOT</version>
	<url>http://tomee.apache.org</url>

	<properties>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    	<tomee.version>8.0.0-M3</tomee.version>
    	<version.krazo>1.0.0-SNAPSHOT</version.krazo>
    	<jnosql.version>0.1.0-SNAPSHOT</jnosql.version>
    	<http.port>8888</http.port>
	</properties>

	<build>
    	<finalName>ROOT</finalName>
    	<plugins>
        	<plugin>
            	<groupId>org.apache.maven.plugins</groupId>
            	<artifactId>maven-compiler-plugin</artifactId>
            	<version>3.5.1</version>
            	<configuration>
                	<source>1.8</source>
                	<target>1.8</target>
            	</configuration>
        	</plugin>
        	<plugin>
            	<groupId>org.apache.maven.plugins</groupId>
            	<artifactId>maven-war-plugin</artifactId>
            	<version>3.2.2</version>
            	<configuration>
                	<failOnMissingWebXml>false</failOnMissingWebXml>
                	<packagingExcludes>pom.xml</packagingExcludes>
            	</configuration>
        	</plugin>
        	<plugin>
            	<groupId>org.apache.tomee.maven</groupId>
            	<artifactId>tomee-maven-plugin</artifactId>
            	<version>${tomee.version}</version>
            	<configuration>
                	<tomeeHttpPort>${http.port}</tomeeHttpPort>
            	</configuration>
        	</plugin>
    	</plugins>
	</build>

	<repositories>
    	<repository>
        	<id>oss.sonatype.org-snapshot</id>
        	<url>http://oss.sonatype.org/content/repositories/snapshots</url>
        	<releases>
            	<enabled>false</enabled>
        	</releases>
        	<snapshots>
            	<enabled>true</enabled>
        	</snapshots>
    	</repository>
	</repositories>

	<dependencies>
    	<dependency>
        	<groupId>jakarta.enterprise</groupId>
        	<artifactId>jakarta.enterprise.cdi-api</artifactId>
        	<version>2.0.1</version>
        	<scope>provided</scope>
    	</dependency>
    	<dependency>
        	<groupId>jakarta.ws.rs</groupId>
        	<artifactId>jakarta.ws.rs-api</artifactId>
        	<version>2.1.5</version>
        	<scope>provided</scope>
    	</dependency>
    	<dependency>
        	<groupId>jakarta.validation</groupId>
        	<artifactId>jakarta.validation-api</artifactId>
        	<version>2.0.1</version>
        	<scope>provided</scope>
    	</dependency>

    	<dependency>
        	<groupId>sh.platform</groupId>
        	<artifactId>config</artifactId>
        	<version>2.2.2-SNAPSHOT</version>
    	</dependency>

    	<!--Eclipse JNoSQL-->
    	<dependency>
        	<groupId>org.jnosql.artemis</groupId>
        	<artifactId>artemis-document</artifactId>
        	<version>${jnosql.version}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.jnosql.diana</groupId>
        	<artifactId>mongodb-driver</artifactId>
        	<version>${jnosql.version}</version>
    	</dependency>

    	<!-- MVC 1.0(JSR 371) -->
    	<dependency>
        	<groupId>org.eclipse.krazo</groupId>
        	<artifactId>krazo-core</artifactId>
        	<version>${version.krazo}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.eclipse.krazo</groupId>
        	<artifactId>krazo-cxf</artifactId>
        	<version>${version.krazo}</version>
    	</dependency>
    	<dependency>
        	<groupId>javax.servlet</groupId>
        	<artifactId>jstl</artifactId>
        	<version>1.2</version>
    	</dependency>

	</dependencies>
</project>

One crucial point to remember is that with the Jakarta EE specification, we can use several vendors without impacting the application. In this tutorial, we’ll use Apache TomEE.

The next step is to define the user model. To make this all smoother, this user just has country, state, name, age, JUG, and description tags as information. So, two classes:

  • The Person class: name, age, JUG, and any description
  • The Address class: country and state

As with any simple register page, the user information comes through in a page form, so the MVC frameworks come with the FormParam annotation. This way, the developer can connect the form with any input field once the annotation matches the input tag exactly.

The MongoDB integration with the model is easy, too; JNoSQL has annotations that look like those of the JPA, so the developer defines the Entity, the Column, and the ID using the Entity, Column, and Id annotations, respectively.

One difference with MongoDB is that we can store the Address class information as a field instead of making it a relationship, as we usually do with SQL technology. So, we’ll use the Address as a subdocument of the Person entity, which is faster because it reduces the number of joins and is more natural to query.

@Entity
public class Person {

	@Id
	@FormParam("id")
	@Convert(ObjectIdConverter.class)
	private String id;

	@FormParam("name")
	@NotEmpty(message = "can not be empty")
	@Size(min = 1, max = 20)
	@MvcBinding
	@Column
	private String name;

	@FormParam("age")
	@MvcBinding
	@Min(18)
	@Column
	private int age;

	@BeanParam
	@Valid
	@Column
	private Address address;

	@FormParam("server")
	@NotNull
	@MvcBinding
	@Column
	private String server;

	@FormParam("description")
	@NotEmpty(message = "can not be empty")
	@MvcBinding
	@Size(min = 10)
	@Column
	private String description;
}

@Entity
public class Address {

	@FormParam("country")
	@NotEmpty(message = "can not be empty")
	@MvcBinding
	@Column
	private String country;

	@FormParam("state")
	@NotEmpty(message = "can not be empty")
	@MvcBinding
	@Column
	private String state;
}

The repository is a DDD pattern that acts as a mediator between the domain and data mapping layers using a collection-like interface for accessing domain objects. To create a Person repository, we need an interface that extends Repository. That’s all; JNoSQL will handle the implementation for you.

Here’s the method with a query whose whole point is to let the Java developer create any method using the JNoSQL convention. Then, the framework will implement it.

public interface PersonRepository extends Repository<Person, String> {

	Optional<Person> findById(Long id);

	List<Person> findAll();
}

The controller is the bridge to connect the view and the model. Models is a map of the name-to-model instances used by ViewEngine to process a view. Viewable is an abstraction that encapsulates information about a view. The other points are clear enough. Use the GET annotation to define access and POST to define the PATH that establishes a URL path.

import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.View;
import javax.mvc.binding.BindingResult;
import javax.validation.Valid;
import javax.validation.executable.ExecutableType;
import javax.validation.executable.ValidateOnExecution;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;

import org.eclipse.krazo.engine.Viewable;


@Controller
@Path("mvc")
public class PersonController {

	private static final Supplier<WebApplicationException> NOT_FOUND_EXCEPTION = () -> new WebApplicationException(NOT_FOUND);

	@Inject
	private Models models;

	@Inject
	private Messages message;

	@Inject
	private Errors erros;

	@Inject
	private BindingResult bindingResult;

	@Inject
	private PersonRepository repository;

	@GET
	@Path("new")
	public Viewable newElement() {
    	this.models.put("countries", getCountries());
    	return new Viewable("insert.jsp");
	}

	@GET
	@Path("show")
	@View("list.jsp")
	public void list() {
    	this.models.put("list", repository.findAll());
	}

	@POST
	@Path("add")
	@ValidateOnExecution(type = ExecutableType.NONE)
	public String add(@Valid @BeanParam Person person) {
    	if (bindingResult.isFailed()) {

        	this.getErrors();
        	this.models.put("countries", getCountries());
        	this.models.put("person", person);
        	return "insert.jsp";

    	}
    	repository.save(person);
    	message.setMessageRedirect("The " + person.getName() + " was successfully registered ! ");
    	return "redirect:mvc/show";
	}

	@POST
	@Path("update")
	@ValidateOnExecution(type = ExecutableType.NONE)
	public String update(@Valid @BeanParam Person person) {
    	if (bindingResult.isFailed()) {

        	this.getErros();
        	this.models.put("countries", getCountries());
        	this.models.put("person", person);
        	return "change.jsp";

    	}
    	repository.save(person);
    	message.setMessageRedirect("The " + person.getName() + " was changed successfully ! ");
    	return "redirect:mvc/show";
	}

	@GET
	@Path("update/{id}")
	public Viewable update(@PathParam("id") String id) {

    	Optional<Person> person = repository.findById(id);
    	this.models.put("person", person.orElseThrow(NOT_FOUND_EXCEPTION));
    	this.models.put("countries", getCountries());
    	return new Viewable("change.jsp", models);
	}

	@GET
	@Path("remove/{id}")
	public String delete(@PathParam("id") String id) {
    	Optional<Person> person = repository.findById(id);
    	repository.deleteById(person.map(Person::getId).orElseThrow(NOT_FOUND_EXCEPTION));
    	message.setMessageRedirect("The register was successfully Excluded ! ");
    	return "redirect:mvc/show";
	}

	private String getCountryName(String country) {
    	return new Locale(country, country).getDisplayCountry(Locale.ENGLISH);
	}

	private List<String> getCountries() {
    	return Arrays.stream(Locale.getISOCountries())
                 	.map(country -> getCountryName(country))
                 	.sorted((a, b) -> a.compareTo(b))
                 	.collect(Collectors.toList());
	}

	private void getErros() {
    	erros.setErrors(bindingResult.getAllErrors()
                                 	.stream()
                                 	.collect(toList()));
	}
}

@Named("message")
@RedirectScoped
public class Messages implements Serializable {

	private static final long serialVersionUID = 1L;

	private String messageRedirect;

	public String getMessageRedirect() {
    	return messageRedirect;
	}

	public void setMessageRedirect(String messageRedirect) {
    	this.messageRedirect = messageRedirect;
	}
}

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import javax.mvc.binding.ParamError;

@Named("error")
@RequestScoped
public class Errors {

	private List<ParamError> errors = new ArrayList<>();

	public void setErrors(List<ParamError> messages) {
    	this.errors = messages;
	}

	public String getErrors() {
    	return errors.stream()
                 	.map(ParamError::getMessage)
                 	.collect(Collectors.joining("<br>"));
	}

	public String getMessage(String param) {
    	return errors.stream()
                 	.filter(v -> v.getParamName().equals(param))
                 	.map(ParamError::getMessage)
                 	.findFirst()
                 	.orElse("");
	}
}

The view/visualization layer will work with the classic JSP pages, which are very popular in the Java world. Note: there are a few view engines that accommodate several files such as asciidoc, HTML 5, peb, html, vm, and so on.

<!-- home.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>MVC 1.0 DEMO</title>
</head>
<body>
	<jsp:include page="/templates/menu.jsp"></jsp:include>

	<h1 align="center">Be welcome!</h1>

	<div align="center">
    	<img src="${pageContext.request.contextPath}/resources/images/tomee.png" class="img-responsive" />
	</div>
	<br/>
	<br/>
	<jsp:include page="/templates/footer.jsp"></jsp:include>
</body>
</html>
<!-- insert.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MVC 1.0 DEMO</title>
</head>
<body>

	<div class="container">
    	<c:if test="${error.errors.length() != 0}">
        	<div class="row">
            	<div class="col-md-12">
                	<p class="alert alert-danger">${error.errors}</p>
            	</div>
        	</div>
    	</c:if>
    	<div class="row">
        	<div class="col-md-4 col-md-offset-4">
            	<form action="add" method="post">
                	<h2>New register</h2>
                	<div class="form-group">
                    	<label for="name">Name:</label>
                    	<input id="name" name="name" class="form-control" autofocus value="${person.name}">
                    	<c:if test="${mvc.encoders.html(error.getMessage('name').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("name"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>
                	<div class="form-group">
                    	<label for="age">Age:</label>
                    	<input type="number" id="age" name="age" class="form-control" value="${person.age}">
                    	<c:if test="${mvc.encoders.html(error.getMessage('age').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("age"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>

                	<div class="form-group">
                    	<label for="state">State:</label>
                    	<input type="text" id="state" name="state" class="form-control" value="${person.address.state}">
                    	<c:if test="${mvc.encoders.html(error.getMessage('state').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("state"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>
                	<div class="form-group">
                    	<label for="state">Application Server:</label> <br/>
                    	<label class="radio-inline">
                    	<input type="radio" name="server" value="TomEE" class="form-check-input"/>TomEE</label>
                    	<label class="radio-inline">
                    	<input type="radio" name="server" value="Wildfly" class="form-check-input"/>Wildfly</label>
                    	<label class="radio-inline">
                    	<input type="radio" name="server" value="Payara" class="form-check-input"/>Payara</label>

                    	<c:if test="${mvc.encoders.html(error.getMessage('server').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("server"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>
                	<div class="form-group">
                    	<label for="country">Country:</label>
                    	<select
                        	id="country" name="country" class="form-control">
                        	<option value="${person.address.country}">${person.address.country}</option>
                        	<c:forEach var="countries" items="${countries}">
                            	<option>${countries}</option>
                        	</c:forEach>
                    	</select>

                    	<c:if test="${mvc.encoders.html(error.getMessage('country').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("country"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>
                	<div class="form-group">
                    	<label for="description">Description:</label>
                    	<textarea type="" id="description" name="description" class="form-control">${person.description}</textarea>
                    	<c:if test="${mvc.encoders.html(error.getMessage('description').length() != 0)}">
                        	<div class="row">
                            	<div class="col-md-12">
                                	<p class="alert alert-danger">${mvc.encoders.html(error.getMessage("description"))}</p>
                            	</div>
                        	</div>
                    	</c:if>
                	</div>
                	<button class="btn btn-primary btn-block" type="submit">Register</button>
            	</form>
        	</div>
    	</div>
	</div>
	<br />
	<br />
	<jsp:include page="/templates/footer.jsp"></jsp:include>
</body>
</html>

Platform.sh structure

The Java application is ready to go! The next step is to set the Platform.sh files required to manage and deploy the application. In our first Java post, we took a deep dive into how each of these files configure the necessary containers in the virtual cluster:

  • One Router (.platform/routes.yaml). Platform.sh allows you to define the routes.
  • Zero or more service containers (.platform/services.yaml). Platform.sh allows you to completely define and configure the topology and services you want to use on your project.
  • One or more application containers (.platform.app.yaml). You control your application and the way it will be built and deployed on Platform.sh via a single configuration file.

The file that will change in this post is the services file, which allows you to define a database, search engine, cache, and so on. In this project, we’ll set MongoDB instead of MySQL.

database:
  type: mongodb:3.6
  disk: 1024

Platform.sh and Java

For the database, we need to define where the connection comes from. Platform.sh provides connection credentials to services using the PLATFORM_RELATIONSHIPS environmental variable.

import com.mongodb.MongoClient;
import jakarta.nosql.document.DocumentCollectionManager;
import org.jnosql.diana.mongodb.document.MongoDBDocumentCollectionManagerFactory;
import org.jnosql.diana.mongodb.document.MongoDBDocumentConfiguration;
import sh.platform.config.Config;
import sh.platform.config.MongoDB;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;

@ApplicationScoped
public class PersonProducer {

	@Produces
	@ApplicationScoped
	public DocumentCollectionManager getDocumentCollectionManager() {
    	Config config = new Config();
    	final MongoDB mongo = config.getCredential("mongodb", MongoDB::new);
    	final MongoClient mongoClient = mongo.get();
    	MongoDBDocumentConfiguration configuration = new MongoDBDocumentConfiguration();
    	MongoDBDocumentCollectionManagerFactory factory = configuration.get(mongoClient);
    	return factory.get(mongo.getDatabase());
	}

	public void close(@Disposes DocumentCollectionManager manager) {
    	manager.close();
	}
}

The Java Config Reader library offers classes that will read the information from the variable and return an object that represents the services, like a DataSource for MySQL, PostgreSQL, MongoClient, Redis, or any other services you’ve defined in .platform/services.yaml!

The application is now ready, so it’s time to move it to Platform.sh using the following steps:

  • Create a new free trial account.
  • Sign up with a new user and password, or login using a current GitHub, Bitbucket, or Google account. If you use a third-party login, you’ll be able to set a password for your Platform.sh account later.
  • Add a new project by selecting “Create a blank project” from the template list after you have named it.

After this wizard, Platform.sh will provision the whole infrastructure for you, providing your project a remote Git repository. The Platform.sh Git-driven infrastructure means it will automatically manage everything your application needs to push it to the master remote repository. After you set up your SSH keys, you only need to write your code—including a few YAML files that specify your desired infrastructure—then commit it to Git and push.

git remote add platform <platform.sh@gitrepository>
git commit -m "Initial project"
git push -u platform master

Code pushed will create both the Java application and a MongoDB instance, and, when it’s done, will return an IP address to the service.

In this tutorial, we showed the new hope for Java Enterprise with Jakarta EE. We made an application using APIs from Java EE/Jakarta EE specification and put it on the cloud easily with Platform.sh. The Java community, including several organizations like the Eclipse Foundation, is tight-knit and represents a brave new technology throughout the galaxy.