This project contains a fully working application based on Java EE 7. Mostly based on Effective Java EE course by Adam Bien at:
https://vimeo.com/ondemand/effectivejavaee
The best way to work with Java EE is to start a Maven project based on an Archetype created by Adam Bien:
Set up a basic JavaEE ready application with Maven
mvn archetype:generate -Dfilter=com.airhacks:javaee7-essentials-archetypehttps://github.com/AdamBien/javaee7-essentials-archetype
Choose version 1.3 of the template, that includes file for JAX-RS Configuration (REST web services)
The application will be developed with BCE pattern and module definition will reflect the choice.
More information about the pattern can be found on Google and at:
https://www.youtube.com/watch?v=grJC6RFiB58
For package definition we can have the following convention:
<company name>.<application name>.<layer>.<component>.<type>
Because in our case we want to separate Presentation from Business Logic and Integration classes, in our case the layer part of the package path can be one of the following:
- presentation (layer)
- business (layer)
- integration (layer)
Our application can contain many components that have connections between each other, and every component is a Java Package that comprehend:
o--| B | C | E |
So, after the component part of the package, we should use one of the following to specify the component part type:
- Boundary
- Control
- Entity
boundary are all the classes that have all the interfaces for humans or other objects.
control this optional package can contain classes with some logic like validation, complex tasks that cannot stay in the boundary package, etc.
entity contains all the classes for persistence of entities
In our "DoIt" application, let's suppose we want to create a new component to manage the Reminders and we want to expose REST web services, we can define the package as follows:
- company name:
io.github.dinolupo - application name:
doit - layer name:
business - component name:
reminders - component layer type name:
boundary
- class:
TodosResource.java
To begin we add a very simple rest service to the class:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("todos")
public class TodosResource {
@GET
//@Produces(MediaType.APPLICATION_JSON)
public String hello(){
return "Hello, time is " + System.currentTimeMillis();
}
}Let's create a new project that is useful to test the main project.
We call it doit-st where st stands for System Test.
Create a simple Maven project with the following pom.xml:
pom.xml that contains JAX-RS dependencies to test the rest services, including JUnit and Matchers
<?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>io.github.dinolupo</groupId>
<artifactId>doit-st</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.23.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-processing</artifactId>
<version>2.23.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
</project>The test class is the following:
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
public class TodosResourceTest {
private Client client;
private WebTarget target;
@Before
public void setUp() throws Exception {
client = ClientBuilder.newClient();
target = client.target("http://localhost:8080/doit/api/todos");
}
@Test
public void fetchTodos() throws Exception {
Response response = target.request(MediaType.TEXT_PLAIN).get();
assertThat(response.getStatus(),is(200));
String payload = response.readEntity(String.class);
assertThat(payload, startsWith("Hello, time is"));
}
}Later on this can become a build job in a Jenkins pipeline to do continuous integration / deployment.
Instead of a String we should manage entities, so we create an Entity class:
package io.github.dinolupo.doit.business.reminders.entity;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ToDo {
private String caption;
private String description;
private int priority;
public ToDo(String caption, String description, int priority) {
this.caption = caption;
this.description = description;
this.priority = priority;
}
public ToDo() {
}
}modify the rest service as follows:
@GET
public ToDo hello(){
return new ToDo("Implement Rest Service", "modify the test accordingly", 100);
}and the test accordingly:
@Test
public void fetchTodos() throws Exception {
Response response = target.request(MediaType.APPLICATION_JSON).get();
assertThat(response.getStatus(),is(200));
JsonObject payload = response.readEntity(JsonObject.class);
assertThat(payload.getString("caption"), startsWith("Implement Rest Service"));
}In this step we create simple CRUD services and modify the test class accordingly.
modified methods of ToDoResource.java
@GET
@Path("{id}")
public ToDo find(@PathParam("id") long id){
return new ToDo("Implement Rest Service Endpoint id="+id, "modify the test accordingly", 100);
}
@DELETE
@Path("{id}")
public void delete(@PathParam("id") long id) {
System.out.printf("Deleted Object with id=%d\n", id);
}
@GET
public List<ToDo> all() {
List<ToDo> all = new ArrayList<>();
all.add(find(42));
return all;
}
@POST
public void save(ToDo todo) {
System.out.printf("Saved ToDo: %s\n", todo);
}we have used POST because later on we will use a technical key in the Entity. If you use a business key, a key with a meaning for the business, then use PUT so you can pass a key along with the client call. PUT is idempotent.
test class
@Test
public void crud() throws Exception {
// GET all
Response response = target.request(MediaType.APPLICATION_JSON).get();
assertThat(response.getStatusInfo(),is(Response.Status.OK));
JsonArray allTodos = response.readEntity(JsonArray.class);
assertFalse(allTodos.isEmpty());
JsonObject todo = allTodos.getJsonObject(0);
assertThat(todo.getString("caption"), startsWith("Implement Rest Service"));
// GET with id
JsonObject jsonObject = target.
path("42").
request(MediaType.APPLICATION_JSON).
get(JsonObject.class);
assertTrue(jsonObject.getString("caption").contains("42"));
Response deleteResponse = target.
path("42").
request(MediaType.APPLICATION_JSON)
.delete();
// DELETE
assertThat(deleteResponse.getStatusInfo(),is(Response.Status.NO_CONTENT));
}Mantaining all the business behaviours in the Rest resource class, can be complicated when you want to create unit tests. So you shoud put all the behaviours in a separate class and inject an instance into the rest resource:
move all the behaviours into a EJB manager class:
@Stateless
public class TodosManager {
public ToDo findById(long id) {
return new ToDo("Implement Rest Service Endpoint id="+id, "modify the test accordingly", 100);
}
public void delete(long id) {
System.out.printf("Deleted Object with id=%d\n", id);
}
public List<ToDo> findAll() {
List<ToDo> all = new ArrayList<>();
all.add(findById(42));
return all;
}
public void save(ToDo todo) {
System.out.printf("Saved ToDo: %s\n", todo);
}
}Inject the property into the resource class
@Stateless
@Path("todos")
public class TodosResource {
@Inject
TodosManager todosManager;
@GET
@Path("{id}")
public ToDo find(@PathParam("id") long id){
return todosManager.findById(id);
}
@DELETE
@Path("{id}")
public void delete(@PathParam("id") long id) {
todosManager.delete(id);
}
@GET
public List<ToDo> all() {
return todosManager.findAll();
}
@POST
public void save(ToDo todo) {
todosManager.save(todo);
}
}After creating the TodosManager class, that is a protocol neutral boundary class, we can use it to save data with JPA.
-
Annotate the ToDo class with
@Entity -
Annotate the
idproperty with@Idand@GeneratedValuesince it is a technical key -
Create a persistence unit, putting the
META-INF/persistance.xmlin the war package:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="production" transaction-type="JTA">
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>- Change
TodosManagerto use JPA as follows:
inject the EntityManager
@Stateless
public class TodosManager {
@PersistenceContext
EntityManager entityManager;
public ToDo findById(long id) {
return entityManager.find(ToDo.class, id);
}
public void delete(long id) {
ToDo reference = entityManager.getReference(ToDo.class, id);
entityManager.remove(reference);
}
public List<ToDo> findAll() {
return entityManager.createNamedQuery(ToDo.findAll, ToDo.class).getResultList();
}
public void save(ToDo todo) {
entityManager.merge(todo);
}
}As stated by the HTTP RFC at https://tools.ietf.org/html/rfc2616#page-54 that says:
If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header
we should return 201 and a Location header instead of void (No Content). So let's adjust the POST method as follows:
-
generate getters in the
ToDobean -
change
savemethod such that it returns theToDosaved object
@POST
public Response save(ToDo todo, @Context UriInfo uriInfo) {
ToDo savedObject = todosManager.save(todo);
long id = savedObject.getId();
URI uri = uriInfo.getAbsolutePathBuilder().path("/" + id).build();
Response response = Response.created(uri).build();
return response;
}- Adjust the test accordingly:
@Test
public void crud() throws Exception {
// create an object with POST
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
JsonObject todoToCreate = jsonObjectBuilder
.add("caption", "Implement Rest Service with JPA")
.add("description", "Connect a JPA Entity Manager")
.add("priority", 100).build();
Response postResponse = target.request().post(Entity.json(todoToCreate));
assertThat(postResponse.getStatusInfo(),is(Response.Status.CREATED));
String location = postResponse.getHeaderString("Location");
System.out.printf("location = %s\n", location);
// GET with id, using the location returned before
JsonObject jsonObject = client.target(location)
.request(MediaType.APPLICATION_JSON)
.get(JsonObject.class);
assertTrue(jsonObject.getString("caption").contains("Implement Rest Service with JPA"));
// GET all
Response response = target.request(MediaType.APPLICATION_JSON).get();
assertThat(response.getStatusInfo(),is(Response.Status.OK));
JsonArray allTodos = response.readEntity(JsonArray.class);
assertFalse(allTodos.isEmpty());
JsonObject todo = allTodos.getJsonObject(0);
assertThat(todo.getString("caption"), startsWith("Implement Rest Service"));
System.out.println(todo);
// DELETE
Response deleteResponse = target.
path("42").
request(MediaType.APPLICATION_JSON)
.delete();
assertThat(deleteResponse.getStatusInfo(),is(Response.Status.NO_CONTENT));
}- paying attention to the fact that the delete method could raise an unchecked exception
EntityNotFoundExceptionwhen you try to delete a proxied non-existent object, so we adjust like the following:
public void delete(long id) {
try {
ToDo reference = entityManager.getReference(ToDo.class, id);
entityManager.remove(reference);
} catch (EntityNotFoundException ex) {
// we want to remove it, so do not care of the exception
}
}Let's add a PUT method to permit an update of an entity:
@PUT
@Path("{id}")
public void update(@PathParam("id") long id, ToDo todo) {
todo.setId(id);
todosManager.save(todo);
}as you can see, we used the id parameter to set the Entity id.
Here we add a new rest service that is needed to update only a field. We add a field "done", that represent if an activity is done, to the entity and we want to update only that field.
We could do it using the PUT method, but it can be dangerous because we want only to update a field, so we introduce a sub-resource that can be used to update only some fields.
-
add the
boolean done;field to the Entity -
Implement the test (test first, TDD)
public void crud() throws Exception {
...
// update status ("done" field) with a subresource PUT method
JsonObjectBuilder statusBuilder = Json.createObjectBuilder();
JsonObject status = statusBuilder
.add("done", true)
.build();
client.target(location)
.path("status")
.request(MediaType.APPLICATION_JSON)
.put(Entity.json(status));
// verify that status is updated
// find again with GET {id}
updatedTodo = client.target(location)
.request(MediaType.APPLICATION_JSON)
.get(JsonObject.class);
assertThat(updatedTodo.getBoolean("done"), is(true));
...
} - Implement the method to satisfy the test
add the following business method to
TodoManager.java
public ToDo updateStatus(long id, boolean done) {
ToDo todo = findById(id);
todo.setDone(done);
return todo;
}add the following rest service to
TodosResource.java
@PUT
@Path("{id}/status")
public ToDo statusUpdate(@PathParam("id") long id, JsonObject status) {
boolean isDone = status.getBoolean("done");
return todosManager.updateStatus(id, isDone);
}We are going to show hot to deal with Exceptions in rest services.
In the previous example we could have two NullPointerException in the following code:
in
TodoManager.updateStatus(),todocan be null
todo.setDone(done);in
TodosResource.statusUpdate(),statuscan be null
boolean isDone = status.getBoolean("done");to test those cases, let's proceed to update the test class:
test the case of non existing object:
// update status on not existing object
JsonObjectBuilder notExistingBuilder = Json.createObjectBuilder();
status = notExistingBuilder
.add("done", true)
.build();
Response response = target.path("-42")
.path("status")
.request(MediaType.APPLICATION_JSON)
.put(Entity.json(status));
assertThat(response.getStatusInfo(), is(Response.Status.BAD_REQUEST));
assertFalse(response.getHeaderString("reason").isEmpty());adjust the code to pass the test:
@PUT
@Path("{id}/status")
public Response statusUpdate(@PathParam("id") long id, JsonObject status) {
boolean isDone = status.getBoolean("done");
ToDo todo = todosManager.updateStatus(id, isDone);
if (todo == null) {
return Response.status(Response.Status.BAD_REQUEST)
.header("reason","ToDo with id " + id + " does not exist.")
.build();
} else {
return Response.ok(todo).build();
}
}test the code of malformed json
// update with malformed status
JsonObjectBuilder malformedBuilder = Json.createObjectBuilder();
status = malformedBuilder
.add("something wrong", true)
.build();
response = client.target(location)
.path("status")
.request(MediaType.APPLICATION_JSON)
.put(Entity.json(status));
assertThat(response.getStatusInfo(), is(Response.Status.BAD_REQUEST));
assertFalse(response.getHeaderString("reason").isEmpty());correct the code accordingly
@PUT
@Path("{id}/status")
public Response statusUpdate(@PathParam("id") long id, JsonObject status) {
if (!status.containsKey("done")) {
return Response.status(Response.Status.BAD_REQUEST)
.header("reason","JSON does not contain required key 'done'")
.build();
}
boolean isDone = status.getBoolean("done");
ToDo todo = todosManager.updateStatus(id, isDone);
if (todo == null) {
return Response.status(Response.Status.BAD_REQUEST)
.header("reason","ToDo with id " + id + " does not exist.")
.build();
} else {
return Response.ok(todo).build();
}
}In this step we basically refator the TodosResource class creating a new TodoResource that manages only basic operation on a single ToDo instance, given the id parameter.
Let's see the class here:
public class TodoResource {
long id;
TodosManager todosManager;
public TodoResource(long id, TodosManager todosManager) {
this.id = id;
this.todosManager = todosManager;
}
@GET
public ToDo find(){
return todosManager.findById(id);
}
@DELETE
public void delete() {
todosManager.delete(id);
}
@PUT
public void update(ToDo todo) {
todo.setId(id);
todosManager.save(todo);
}
@PUT
@Path("/status")
public Response statusUpdate(JsonObject status) {
if (!status.containsKey("done")) {
return Response.status(Response.Status.BAD_REQUEST)
.header("reason","JSON does not contain required key 'done'")
.build();
}
boolean isDone = status.getBoolean("done");
ToDo todo = todosManager.updateStatus(id, isDone);
if (todo == null) {
return Response.status(Response.Status.BAD_REQUEST)
.header("reason","ToDo with id " + id + " does not exist.")
.build();
} else {
return Response.ok(todo).build();
}
}
}We have moved all the {id} related operations, leaving only the findAll and the save method.
The new {id} path method in the original TodosResource class will become:
@Path("{id}")
public TodoResource find(@PathParam("id") long id){
return new TodoResource(id, todosManager);
}When executing a path like /todos/{id} the JAX-RS engine will enter the TodosResource and will hit the previous modified method, so it will return a new TodoResource the will execute one of the provided HTTP verb present in that class.
If you want to update a single resource from different clients without risking to overwrite previous values, we could implement an optimistick lock.
To implement optimistick locking we have to introduce a field into the ToDo bean:
@Version
private long version;The JPA manager will update that field every time an object is updated. If you try to update an object with the same @Version field multiple times, you will get an Exception and a Rollback:
Hibernate will generate a org.hibernate.StaleObjectStateException
that JPA will catch in a javax.persistence.OptimisticLockException
that follows in a javax.ejb.EJBException that generates a Rollback of the entire transaction.
To verify the problem, let's try to update a single resource two times in the test case, without reading again the bean, the version isn't changed and there is a 500 http response (the EJB exception generates it).
To return a more meaningful http response code we could map exceptions using an ExceptionMapper:
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class EJBExceptionMapper implements ExceptionMapper<EJBException>{
@Override
public Response toResponse(EJBException exception) {
Throwable cause = exception.getCause();
Response unknownError = Response.serverError()
.header("cause", exception.toString())
.build();
if (cause == null) {
return unknownError;
}
if (cause instanceof OptimisticLockException) {
return Response.status(Response.Status.CONFLICT)
.header("cause", "conflict occurred: " + cause)
.build();
}
return unknownError;
}
}Conflict is the correct status code to return in this case as stated by the http RFC.
To demonstrate validation we put the following in the ToDo class:
@NotNull
@Size(min = 1, max = 256)
private String caption;and to validate the bean before it reaches the persistence context add @Valid to the rest method:
...
@POST
public Response save(@Valid ToDo todo, @Context UriInfo uriInfo) {
...
}Test both cases:
@Test
public void createNotValidTodo() {
// create an object with POST with missing "caption" field
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
JsonObject todoToCreate = jsonObjectBuilder
.add("description", "Connect a JPA Entity Manager")
.add("priority", 100).build();
Response postResponse = target.request().post(Entity.json(todoToCreate));
assertThat(postResponse.getStatusInfo(),is(Response.Status.BAD_REQUEST));
}
@Test
public void createValidTodo() {
// create an object with POST with missing "caption" field
JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
JsonObject todoToCreate = jsonObjectBuilder
.add("caption", "valid caption")
.add("description", "Connect a JPA Entity Manager")
.add("priority", 100).build();
Response postResponse = target.request().post(Entity.json(todoToCreate));
assertThat(postResponse.getStatusInfo(),is(Response.Status.CREATED));
}Let's suppose we want to introduce a more complicated validation, for example we want that a ToDo has to have a description not null if a priority is higher than 10. We are going to show one method to do that:
- Let's implement an interface in the business layer that is called
ValidEntity
public interface ValidEntity {
public boolean isValid();
}- Our
ToDobean should implement the interface:
public class ToDo implements ValidEntity {
...
@Override
public boolean isValid() {
return (priority > 10 && description != null) || priority <= 10;
}
}- We can create unit tests for the bean like the following (unit tests can go in the same business project
doit, while we mantain integration tests in the otherdoit-stproject):
public class ToDoTest {
@Test
public void valid() {
ToDo toDo = new ToDo("", "description", 11);
assertTrue(toDo.isValid());
}
@Test
public void notValid() {
ToDo toDo = new ToDo("", null, 11);
assertFalse(toDo.isValid());
}
}- Now we can create a reusable ConstraintValidator class that will be used with an Annotation:
CrossCheckConstraintValidator- put this in the business package
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CrossCheckConstraintValidator implements ConstraintValidator<CrossCheck, ValidEntity> {
@Override
public void initialize(CrossCheck constraintAnnotation) {
}
@Override
public boolean isValid(ValidEntity entity, ConstraintValidatorContext context) {
return entity.isValid();
}
}
CrossCheckannotation - put this in the business package
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = CrossCheckConstraintValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CrossCheck {
String message() default "Cross check failed!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}- Now we can use the annotation to validate our
ToDobean:
put
CrossCheckannotation to the bean that you want to validate during a transaction
...
@CrossCheck
public class ToDo implements ValidEntity {...}- Deploy and add an integration test
We add an Interceptor to log all the calls to the methods of the TodosManager class
- Create an interceptor class:
create the interceptor class in the business package
package io.github.dinolupo.doit.business;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
public class BoundaryLogger {
@AroundInvoke
public Object logCall(InvocationContext invocationContext) throws Exception {
System.out.printf("--> %s\n", invocationContext.getMethod());
return invocationContext.proceed();
}
}- add the annotation to the manager class
adding the annotation
...
@Interceptors(BoundaryLogger.class)
public class TodosManager {...}- instead of the System.out line in the interceptor, we could be smarter and add a functional interface (we choose later what concrete class to inject) to collect all the logging
in the business package, create a new functional interface that specify what to do with log messages
@FunctionalInterface
public interface LogSink {
void log(String msg);
}- change the
BoundaryLoggeras follows:
injecting the
LogSinkand use it
public class BoundaryLogger {
@Inject
LogSink LOG;
@AroundInvoke
public Object logCall(InvocationContext invocationContext) throws Exception {
LOG.log("--> " + invocationContext.getMethod());
return invocationContext.proceed();
}
}- Now to use that
LogSinkinterface, someone has to produce a concrete class:
create a
LogSinkProducerthat returns the JDK log info method
package io.github.dinolupo.doit.business;
import io.github.dinolupo.doit.business.LogSink;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import java.util.logging.Logger;
public class LogSinkProducer {
@Produces
public LogSink produce(InjectionPoint injectionPoint) {
Class<?> injectionTarget = injectionPoint.getMember().getDeclaringClass();
return Logger.getLogger(injectionTarget.getName())::info;
}
}- If the logging is a bunsiness requirement, than we can put the logging classes in a business package
refactor to put all the three logging classes into the correct package
package io.github.dinolupo.doit.business.logging.boundary;
Let's capture the performance of the method:
- Let's create a class that is needed for our monitoring purpose
create a
CallEventclass in thebusiness.monitoring.entitypackage
package io.github.dinolupo.doit.business.monitoring.entity;
public class CallEvent {
private String methodName;
private long duration;
public CallEvent(String methodName, long duration) {
this.methodName = methodName;
this.duration = duration;
}
public String getMethodName() {
return methodName;
}
public long getDuration() {
return duration;
}
@Override
public String toString() {
return "CallEvent{" +
"methodName='" + methodName + '\'' +
", duration=" + duration +
'}';
}
}- Let's modify the BoundaryLogger to fire an Enterprise Event of type CallEvent
remove the previous
@Injectand modify the method to send an Enterpise Event with the duration of the method call
public class BoundaryLogger {
@Inject
Event<CallEvent> monitoring;
@AroundInvoke
public Object logCall(InvocationContext invocationContext) throws Exception {
long start = System.currentTimeMillis();
try {
return invocationContext.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
monitoring.fire(new CallEvent(invocationContext.getMethod().getName(),duration));
}
}
}- Create a class that captures and processes that Event
use
@Observesto process the Event, inject again the LogSink created before to show the output
package io.github.dinolupo.doit.business.monitoring.boundary;
import io.github.dinolupo.doit.business.logging.boundary.LogSink;
import io.github.dinolupo.doit.business.monitoring.entity.CallEvent;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class MonitoringSink {
@Inject
LogSink LOG;
public void onCallEvent(@Observes CallEvent callEvent) {
LOG.log(callEvent.toString());
}
}We want to expose the Events via a Rest interface:
- Add a structure to hold the Events and add every Event to it. For now we don't care about the the OutOfMemory Exception, we cover this topic later ;)
add a field
CopyOnWriteArrayList<CallEvent>and add every event to this collection
public class MonitoringSink {
@Inject
private LogSink LOG;
private CopyOnWriteArrayList<CallEvent> recentEvents;
@PostConstruct
public void postConstruct() {
recentEvents = new CopyOnWriteArrayList<>();
}
public void onCallEvent(@Observes CallEvent callEvent) {
LOG.log(callEvent.toString());
recentEvents.add(callEvent);
}
public List<CallEvent> getRecentEvents() {
return recentEvents;
}
}- modify the
CallEventto add a default constructor needed by JAX-RS and define the annotations to serialize the object:
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class CallEvent {
...
public CallEvent() {
}
...
}- In the
monitoring.boundarypackage add a class to expose the list of events
new Resource to expose events via REST
@Stateless
@Path("boundary-invocations")
public class BoundaryInvocationsResource {
@Inject
MonitoringSink monitoringSink;
@GET
public List<CallEvent> expose() {
return monitoringSink.getRecentEvents();
}
}- run and then execute the tests to add some data. Then retrieve it with the following:
curl command
% curl -i -H "accept: application/json" http://localhost:8080/doit/api/boundary-invocations
HTTP/1.1 200 OK
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 423
Date: Sun, 17 Jul 2016 12:45:52 GMTshell output
[ {"methodName":"save","duration":118},
{"methodName":"findById","duration":14},
{"methodName":"save","duration":2},
{"methodName":"save","duration":3},
{"methodName":"findById","duration":1},
{"methodName":"updateStatus","duration":1},
{"methodName":"findById","duration":2},
{"methodName":"updateStatus","duration":1},
{"methodName":"findAll","duration":15},
{"methodName":"delete","duration":7},
{"methodName":"save","duration":3}
]- to be more flexible, you could change the return type of the
expose()method toJsonArrayand format information as you like.
We could expose basic statistics in the following way:
- create the business method to calculate the statistics
in the
MonitoringSinkclass add a method to expose aLongSummaryStatisticsobject
public class MonitoringSink {
...
public LongSummaryStatistics getStatistics() {
return recentEvents.stream().collect(Collectors.summarizingLong(CallEvent::getDuration));
}
...
}- Expose the statistics with a new Resource class:
create a
BoundaryStatisticsResourceclass that expose statistics with a JsonObject (no XML support when using a JsonObject)
package io.github.dinolupo.doit.business.monitoring.boundary;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.LongSummaryStatistics;
@Path("boundary-statistics")
public class BoundaryStatisticsResource {
@Inject
MonitoringSink monitoringSink;
@GET
public JsonObject get() {
LongSummaryStatistics statistics = monitoringSink.getStatistics();
JsonObject jsonStats = Json.createObjectBuilder()
.add("average-duration", statistics.getAverage())
.add("max-duration", statistics.getMax())
.add("min-duration", statistics.getMin())
.add("invocations-count", statistics.getCount())
.build();
return jsonStats;
}
}Statistics can be useful in an enterprise project when dealing with stress tests. Few project expose this type of statistics.
Let's create a presentation layer with JSF to show the application on the web:
- Create a web.xml file under the WEB-INF directory with the following content:
web.xmlconfiguration file to work with JSF
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>- Let's create a simple JSF page
index.xhtmlJSF web page
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h:form>
Caption: <h:inputText value="#{index.todo.caption}"/>
Description: <h:inputText value="#{index.todo.description}"/>
Priority: <h:inputText value="#{index.todo.priority}"/>
<h:commandButton value="Save" action="#{index.save}"/>
</h:form>
</h:body>
</html>-
We want to reuse the same business
ToDobean here, so let's add the setters to it, otherwise you will getPropertyNotWritableExceptiontrying to save it: -
create a Model class that works together with the JSF Page just created
Indexclass
package io.github.dinolupo.doit.presentation;
import io.github.dinolupo.doit.business.reminders.boundary.TodosManager;
import io.github.dinolupo.doit.business.reminders.entity.ToDo;
import javax.annotation.PostConstruct;
import javax.enterprise.inject.Model;
import javax.inject.Inject;
@Model
public class Index {
@Inject
TodosManager boundary;
ToDo todo;
@PostConstruct
public void init() {
todo = new ToDo();
}
public ToDo getTodo() {
return todo;
}
// JSF action
public Object save() {
this.boundary.save(todo);
// stay on the same page
return null;
}
}To try this example, firs fix the ToDo isValid() method adding the isEmpty() case as follows:
@Override
public boolean isValid() {
return (priority > 10 && description != null && !description.isEmpty())
|| priority <= 10;
}Validation on ToDo fields works out of the box with JSF, while Cross-Field validation with the CrossCheck annotation is not intercepted by JSF (because our jsf page bind fields and not the object) but only by JPA. So if you test the application with no description and priority > 10 you will get an Exception page.
To fix this, let's do the following steps:
- Add a utility method to the Managed Bean, that prints a validation message to the page:
public void showValidationError(String content) {
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_WARN, content, content);
FacesContext.getCurrentInstance().addMessage("", message);
}- Inject the
javax.validation.Validatorinto theIndexManaged Bean:
import javax.validation.Validator;
...
public class Index {
...
@Inject
Validator validator;
...
}- Adapt the
save()to verify validation violations:
// JSF action
public Object save() {
Set<ConstraintViolation<ToDo>> violations = validator.validate(todo);
for (ConstraintViolation<ToDo> violation : violations) {
showValidationError(violation.getMessage());
}
if (violations.isEmpty()) {
this.boundary.save(todo);
}
// stay on the same page
return null;
}Let's suppose we want to use HTML5 instead of xhtml markup.
As an example let's substitute the caption field with an HTML5 field:
Old JSP markup Caption
Caption: <h:inputText value="#{index.todo.caption}"/>New HTML5 Caption
Caption: <input jsf:id="caption"
type="text"
placeholder="Enter the caption"
value="#{index.todo.caption}"/>With JEE7 it is very simple to transform a JSF element into an HTML5 page.
This is a nice and pragmatic way to work with Designers because they release to programmers an HTML5 page and not a JSF page.
Let's change the UI to use primefaces.
- add Maven dependency
PrimeFaces URL: http://primefaces.org/downloads# at the bottom of the page
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>6.0</version>
</dependency> - Go to the Primefaces showcase on the web at: http://www.primefaces.org/showcase/
and find the suitable component to use, in our case is inputText
- Add the primefaces namespace to the .xhtml page
xmlns:p="http://primefaces.org/ui"
- change the
hwithpin elements that we want to change:
Description: <p:inputText value="#{index.todo.description}"/>
Priority: <p:inputText value="#{index.todo.priority}"/>What's an efficient way to internationalize a Java EE application built so far? Let's explain how it works.
First of all, all localized messages are put in property files. Those files are defined in the WEB-INF/faces-config.xml file.
In the faces-config.xml we have basically two types of property files (read this Stackoverflow question for a nice explanation):
<message-bundle><resource-bundle>
In our example application, we defined the following:
WEB-INF/faces-config.xml
<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
<application>
<resource-bundle>
<base-name>resources</base-name>
<var>i18n</var>
</resource-bundle>
<message-bundle>messages</message-bundle>
</application>
</faces-config>This means that we should have two files: one with name resources.properties and the other with name messages.properties. They are both in the src root, no package used here, so they are located in the folder main/resources of our project (this is a Maven Standard Directory)
The `<resource-bundle>` has to be used whenever you want to register a localized resource bundle which is available throughout the entire JSF application without the need to specify <f:loadBundle> in every single view.
resources.propertiescontent
index.caption=Caption
index.description=Description
index.priority=PriorityThe resource bundle have a <var> tag, that identifies the variable name that can be used in the JSF pages of our application.
For example let's suppose we want to internationalize the label of the description field, we put a outputLabel with the value referring to the property index.description in our file resources.properties.
We also have almost the same value for the label property of inputText:
<p:outputLabel for="description" value="#{i18n['index.description']}: "/>
<p:inputText id="description" value="#{index.todo.description}" label="#{i18n['index.description']}"/>The label will be used also by the validation messages generated by JSF (see description of message-bundle)
The <message-bundle> has to be used whenever you want to override JSF default warning/error messages which is been used by the JSF validation/conversion stuff. You can find keys of the default warning/error messages in chapter 2.5.2.4 of the JSF specification.
In our case, in messages.properties file we should put all the localized validation messages, so I decided to put here the following:
messages.propertiescontent
javax.faces.validator.BeanValidator.MESSAGE = {1}: {0}
validation.todo.crosscheckfailed=Cross Check Failed, but i18n works!javax.faces.validator.BeanValidator.MESSAGE is necessary because when bean annotated validators fails, the default message does not contain field information see here
To be consistent, we should i18n also the cross check validation messages:
- Use localized message key when using cross-check validator:
...
@CrossCheck(message = "validation.todo.crosscheckfailed")
public class ToDo implements ValidEntity {...}- Modify the message shown using FacesMessage reading a property from the message bundle:
...
public class Index {
...
public void showValidationError(String content) {
FacesContext context = FacesContext.getCurrentInstance();
String msgBundle = context.getApplication().getMessageBundle();
Locale locale = context.getViewRoot().getLocale();
ResourceBundle messageBundle = ResourceBundle.getBundle(msgBundle,locale);
FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_WARN,
messageBundle.getString(content), content);
context.addMessage(null, message);
}
...
}To add a table with PrimeFaces, let's do the following:
-
Choose the right component (remember that using Prime Faces or any other JSF component library is productive if you do not modify the behaviour of the components). Go to the PrimeFaces Showcase and let's suppose we have chosen DataList, the first one with an ordered list.
-
Take the code and add to our `index.xhtml' page:
<p:dataList id="reminders" value="#{index.toDos}" var="todo" type="ordered">
<f:facet name="header">
#{i18n['index.reminders.list']}
</f:facet>
#{todo.caption}, #{todo.description}, #{todo.priority}
</p:dataList>- Add the
remindersid of the table to theupdatecommand button property, in this way you update the list when adding new ToDos:
<p:commandButton value="Save" action="#{index.save}" update="messages, growl, reminders"/>Let's add some style to our JSF page:
- add a
panelGridwith a header, to better render our input form:
<p:panelGrid columns="2" cellpadding="5">
<f:facet name="header">
#{i18n['index.form.label']}
</f:facet>
<h:outputLabel for="caption" value="#{i18n['index.caption']}: "/>
<input jsf:id="caption"
type="text"
placeholder="Enter the caption"
value="#{index.todo.caption}"
label="#{i18n['index.caption']}"/>
<p:outputLabel for="description" value="#{i18n['index.description']}: "/>
<p:inputText id="description" value="#{index.todo.description}" label="#{i18n['index.description']}"/>
<p:outputLabel for="priority" value="#{i18n['index.priority']}: "/>
<p:inputText id="priority" value="#{index.todo.priority}" label="#{i18n['index.priority']}"/>
</p:panelGrid>- Before adding some style to HTML5 components, let's first we remove the prefix for field names using:
<h:form prependId="false">- We left an HTML5 input field (caption), so let's show how to add some style with the Bootstrap CSS Library, add
class="form-control"to the html5 control:
HTML5 input field with class parameter
<input jsf:id="caption"
type="text"
placeholder="Enter the caption"
value="#{index.todo.caption}"
label="#{i18n['index.caption']}"
class="form-control"/>Do not forget the label property because of the i18n validation messages. If label is missing then the value of the id property will be used.
- Add bootstrap library in the
headsection:
you can copy the text from the Boostrap site, but do not forget to close the xml link tag
<h:head>
<title>Facelet Title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"/>
</h:head>We are going to show hot to decorate a bean to log operations on entities, with JPA Entity Listeners:
- Create a listener class in the
entitypackage:
Audit when the entity is persisted
public class ToDoAuditor {
@PostPersist
public void onToDoUpdate(ToDo todo) {
System.out.printf("---------------> %s\n", todo.toString());
}
}- Add an Annotation to the entity:
Register the Listener onto the
ToDoentity:
...
@EntityListeners(ToDoAuditor.class)
public class ToDo implements ValidEntity {...}- Run and insert a ToDo to see the logged output
Now, let's suppose we want to track the changes made on an Entity, we can create a new class:
- Create a
ToDoEntityTrackerin the control package:
package io.github.dinolupo.doit.business.reminders.control;
import io.github.dinolupo.doit.business.reminders.entity.ToDo;
import javax.enterprise.event.Observes;
import javax.enterprise.event.TransactionPhase;
public class ToDoChangeTracker {
// only observe on success update
public void onToDoChange(@Observes(during = TransactionPhase.AFTER_SUCCESS) ToDo todo){
System.out.printf("########## ToDo changed and committed: %s\n", todo);
}
}- We should use now the newly created
ToDoChangeTrackerinto theToDoAuditorclass, but we do not want to violate the dependency rule of the BCE pattern, so we cannot inject thecontrolclass into theentityclass. But we could send an Event, it is ok to send Event and capture those events in other classes.
public class ToDoAuditor {
@Inject
Event<ToDo> events;
@PostPersist
public void onToDoUpdate(ToDo todo) {
System.out.printf("---------------> %s\n", todo.toString());
events.fire(todo);
}
}Warning in Wildfly 10 the injection does not work, so switch to Payara or Glassfish to try the example.
When switching to Payara, you doesn't have anymore the H2 default database, but the Derby DB. You have to start the DB manually, so go to the bin directory of the application server and run the following command to start the Database:
cd <Payara Direcotry>/bin
./asadmin start-databaseif needed, you can use your DB tool to inspect the database using the following connection data:
URL: jdbc:derby://localhost:1527/sun-appserv-samples
user: APP
password: APP
Instead of writing change notifications with System.out, we should do something more interesting, like distributing notifications via Web Sockets. To do this, let's refactor the class ToDoChangeTracker moving it to the boundary package.
To transform the class into a Web Socket ready component, let's do the following:
-
Add an annotation
@ServerEndpoint("/changes")to the class -
Transform the class into a Singleton EJB with concurrency management type BEAN (no locking)
-
Add a Session field and add methods to open and close the Session:
import io.github.dinolupo.doit.business.reminders.entity.ToDo;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;
import javax.enterprise.event.Observes;
import javax.enterprise.event.TransactionPhase;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@ServerEndpoint("/changes")
public class ToDoChangeTracker {
private Session session;
@OnOpen
public void onOpen(Session session){
this.session = session;
}
@OnClose
public void onClose(){
session = null;
}
// only observe on success update
public void onToDoChange(@Observes(during = TransactionPhase.AFTER_SUCCESS) ToDo todo) {
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(todo.toString());
} catch (IOException e) {
// ignore because the connection could be closed anytime
}
}
}
}In real world enterprise applications we do not send the String representation but a more structured object like a Json object. In the following paragraph we will see how to test a websocket implementing a Client and we will change the message into a Json object.
How to quickly test a websocket?
- add a client dependency on the pom.xml
Tyrus is the reference implementation of Web Sockets
pom.xml dependency for websocket client
<!-- https://mvnrepository.com/artifact/org.glassfish.tyrus/tyrus-server -->
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>1.13</version>
</dependency>- We need a Helper class, a Changes Listener to be able to recevive WebSocket messages in our test:
add a ChangesListener class to the doit-st project:
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.Session;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ChangesListener extends Endpoint {
String message;
CountDownLatch latch = new CountDownLatch(1);
@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
session.addMessageHandler(String.class, s -> {
message = s;
latch.countDown();
System.out.println("onOpen message: " + message);
});
}
public String getMessage() throws InterruptedException {
latch.await(1, TimeUnit.MINUTES);
return message;
}
}In this test, we should create a message reusing some code from the CRUD test, but to show quickly how it works, we will add the message via the browser.
So, in the Changes Listener class, we wait 1 minute to receive the message via websocket using a CountDownLatch to wait for it.
- Go to the System Test (Integration Tests) project and add a new class (JUnit)
import static org.junit.Assert.assertNotNull;
public class ToDoChangeTrackerTest {
private WebSocketContainer webSocketContainer;
private ChangesListener listener;
@Before
public void initContainer() throws URISyntaxException, IOException, DeploymentException {
webSocketContainer = ContainerProvider.getWebSocketContainer();
URI uri = new URI("ws://localhost:8080/doit/changes");
this.listener = new ChangesListener();
webSocketContainer.connectToServer(listener, uri);
}
@Test
public void receiveNotifications() throws InterruptedException {
String message = listener.getMessage();
assertNotNull(message);
System.out.println("receiveNotifications message: " + message);
}
}- Run the test and the server, the test will wait until you insert a message into the web interface.
We now want to serialize the object with Json and we want to serialize also the type of the operation like Created or Uopdated.
- First of all, to distinguish if it is a creation or an update, we will add a new qualifier to our project, creating the following in the boundary package of the doit project:
create a new
@Qualifier
import javax.inject.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface ChangeEvent {
Type value();
enum Type {
CREATION, UPDATE;
}
}- We will use this qualifier into the ToDoAuditor class:
Modify the
ToDoAuditorclass to distinguish between creation and update events
import io.github.dinolupo.doit.business.reminders.boundary.ChangeEvent;
import javax.ejb.EJB;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.persistence.PostPersist;
import javax.persistence.PostUpdate;
public class ToDoAuditor {
// CDI Injection on JPA Listeners does not work on WildFly 10, switch to Payara or Glassfish
@Inject
@ChangeEvent(ChangeEvent.Type.CREATION)
Event<ToDo> create;
@Inject
@ChangeEvent(ChangeEvent.Type.UPDATE)
Event<ToDo> update;
@PostUpdate
public void onUpdate(ToDo todo) {
update.fire(todo);
}
@PostPersist
public void onPersist(ToDo todo) {
create.fire(todo);
}
}In this class now we have two different types of events that will be fired on @PostPersist or @PostUpdate. The events will carry the previously created qualifier with them.
- To serialize JSON via websockets, we need an Encoder, so let's create it:
Create an Encoder for Json to serialize it via websockets:
package io.github.dinolupo.doit.business.reminders.boundary;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonWriter;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import java.io.IOException;
import java.io.Writer;
public class JsonEncoder implements Encoder.TextStream<JsonObject> {
@Override
public void init(EndpointConfig config) {
}
@Override
public void encode(JsonObject payload, Writer writer) throws EncodeException, IOException {
// use autocloseable feature
try (JsonWriter jsonWriter = Json.createWriter(writer)) {
jsonWriter.writeObject(payload);
}
}
@Override
public void destroy() {
}
}Then we modify the ToDoChangeTracker class to send a Json object:
use the Encoder and send Json object instead of String in the
ToDoChangeTrackerclass:
package io.github.dinolupo.doit.business.reminders.boundary;
import io.github.dinolupo.doit.business.reminders.entity.ToDo;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;
import javax.enterprise.event.Observes;
import javax.enterprise.event.TransactionPhase;
import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
/**
* Created by dinolupo.github.io on 26/07/16.
*/
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@ServerEndpoint(value = "/changes", encoders = {JsonEncoder.class})
public class ToDoChangeTracker {
private Session session;
@OnOpen
public void onOpen(Session session){
this.session = session;
}
@OnClose
public void onClose(){
session = null;
}
// only observe on success creation
public void onToDoCreation(@Observes(during = TransactionPhase.AFTER_SUCCESS)
@ChangeEvent(ChangeEvent.Type.CREATION) ToDo todo) throws EncodeException {
if (session != null && session.isOpen()) {
try {
JsonObject event = Json.createObjectBuilder()
.add("id", todo.getId())
.add("mode", ChangeEvent.Type.CREATION.toString())
.build();
session.getBasicRemote().sendObject(event);
} catch (IOException e) {
// ignore because the connection could be closed anytime
}
}
}
}As we see from the previous code the folling has changed:
a) using encoder in the class declaration:
add encoders parameter
@ServerEndpoint(value = "/changes", encoders = {JsonEncoder.class})b) get only event of type CREATION (we need to implement another method for UPDATE)
add a qualifier on ToDo method parameter
@ChangeEvent(ChangeEvent.Type.CREATION)c) Create a Json object instead of toString() and use sendObject() to the remote endpoint
Json object creation
JsonObject event = Json.createObjectBuilder()
.add("id", todo.getId())
.add("mode", ChangeEvent.Type.CREATION.toString())
.build();Now let's add support to decode JSON on the client side
- In the dois-st project, add a decoder class
JsonDecoder
package io.github.dinolupo.doit.business.reminders.boundary;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;
import java.io.IOException;
import java.io.Reader;
public class JsonDecoder implements Decoder.TextStream<JsonObject> {
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public JsonObject decode(Reader reader) throws DecodeException, IOException {
try (JsonReader jsonReader = Json.createReader(reader)) {
return jsonReader.readObject();
}
}
@Override
public void destroy() {
}
}-
Change all the String message types into JsonObject type in the
ChangesListenerclass andToDoChangeTrackerTest.receiveNotifications()method -
Run the test again to see the output
It is forbidden to use generic package names like utils or infrastructure.
In our code we have put the JsonEncoder class in the boundary package, it is correct because only one class use it, but to organize better we could put the class in the encoders package. Also we could introduce a validation package to put all the CROSS CHECK related classes.