Skip to content

3. Sample Application – 01: rdvmedecins-jsf2-ejb

The following text refers to the following documents:

The sample application to be studied comes from [ref9].

3.1. The Application

An IT services company [ISTIA-AGI] wants to offer an appointment scheduling service. The primary target market is solo practitioners. These doctors generally do not have administrative staff. Clients wishing to make an appointment therefore call the doctor directly. This frequently disrupts the doctor’s work throughout the day, reducing their availability to patients. The company [ISTIA-AGI] wishes to offer them an appointment scheduling service based on the following principle:

  • a receptionist handles appointment scheduling for a large number of doctors. This receptionist can be just one person. Their salary is shared among all the doctors using the appointment service.
  • The administrative office and all doctors are connected to the Internet
  • Appointments are recorded in a centralized database, accessible via the Internet by the administrative office and the doctors
  • Appointments are normally scheduled by the administrative office. They can also be scheduled by the doctors themselves. This is particularly the case when, at the end of a consultation, the doctor schedules a new appointment for the patient.

The architecture of the appointment scheduling service is as follows:

Doctors become more efficient if they no longer have to manage appointments. If there are enough of them, their contribution to the administrative office’s operating costs will be minimal.

The company [ISTIA-AGI] has decided to develop the application in two versions:

  • a JSF / EJB3 / JPA EclipseLink / Glassfish server version:
  • and a JSF / Spring / JPA Hibernate / Tomcat server version:

3.2. How the application works

We will call the application [RdvMedecins]. Below are screenshots showing how it works.

The application’s home page is as follows:

From this initial page, the user (Receptionist, Doctor) will perform a number of actions. We present them below. The left view shows the page from which the user makes a request; the right view shows the response sent by the server.

Finally, an error page may also appear:

3.3. The Database

Let’s return to the architecture of the application to be built:

The database, which we will call [ dbrdvmedecins2], is a MySQL5 database with four tables:

  

3.3.1. The [MEDECINS] table

It contains information about the doctors managed by the [RdvMedecins] application.

  • ID: the doctor’s ID number—the table’s primary key
  • VERSION: a number identifying the version of the row in the table. This number is incremented by 1 each time a change is made to the row.
  • LAST_NAME: the doctor’s last name
  • FIRST_NAME: the doctor's first name
  • TITLE: their title (Ms., Mrs., Mr.)

3.3.2. The [CLIENTS] table

The clients of the various doctors are stored in the [CLIENTS] table:

  • ID: the client's ID number - the table's primary key
  • VERSION: A number identifying the version of the row in the table. This number is incremented by 1 each time a change is made to the row.
  • LAST NAME: the customer's last name
  • FIRST NAME: the customer's first name
  • TITLE: their title (Ms., Mrs., Mr.)

3.3.3. The [SLOTS] table

It lists the time slots when appointments are available:

  • ID: ID number for the time slot - primary key of the table (row 8)
  • VERSION: number identifying the version of the row in the table. This number is incremented by 1 each time a change is made to the row.
  • DOCTOR_ID: ID number identifying the doctor to whom this time slot belongs – foreign key on the DOCTORS(ID) column.
  • START_TIME: start time of the time slot
  • MSTART: Start minute of the time slot
  • HFIN: slot end time
  • MFIN: End minutes of the slot

The second row of the [SLOTS] table (see [1] above) indicates, for example, that slot #2 begins at 8:20 a.m. and ends at 8:40 a.m. and belongs to doctor #1 (Ms. Marie PELISSIER).

3.3.4. The [RV] table

It lists the appointments made for each doctor:

  • ID: unique identifier for the appointment – primary key
  • DAY: day of the appointment
  • SLOT_ID: appointment time slot – foreign key on the [ID] field of the [SLOTS] table – determines both the time slot and the doctor involved.
  • CLIENT_ID: ID of the client for whom the reservation was made – foreign key on the [ID] field of the [CLIENTS] table

This table has a uniqueness constraint on the values of the joined columns (DAY, SLOT_ID):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (DAY, SLOT_ID);

If a row in the [RV] table has the value (DAY1, SLOT_ID1) for the columns (DAY, SLOT_ID), this value cannot appear anywhere else. Otherwise, this would mean that two appointments were booked at the same time for the same doctor. From a Java programming perspective, the database’s JDBC driver throws an SQLException when this occurs.

The row with ID equal to 3 (see [1] above) means that an appointment was booked for slot #20 and client #4 on 08/23/2006. The [SLOTS] table tells us that slot no. 20 corresponds to the time slot 4:20 PM – 4:40 PM and belongs to doctor no. 1 (Ms. Marie PELISSIER). The [CLIENTS] table tells us that client no. 4 is Ms. Brigitte BISTROU.

3.3.5. Generating the database

To create the tables and populate them, you can use the script [dbrdvmedecins2.sql], which can be found on the examples website. Using [WampServer] (see section 1.3.3), proceed as follows:

  • In [1], click on the [WampServer] icon and select the [PhpMyAdmin] option [2],
  • in [3], in the window that opens, select the [Databases] link,
  • in [2], create a database with the name [4] and encoding [5],
  • in [7], the database has been created. Click on its link,
  • in [8], import an SQL file,
  • which you select from the file system using the [9] button,
  • in [11], select the SQL script and in [12] execute it,
  • in [13], the four tables in the database have been created. Follow one of the links,
  • in [14], the table’s contents.

We will not return to this database again. However, the reader is invited to follow its evolution throughout the programs, especially when things don’t work.

3.4. The [DAO] and [JPA] layers

Let’s return to the architecture we need to build:

We will build four Maven projects:

  • one project for the [DAO] and [JPA] layers,
  • a project for the [business] layer,
  • a project for the [web] layer,
  • an enterprise project that will bring together the three previous projects.

We will now build the Maven project for the [DAO] and [JPA] layers.

Note: Understanding the [business], [DAO], and [JPA] layers requires knowledge of Java EE. For this, you can refer to [ref7] (see paragraph 3).

3.4.1. The NetBeans project

Here is what to do:

  • In [1], we create a Maven project of type [EJB Module] [2],
  • in [3], we name the project,
  • in [4], select the GlassFish server,
  • in [5], the generated project.

3.4.2. Generating the [JPA] layer

Let’s return to the architecture we need to build:

With NetBeans, it is possible to automatically generate the [JPA] layer and the [EJB] layer that controls access to the generated JPA entities. It is useful to be familiar with these automatic generation methods because the generated code provides valuable insights into how to write JPA entities or the EJB code that uses them.

We will now describe some of these automatic generation tools. To understand the generated code, you need a solid understanding of JPA entities [ref8] and EJBs [ref7] (see Section 3).

3.4.2.1. Creating a NetBeans connection to the database

  • Start the MySQL 5 DBMS so that the database is available,
  • create a NetBeans connection to the database [dbrdvmedecins2],
  • in the [Services] tab [1], under the [Databases] section [2], select the MySQL JDBC driver [3],
  • then select the [4] "Connect Using" option to create a connection to a MySQL database,
  • in [5], enter the requested information. In [6], the database name; in [7], the database user and password;
  • in [8], you can test the information you have provided,
  • in [9], the expected message if the information is correct,
  • in [10], the connection is established. You can see the four tables in the connected database.

3.4.2.2. Creating a persistence unit

Let’s return to the architecture we’re building:

We are currently building the [JPA] layer. Its configuration is done in a [persistence.xml] file where persistence units are defined. Each one requires the following information:

  • the JDBC connection details for the database (URL, username, password),
  • the classes that will represent the database tables,
  • the JPA implementation used. Indeed, JPA is a specification implemented by various products. Here, we will use EclipseLink, which is the default implementation used by the GlassFish server. This saves us from having to add libraries from another implementation to GlassFish.

NetBeans can generate this persistence file using a wizard.

  • Right-click on the project and select "Create Persistence Unit" [1],
  • in [2], give the persistence unit you are creating a name,
  • in [3], select the EclipseLink JPA implementation (JPA 2.0),
  • in [4], specify that database transactions will be managed by the GlassFish server’s EJB container,
  • in [5], specify that the database tables have already been created and therefore will not be created,
  • in [6], create a new data source for the GlassFish server,
  • in [7], provide a JNDI (Java Naming Directory Interface) name,
  • in [8], link this name to the MySQL connection created in the previous step,
  • in [9], finish the wizard,
  • in [10], the new project,
  • in [11], the [persistence.xml] file was generated in the [META-INF] folder,
  • in [12], a [setup] folder has been generated,
  • in [13], new dependencies have been added to the Maven project.

The generated [META-INF/persistence.xml] file is as follows:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties/>
  </persistence-unit>
</persistence>

It includes the information provided in the wizard:

  • line 3: the name of the persistence unit,
  • line 3: the type of database transactions, in this case JTA (Java Transaction API) transactions managed by the GlassFish server’s EJB3 container,
  • line 4: the JNDI name of the data source.

Normally, this file specifies the type of JPA implementation being used. In the wizard, we selected EclipseLink. Since this is the default JPA implementation used by the GlassFish server, it is not mentioned in the [persistence.xml] file.

In the [Design] tab, you can see an overview of the [persistence.xml] file:

To obtain EclipseLink logs, we will use the following [persistence.xml] file:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="dbrdvmedecins2-PU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>jdbc/dbrdvmedecins2</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.logging.level" value="FINE"/> 
    </properties>
  </persistence-unit>
</persistence>
  • line 4: specifies that the EclipseLink JPA implementation is being used,
  • lines 7–9: contain the configuration properties for the JPA provider, in this case EclipseLink,
  • line 8: this property enables logging of the SQL statements that EclipseLink will execute.

The [glassfish-resources.xml] file that was created is as follows:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN" "http://glassfish.org/dtds/glassfish-resources_1_5.dtd">
<resources>
    <jdbc-connection-pool allow-non-component-callers="false" ... steady-pool-size="8" validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
        <property name="serverName" value="localhost"/>
        <property name="portNumber" value="3306"/>
        <property name="databaseName" value="dbrdvmedecins2"/>
        <property name="User" value="root"/>
        <property name="Password" value=""/>
        <property name="URL" value="jdbc:mysql://localhost:3306/dbrdvmedecins2"/>
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
    </jdbc-connection-pool>
    <jdbc-resource enabled="true" jndi-name="jdbc/dbrdvmedecins2" object-type="user" pool-name="mysql_dbrdvmedecins2_rootPool"/>
</resources>

This file contains the information we entered in the two wizards used previously:

  • lines 5–11: the JDBC properties of the MySQL5 database [dbrdvmedecins2],
  • line 13: the JNDI name of the data source.

This file will be used to create the JNDI data source [jdbc/dbrdvmedecins2] for the GlassFish server. This is specific to this server. For another server, a different approach would be required, typically using an administration tool. Such a tool is also available for GlassFish.

Finally, dependencies have been added to the project. The [pom.xml] file is as follows:


<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>istia.st</groupId>
    <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>

    <name>mv-rdvmedecins-ejb-dao-jpa</name>

    ...
    <dependencies>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.0.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.3.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

...
    <repositories>
        <repository>
            <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
            <id>eclipselink</id>
            <layout>default</layout>
            <name>Repository for library Library[eclipselink]</name>
        </repository>
    </repositories>
</project>
  • Lines 32–37: A [JPA] layer requires the [javaee-api] artifact;
  • lines 16, 22, 28: the artifacts required by the JPA/EclipseLink implementation used here.
  • lines 18, 24, 30, 36: all artifacts have the provided attribute. Note that this means they are required for compilation but not for runtime. In fact, at runtime, they are provided by the GlassFish server,
  • Lines 41–48: define a new Maven artifact repository, where the EclipseLink artifacts can be found.

3.4.2.3. Generating JPA Entities

JPA entities can be generated using a NetBeans wizard:

  • in [1], create JPA entities from a database,
  • in [2], select the previously created data source [jdbc / dbrdvmedecins2],
  • in [3], the list of tables for this data source,
  • in [4], select all of them,
  • in [5], the selected tables,
  • in [6], we name the Java classes associated with the four tables,
  • as well as a package name [7],
  • In [8], JPA groups database table rows into collections. We choose a list as the collection,
  • in [9], the Java classes created by the wizard.

3.4.2.4. The generated JPA entities

The [Medecin] entity represents the [medecins] table. The Java class is littered with annotations that make the code difficult to read at first glance. If we keep only what is essential to understanding the entity’s role, we get the following code:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "medecins")
public class Doctor implements Serializable {
  
@Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;
  
  @Column(name = "TITLE")
  private String title;

  @Column(name = "LAST_NAME")
  private String name;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "FIRST_NAME")
  private String firstName;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "doctorId")
  private List<AppointmentSlot> appointmentSlotList;

// constructors
....

  // getters and setters
....

  @Override
  public int hashCode() {
  ...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • Line 4: The @Entity annotation makes the [Medecin] class a JPA entity, i.e., a class linked to a database table via the JPA API.
  • line 5, the name of the database table associated with the JPA entity. Each field in the table corresponds to a field in the Java class,
  • line 6: the class implements the Serializable interface. This is necessary in client/server applications, where entities are serialized between the client and the server.
  • lines 10–11: the id field of the [Medecin] class corresponds to the [ID] field (line 10) of the [medecins] table,
  • lines 13–14: The *title* field in the [Doctor] class corresponds to the [TITLE] field (line 13) in the [doctors] table,
  • lines 16–17: the name field of the [Doctor] class corresponds to the [NAME] field (line 16) of the [doctors] table,
  • lines 19-20: the version field of the [Medecin] class corresponds to the [VERSION] field (line 19) of the [medecins] table. Here, the wizard does not recognize that the column is actually a version column that must be incremented each time the row to which it belongs is modified. To assign this role to it, you must add the @Version annotation. We will do this in a later step,
  • lines 22–23: the first_name field of the [Doctor] class corresponds to the [FIRST_NAME] field of the [doctors] table,
  • lines 10–11: the id field corresponds to the primary key [ID] of the table. The annotations on lines 8–9 clarify this point,
  • line 8: the @Id annotation indicates that the annotated field is associated with the table’s primary key,
  • line 9: the [JPA] layer will generate the primary key for the rows it inserts into the [Doctors] table. There are several possible strategies. Here, the GenerationType.IDENTITY strategy indicates that the JPA layer will use the auto_increment mode of the MySQL table,
  • lines 25–26: the [slots] table has a foreign key on the [doctors] table. A slot belongs to a doctor. Conversely, a doctor has several slots associated with them. We therefore have a one-to-many relationship (one doctor to many slots), a relationship qualified by the @OneToMany annotation in JPA (line 25). The field on line 26 will contain all of the doctor’s slots. This is achieved without any programming. To fully understand line 25, we need to introduce the [Creneau] class.

It is as follows:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "slots")
public class Slot implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "START_TIME")
  private int mstart;

  @Column(name = "END")
  private int end;

  @Column(name = "START")
  private int start_time;

  @Column(name = "MFIN")
  private int mfin;

  @Column(name = "VERSION")
  private int version;

  @JoinColumn(name = "DOCTOR_ID", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Doctor doctorId;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idCreneau")
  private List<Rv> appointmentList;

// constructors
...
// getters and setters
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}

We only comment on the new annotations:

  • we have specified that the [slots] table has a foreign key to the [doctors] table: a slot is associated with a doctor. Multiple slots can be associated with the same doctor. We have a relationship from the [slots] table to the [doctors] table that is defined as many-to-one (slots to doctor). The @ManyToOne annotation on line 32 is used to define the foreign key,
  • line 31, with the @JoinColumn annotation, specifies the foreign key relationship: the [ID_MEDECIN] column in the [slots] table is a foreign key on the [ID] column in the [doctors] table,
  • Line 33: a reference to the doctor who owns the slot. This is achieved here as well without any coding.

The foreign key relationship between the [Creneau] entity and the [Medecin] entity is therefore implemented by two annotations:

  • in the [Creneau] entity:

@JoinColumn(name = "ID_MEDECIN", referencedColumnName = "ID")
  @ManyToOne(optional = false)
private Doctor doctorId;
  • in the [Doctor] entity:

@OneToMany(cascade = CascadeType.ALL, mappedBy = "idMedecin")
private List<Slot> slotList;

Both annotations reflect the same relationship: that of the foreign key from the [Appointments] table to the [Doctors] table. They are said to be inverses of each other. Only the @ManyToOne relationship is essential. It unambiguously defines the foreign key relationship. The @OneToMany relationship is optional. If present, it simply references the @ManyToOne relationship with which it is associated. This is the meaning of the mappedBy attribute on line 1 of the [Doctor] entity. The value of this attribute is the name of the field in the [Slot] entity that has the @ManyToOne annotation specifying the foreign key. Also on line 1 of the [Medecin] entity, the cascade=CascadeType.ALL attribute defines the behavior of the [Medecin] entity with respect to the [Creneau] entity:

  • if a new [Doctor] entity is inserted into the database, then the [TimeSlot] entities in the field on line 2 must also be inserted,
  • if a [Doctor] entity is modified in the database, then the [Slot] entities in the field on line 2 must also be modified,
  • if an [Doctor] entity is deleted from the database, then the [Slot] entities in the field on line 2 must also be deleted.

We provide the code for the other two entities without any specific comments, since they do not introduce any new notation.

The [Client] entity


package rdvmedecins.jpa;

...
@Entity
@Table(name = "clients")
public class Client implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "TITLE")
  private String title;

  @Column(name = "NAME")
  private String name;

  @Column(name = "VERSION")
  private int version;

  @Column(name = "FIRST_NAME")
  private String firstName;

  @OneToMany(cascade = CascadeType.ALL, mappedBy = "idClient")
  private List<Rv> rvList;

// constructors
...
// getters and setters
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • Lines 24–25 reflect the foreign key relationship between the [rv] table and the [clients] table.

The [Rv] entity:


package rdvmedecins.jpa;

...
@Entity
@Table(name = "rv")
public class Rv implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Column(name = "DAY")
  @Temporal(TemporalType.DATE)
  private Date day;

  @JoinColumn(name = "SLOT_ID", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Slot slotId;

  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client clientId;

  // constructors
...

  // getters and setters
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    ...
  }
  
}
  • Line 13 specifies that the `jour` field is of type Java Date. It indicates that in the [rv] table, the [JOUR] column (line 12) is of type date (without time),
  • lines 16–18: define the foreign key relationship from the [rv] table to the [slots] table,
  • Lines 20–22: define the foreign key relationship from the [rv] table to the [clients] table.

The automatic generation of JPA entities provides us with a working foundation. Sometimes this is sufficient, sometimes it is not. This is the case here:

  • we need to add the @Version annotation to the various version fields of the entities,
  • we need to write toString methods that are more explicit than the generated ones,
  • the [Medecin] and [Client] entities are analogous. We will have them derive from a [Person] class,
  • we will remove the inverse @OneToMany relationships from the @ManyToOne relationships. They are not essential and introduce programming complications,
  • we remove the @NotNull validation on the primary keys. When persisting a JPA entity with MySQL, the entity initially has a null primary key. It is only after persistence in the database that the primary key of the persisted entity has a value.

With these specifications, the various classes become as follows:

The Person class is used to represent doctors and clients:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@MappedSuperclass
public class Person implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "ID")
  private Long id;

  @Basic(optional = false)
  @Size(min = 1, max = 5)
  @Column(name = "TITLE")
  private String title;

  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "NAME")
  private String name;

  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @Basic(optional = false)
  @NotNull
  @Size(min = 1, max = 30)
  @Column(name = "FIRST_NAME")
  private String firstName;
// constructors
...

// getters and setters
  ...

  @Override
  public String toString() {
    return String.format("[%s,%s,%s,%s,%s]", id, version, title, firstName, lastName);
  }
  
}
  • Line 8: Note that the [Person] class is not itself an entity (@Entity). It will serve as the parent class for entities. The @MappedSuperClass annotation indicates this.

The [Client] entity encapsulates the rows of the [clients] table. It derives from the preceding [Person] class:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client extends Person implements Serializable {
  private static final long serialVersionUID = 1L;

// constructors
...

  @Override
  public int hashCode() {
...
  }

  @Override
  public boolean equals(Object object) {
  ...
  }

  @Override
  public String toString() {
    return String.format("Client[%s,%s,%s,%s]", getId(), getTitle(), getFirstName(), getLastName());
  }
  
}
  • Line 6: The [Client] class is a JPA entity,
  • line 7: it is associated with the [clients] table,
  • line 8: it derives from the [Person] class.

The [Doctor] entity, which encapsulates the rows of the [doctors] table, follows the same pattern:


package rdvmedecins.jpa;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@Table(name = "doctors")
public class Doctor extends Person implements Serializable {
  private static final long serialVersionUID = 1L;

  // constructors
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Doctor[%s,%s,%s,%s]", getId(), getTitle(), getFirstName(), getLastName());
  }
  
}

The [Creneau] entity encapsulates the rows of the [creneaux] table:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "slots")
public class Slot implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "START_DATE")
  private int mstart;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "ENDTIME")
  private int end;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "START_TIME")
  private int hstart;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "MFIN")
  private int mfin;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "VERSION")
  @Version
  private int version;
  
  @JoinColumn(name = "DOCTOR_ID", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Doctor doctor;

  // constructors
  ...

  // getters and setters
  ...
 
  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    // TODO: Warning - this method won't work if the id fields are not set
    ...
  }

  @Override
  public String toString() {
    return String.format("Slot [%s, %s, %s:%s, %s:%s,%s]", id, version, startHour, startMinute, endHour, endMinute, doctor);
  }
}
  • Lines 45–47 model the "many-to-one" relationship between the [slots] table and the [doctors] table in the database: a doctor has multiple slots, and a slot belongs to a single doctor.

The [Rv] entity encapsulates the rows of the [rv] table:


package rdvmedecins.jpa;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.*;
import javax.validation.constraints.NotNull;

@Entity
@Table(name = "rv")
public class Rv implements Serializable {

  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Basic(optional = false)
  @Column(name = "ID")
  private Long id;
  
  @Basic(optional = false)
  @NotNull
  @Column(name = "DAY")
  @Temporal(TemporalType.DATE)
  private Date day;
  
  @JoinColumn(name = "SLOT_ID", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Slot slot;
  
  @JoinColumn(name = "ID_CLIENT", referencedColumnName = "ID")
  @ManyToOne(optional = false)
  private Client client;

  // constructors
...

  // getters and setters
...

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object object) {
    ...
  }

  @Override
  public String toString() {
    return String.format("Appointment[%s, %s, %s]", id, time slot, client);
  }
}
  • Lines 29–31 model the "many-to-one" relationship between the [rv] table and the [clients] table (a client can appear in multiple Rv entries) in the database, and lines 25–27 model the "many-to-one" relationship between the [rv] table and the [slots] table (a slot can appear in multiple Rv).

3.4.3. The exception class

The application's exception class [ RdvMedecinsException] is as follows:


package rdvmedecins.exceptions;

import java.io.Serializable;
import javax.ejb.ApplicationException;

@ApplicationException(rollback=true)
public class RdvMedecinsException extends RuntimeException implements Serializable{

  // private fields
  private int code = 0;

  // constructors
  public RdvMedecinsException() {
    super();
  }

  public RdvMedecinsException(String message) {
    super(message);
  }

  public RdvMedecinsException(String message, Throwable cause) {
    super(message, cause);
  }

  public RdvMedecinsException(Throwable cause) {
    super(cause);
  }

  public RdvMedecinsException(String message, int code) {
    super(message);
    setCode(code);
  }

  public RdvMedecinsException(Throwable cause, int code) {
    super(cause);
    setCode(code);
  }

  public RdvMedecinsException(String message, Throwable cause, int code) {
    super(message, cause);
    setCode(code);
  }

  // getters - setters
  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • Line 7: The class extends the [RuntimeException] class. Therefore, the compiler does not require it to be handled with try/catch blocks.
  • line 6: The @ApplicationException annotation ensures that the exception will not be "swallowed" by an [EjbException].

To understand the @ApplicationException annotation, let’s revisit the server-side architecture:

The [RdvMedecinsException] exception will be thrown by the EJB methods in the [DAO] layer within the EJB3 container and intercepted by it. Without the @ApplicationException annotation, the EJB3 container encapsulates the exception that occurred within an [EjbException] and rethrows it. You may not want this encapsulation and may prefer to let an exception of type [RdvMedecinsException] escape from the EJB3 container. This is what the @ApplicationException annotation allows. Furthermore, the (rollback=true) attribute of this annotation instructs the EJB3 container that if an [RdvMedecinsException] occurs within a method executed as part of a transaction with a DBMS, the transaction must be rolled back. In technical terms, this is called rolling back the transaction.

3.4.4. The EJB of the [DAO] layer

The Java interface [ IDao] of the [DAO] layer is as follows:


package rdvmedecins.dao;


import java.util.Date;
import java.util.List;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Creneau;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Appointment;

public interface IDao {

  // list of clients
  public List<Client> getAllClients();
  // list of doctors
  public List<Doctor> getAllDoctors();
  // list of a doctor's available time slots
  public List<TimeSlot> getAllTimeSlots(Doctor doctor);
  // list of a doctor's appointments on a given day
  public List<Appointment> getDoctorAppointmentsForDay(Doctor doctor, Date day);
  // find a client identified by their ID
  public Client getClientById(Long id);
  // Find a doctor identified by their ID
  public Doctor getDoctorById(Long id);
  // Find an appointment identified by its ID
  public Appointment getAppointmentById(Long id);
  // Find a time slot identified by its ID
  public Slot getSlotById(Long id);
  // add an appointment
  public Rv addAppointment(Date date, TimeSlot timeSlot, Client client);
  // delete an appointment
  public void deleteAppointment(Appointment appointment);
}

This interface was built after identifying the requirements of the [web] layer:

  • line 14: the list of clients. We will need this to populate the client dropdown list,
  • line 16: the list of doctors. We will need this to populate the drop-down list of doctors,
  • line 18: the list of a doctor’s available time slots. We’ll need this to display the doctor’s schedule for a given day,
  • line 20: the list of a doctor’s appointments for a given day. Combined with the previous method, this will allow us to display the doctor’s schedule for a given day with their already booked slots,
  • line 22: allows us to find a client by their ID number. This method will let us find a client by selecting from the client dropdown list,
  • line 24: same as above for doctors,
  • line 26: retrieves an appointment by its number. Can be used when deleting an appointment to verify beforehand that it actually exists,
  • line 28: retrieves a time slot by its number. Allows you to identify the slot a user wants to add or delete,
  • line 30: to add an appointment,
  • line 32: to delete an appointment.

The EJB's local interface [IDaoLocal] simply derives from the previous [IDao] interface:


package rdvmedecins.dao;

import javax.ejb.Local;

@Local
public interface IDaoLocal extends IDao{

}

The same applies to the remote interface [IDaoRemote]:


package rdvmedecins.dao;

import javax.ejb.Remote;

@Remote
public interface IDaoRemote extends IDao{

}

The EJB [DaoJpa] implements both the local and remote interfaces:


package rdvmedecins.dao;

...

@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {
  • Line 5 indicates that the remote EJB is named "rdvmedecins.dao". Additionally, the @Singleton annotation (Java EE6) ensures that only a single instance of the EJB will be created. The @Stateless annotation (Java EE5) defines an EJB that can be created in multiple instances to populate an EJB pool,
  • line 6 indicates that all EJB methods run within a transaction managed by the EJB3 container,
  • Line 7 shows that the EJB implements the local and remote interfaces and is also serializable.

The complete EJB code is as follows:


package rdvmedecins.dao;

import java.io.Serializable;
import java.util.Date;
import java.util.List;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import rdvmedecins.exceptions.RdvMedecinsException;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.TimeSlot;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Rv;

@Singleton (mappedName="rdvmedecins.dao")
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class DaoJpa implements IDaoLocal, IDaoRemote, Serializable {

  @PersistenceContext
  private EntityManager em;

  // list of clients
  public List<Client> getAllClients() {
    try {
      return em.createQuery("select rc from Client rc").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 1);
    }
  }

  // list of doctors
  public List<Doctor> getAllDoctors() {
    try {
      return em.createQuery("select rm from Doctor rm").getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 2);
    }
  }

  // list of time slots for a given doctor
  // doctor: the doctor
  public List<TimeSlot> getAllTimeSlots(Doctor doctor) {
    try {
      return em.createQuery("select rc from Creneau rc join rc.medecin m where m.id=:idMedecin").setParameter("idMedecin", medecin.getId()).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // list of appointments for a given doctor on a given day
  // doctor: the doctor
  // day: the day
  public List<Rv> getRvMedecinJour(Medecin doctor, Date day) {
    try {
      return em.createQuery("select rv from Rv rv join rv.slot c join c.doctorId m where m.id=:doctorId and rv.day=:day").setParameter("doctorId", doctor.getId()).setParameter("day", day).getResultList();
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 3);
    }
  }

  // Add an appointment
  // day: day of the appointment
  // time slot: appointment time slot
  // client: client for whom the appointment is being made
  public Appointment addAppointment(Date date, TimeSlot timeSlot, Client client) {
    try {
      Appointment appointment = new Appointment(null, day);
      rv.setClient(client);
      rv.setSlot(slot);
      em.persist(rv);
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
  }

  // Delete an Rv
  // rv: the appointment being deleted
  public void deleteAppointment(Appointment appointment) {
    try {
      em.remove(em.merge(rv));
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 5);
    }
  }

  // retrieve a given client
  public Client getClientById(Long id) {
    try {
      return (Client) em.find(Client.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // retrieve a specific doctor
  public Doctor getDoctorById(Long id) {
    try {
      return (Doctor) em.find(Doctor.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // retrieve a given appointment
  public Rv getRvById(Long id) {
    try {
      return (Rv) em.find(Rv.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }

  // retrieve a given time slot
  public Creneau getCreneauById(Long id) {
    try {
      return (Slot) em.find(Slot.class, id);
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 6);
    }
  }
}
  • line 22: the EntityManager object that manages access to the persistence context. When the class is instantiated, this field will be initialized by the EJB container using the @PersistenceContext annotation on line 21,
  • line 27: JPQL (Java Persistence Query Language) query that returns all rows from the [clients] table as a list of [Client] objects,
  • line 36: a similar query for doctors,
  • line 46: a JPQL query performing a join between the [slots] and [doctors] tables. It is parameterized by the doctor’s ID,
  • line 57: a JPQL query performing a join between the [appointments], [slots], and [doctors] tables and having two parameters: the doctor’s ID and the appointment date,
  • lines 69–73: creation of an appointment followed by its persistence in the database,
  • line 83: deletion of an appointment from the database,
  • line 92: performs a SELECT query on the database to find a given client,
  • line 101: same for a doctor,
  • line 110: same for an appointment,
  • line 119: same for a time slot,
  • All operations using the persistence context from line 22 are likely to encounter a problem with the database. Therefore, they are all enclosed in a try/catch block. Any exception is encapsulated in the custom exception RdvMedecinsException.

3.4.5. Implementation of the MySQL JDBC driver

In the architecture below:

EclipseLink requires the MySQL JDBC driver. This must be installed in the GlassFish server libraries in the folder <glassfish>/domains/domain1/lib/ext, where <glassfish> is the GlassFish server installation directory. It can be obtained as follows:

The folder where the MySQL JDBC driver should be placed is <Domains folder>[1]/domain1/lib/ext [2]. This driver is available at the URL [http://www.mysql.fr/downloads/connector/j/]. Once installed, you must restart the GlassFish server for it to recognize this new library.

3.4.6. Deployment of the [DAO] layer EJB

Let’s return to the architecture we’ve built so far:

The entire [web, business logic, DAO, JPA] stack must be deployed on the GlassFish server. Here’s how we do it:

  • In [1], we build the Maven project,
  • In [2], run it,
  • in [3], it has been deployed to the GlassFish server (the [Services] tab)

You might be curious to check the GlassFish logs:

In [1], the Glassfish logs are available in the [Output / Glassfish Server 3+] tab. They are as follows:

Config: The access type for the persistent class [class rdvmedecins.jpa.Personne] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Client] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Rv] is set to [FIELD].
Config: The target entity (reference) class for the many-to-one mapping element [field client] is being defaulted to: class rdvmedecins.jpa.Client.
Config: The target entity (reference) class for the many-to-one mapping element [field creneau] is being set to: class rdvmedecins.jpa.Creneau.
Config: The access type for the persistent class [class rdvmedecins.jpa.Medecin] is set to [FIELD].
Config: The access type for the persistent class [class rdvmedecins.jpa.Creneau] is set to [FIELD].
Config: The target entity (reference) class for the many-to-one mapping element [field medecin] is being defaulted to: class rdvmedecins.jpa.Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Client] is being defaulted to: Client.
Config: The alias name for the entity class [class rdvmedecins.jpa.Rv] is being defaulted to: Rv.
Config: The alias name for the entity class [class rdvmedecins.jpa.Medecin] is being set to: Medecin.
Config: The alias name for the entity class [class rdvmedecins.jpa.Creneau] is being set to: Creneau.
Info: rdvmedecins.jpa.Creneau was actually transformed
Info: rdvmedecins.jpa.Medecin was actually transformed
Info: rdvmedecins.jpa.Personne was actually transformed
Info: rdvmedecins.jpa.Client was actually transformed
Info: rdvmedecins.jpa.Rv was actually transformed
Info: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Details: Detected database platform: org.eclipse.persistence.platform.database.MySQLPlatform
Config: connecting(DatabaseLogin(
    platform=>DatabasePlatform
    user name=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Config: connecting(DatabaseLogin(
    platform=>MySQLPlatform
    username=> ""
    connector=>JNDIConnector datasource name=>null
))
Config: Connected: jdbc:mysql://localhost:3306/dbrdvmedecins2
    User: root@localhost
    Database: MySQL  Version: 5.5.8-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
Info: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-ejb-dao-jpa/target/classes/_dbrdvmedecins2-PU login successful
Info: EJB5181: Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Info: istia.st_mv-rdvmedecins-ejb-dao-jpa_ejb_1.0-SNAPSHOT was deployed in 270 ms.

Lines marked with [Config] and [Details] are EclipseLink logs; those marked with [Info] come from GlassFish.

  • Lines 1–12: EclipseLink processes the JPA entities it has discovered,
  • lines 13–17: information indicating that the processing of JPA entities proceeded normally,
  • Line 18: EclipseLink reports its presence,
  • line 19: EclipseLink recognizes that it is dealing with the MySQL DBMS,
  • lines 20–24: EclipseLink attempts to connect to the database,
  • lines 25–28: it succeeded,
  • lines 29–33: it attempts to reconnect, this time specifically using a MySQL platform (line 30),
  • lines 34–37: successful again,
  • line 38: confirmation that the persistence unit [dbrdvmedecins-PU] could be instantiated,
  • line 39: the portable names of the remote and local interfaces of the EJB [DaoJpa], where "portable" means recognized by all Java EE 6 application servers,
  • line 40: the names of the remote and local interfaces of the EJB [DaoJpa], specific to GlassFish. In the upcoming test, we will use the name "rdvmedecins.dao".

Lines 39 and 40 are important. When writing an EJB client on GlassFish, it is necessary to know them.

3.4.7. Testing the [DAO] Layer EJB

Now that the [DAO] layer EJB of our application has been deployed, we can test it. We will do this within the context of a client/server application:

The client will test the remote interface of the [DAO] EJB deployed on the GlassFish server.

We start by creating a new Maven project :

  • In [1], we create a new project,
  • in [2,3], we create a Maven project of type [Java Application],
  • in [4], we give it a name and place it in the same folder as the EJB [DAO],
  • In [5], the generated project,
  • in [6], a class [App.java] was generated. We will delete it,
  • in [7], a [Source Packages] branch has been generated. We haven’t encountered this yet. We can put JUnit tests in this branch. We will do so. We will not keep the generated test class [AppTest],
  • in [8], the Maven project dependencies. The [Dependencies] branch is empty. We will need to add new dependencies there. The [Test Dependencies] branch contains the dependencies required for testing. Here, the library used is the JUnit 3.8 framework. We will need to change it.

The project evolves as follows:

  • in [1], the project where the two generated classes have been removed, along with the JUnit dependency.

Let’s return to the client/server architecture that will be used for testing:

The client needs to know the remote interface provided by the EJB [DAO]. Additionally, it will exchange JPA entities with the EJB. It therefore needs the definitions of these entities. To ensure the EJB test project has access to this information, we will add the EJB [DAO] project as a dependency to the project:

  • In [1], add a dependency to the [Test Dependencies] branch,
  • in [2], select the [Open Projects] tab,
  • in [3], select the EJB [DAO] Maven project,
  • in [4], the dependency is added.

Let’s return to the client/server architecture of the test:

At runtime, the client and server communicate via the TCP-IP network. We will not be programming these exchanges. For each application server, there is a library to be integrated into the client’s dependencies. The one for Glassfish is called [gf-client]. We add it:

  • in [1], we add a dependency,
  • in [2], we specify the characteristics of the desired artifact,
  • in [3], a large number of dependencies are added. Maven will download them. This may take several minutes. They are then stored in the local Maven repository.

We can now create the JUnit test:

  • in [2], right-click on [Test Packages] to create a new JUnit test,
  • in [3], we name the test class and specify a package for it [4],
  • in [5], select the JUnit 4.x framework,
  • in [6], the generated test class,
  • in [7], the new Maven project dependencies.

The [pom.xml] file is then as follows:


<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>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-dao</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-dao</name>
  <url>http://maven.apache.org</url>

  <repositories>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>junit_4</id>
      <layout>default</layout>
      <name>Repository for library Library[junit_4]</name>
    </repository>
  </repositories>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Note:

  • lines 32–51, the project dependencies,
  • lines 13–26: two Maven repositories have been defined, one for EclipseLink (lines 14–19) and the other for JUnit4 (lines 20–25).

The test class will be as follows:


package rdvmedecins.tests.dao;

import java.util.Date;
import java.util.List;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import rdvmedecins.dao.IDaoRemote;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.TimeSlot;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Rv;

public class JUnitTestDao {

  // [DAO] layer being tested
  private static IDaoRemote dao;
  // today's date
  currentDate = new Date();

  @BeforeClass
  public static void init() throws NamingException {
    // Initialize the JNDI environment
    InitialContext initialContext = new InitialContext();
    // instantiate DAO layer
    dao = (IDaoRemote) initialContext.lookup("rdvmedecins.dao");
  }

  @Test
  public void test1() {
    // display clients
    List<Client> clients = dao.getAllClients();
    display("List of clients:", clients);
    // display doctors
    List<Doctor> doctors = dao.getAllDoctors();
    display("List of doctors:", doctors);
    // Display a doctor's available slots
    Doctor doctor = doctors.get(0);
    List<AppointmentSlot> appointmentSlots = dao.getAllAppointmentSlots(doctor);
    display(String.format("List of slots for doctor %s", doctor), slots);
    // list of a doctor's appointments on a given day
    display(String.format("List of slots for doctor %s on [%s]", doctor, day), dao.getDoctorAppointmentsDay(doctor, day));
    // add an appointment
    Appointment appointment = null;
    Slot slot = slots.get(2);
    Client client = clients.get(0);
    System.out.println(String.format("Adding an appointment on [%s] in slot %s for client %s", day, slot, client));
    rv = dao.addAppointment(day, slot, client);
    System.out.println("Appointment added");
    display(String.format("List of appointments for doctor %s on [%s]", doctor, day), dao.getDoctorAppointmentsDay(doctor, day));
    // Adding an appointment in the same time slot on the same day
    // should throw an exception
    System.out.println(String.format("Adding an appointment on [%s] in slot %s for client %s", day, slot, client));
    Boolean error = false;
    try {
      appointment = dao.addAppointment(day, timeSlot, client);
      System.out.println("Appointment added");
    } catch (Exception ex) {
      Throwable th = ex;
      while (th != null) {
        System.out.println(ex.getMessage());
        th = th.getCause();
      }
      // record the error
      error = true;
    }
    // check if there was an error
    Assert.assertTrue(error);
    // list of RVs
    display(String.format("List of appointments for doctor %s on [%s]", doctor, day), dao.getRvMedecinJour(doctor, day));
    // Delete an appointment
    System.out.println("Deleting the added appointment");
    dao.deleteAppointment(appointment);
    System.out.println("Appointment deleted");
    display(String.format("List of appointments for doctor %s on [%s]", doctor, day), dao.getDoctorAppointment(doctor, day));
  }

  // utility method - displays the elements of a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • lines 23–29: The @BeforeClass annotated method is executed before all others. Here, we create a reference to the EJB’s remote interface [DaoJpa]. Recall that we gave it the JNDI name "rdvmedecins.dao",
  • lines 34–35: display the list of clients,
  • lines 37-38: display the list of doctors,
  • lines 40–42: display the time slots for the first doctor,
  • line 44: displays the appointments for the first doctor on the day specified in line 21,
  • lines 46–51: add an appointment for the first doctor, for time slot #2 and the day specified in line 21,
  • line 52: displays, for verification, the first doctor’s appointments for the day specified in line 21. There must be at least one—the one just added,
  • lines 55-70: add the same appointment. Since the [RV] table has a uniqueness constraint, this addition must cause an exception. We verify this in line 70,
  • line 72: display the first doctor’s appointments for the day in line 21 for verification. The one we wanted to add should not be there,
  • lines 74–76: we delete the single appointment that was added,
  • line 77: display the appointments for the first doctor on the day specified in line 21 for verification. The one we just deleted should not be there.

This is a dummy JUnit test. It contains only one assertion (line 70). It is a visual test with the flaws that come with it.

If all goes well, the tests should pass:

  • in [1], we build the test project,
  • in [2], the test is run,
  • in [3], the test passed.

Let’s take a closer look at the test output:

List of customers:
Customer[1,Mr,Jules,MARTIN]
Customer[2,Ms.,Christine,GERMAN]
Customer[3,Mr.,Jules,JACQUARD]
Customer[4,Ms.,Brigitte,BISTROU]
List of doctors:
Doctor[1,Ms.,Marie,PELISSIER]
Doctor[2,Mr.,Jacques,BROMARD]
Doctor[3,Mr.,Philippe,JANDOT]
Doctor[4,Ms.,Justine,JACQUEMOT]
List of doctor's appointment slots Doctor[1,Ms.,Marie,PELISSIER]
Slot [1, 1, 8:00, 8:20,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [2, 1, 8:20, 8:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [4, 1, 9:00, 9:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [5, 1, 9:20, 9:40, Doctor [1, Ms. Marie PELISSIER]]
Slot [6, 1, 9:40, 10:00, Doctor [1, Ms. Marie PELISSIER]]
Slot [7, 1, 10:00, 10:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [8, 1, 10:20, 10:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [9, 1, 10:40, 11:00,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [10, 1, 11:00, 11:20,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [11, 1, 11:20, 11:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [12, 1, 11:40, 12:00,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [13, 1, 14:00, 14:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [14, 1, 2:20 PM, 2:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [15, 1, 2:40 PM, 3:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [16, 1, 3:00 PM, 3:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [17, 1, 3:20 PM, 3:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [18, 1, 3:40 PM, 4:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [19, 1, 4:00 PM, 4:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [20, 1, 4:20 PM, 4:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Time slot [21, 1, 4:40 PM, 5:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [22, 1, 5:00 PM, 5:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [23, 1, 5:20 PM, 5:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [24, 1, 5:40 PM, 6:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
List of time slots for Doctor [1,Ms.,Marie,PELISSIER], on [Wed May 23 15:34:15 CEST 2012]
An appointment was added on [Wed May 23 15:34:15 CEST 2012] in the time slot [3, 1, 8:40, 9:0, Doctor[1,Ms.,Marie,PELISSIER]] for the client [1,Mr.,Jules,MARTIN]
Appointment added
List of appointments for Doctor [1, Ms. Marie PELISSIER], on [Wed May 23 15:34:15 CEST 2012]
Appointment[242, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]], Client[1,Mr.,Jules,MARTIN]]
Added an Appointment on [Wed May 23 15:34:15 CEST 2012] in the time slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]] for the client [1,Mr.,Jules,MARTIN]
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
List of doctor's appointments Doctor[1,Ms.,Marie,PELISSIER], on [Wed May 23 15:34:15 CEST 2012]
Appointment[242, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]], Client[1,Mr.,Jules,MARTIN]]
Deleted the added appointment
Appointment deleted
List of doctor's appointments Doctor[1,Ms.,Marie,PELISSIER], on [Wed May 23 15:34:15 CEST 2012]

The reader is invited to read these logs alongside the code that generated them. We will focus on the exception that occurred when adding an existing appointment, lines 41–49. The exception stack trace is shown in lines 42–48. It is unexpected. Let’s return to the code for the method that adds an appointment:


  // add an appointment
  // day: day of the appointment
  // slot: appointment time slot
  // client: client for whom the appointment is being made
  public Rv addAppointment(Date day, TimeSlot timeSlot, Client client) {
    try {
      Appointment appointment = new Appointment(null, day);
      rv.setClient(client);
      rv.setSlot(slot);
      System.out.println(String.format("before persistence: %s", rv));
      em.persist(rv);
      System.out.println(String.format("after persist: %s", rv));
      return rv;
    } catch (Throwable th) {
      throw new RdvMedecinsException(th, 4);
    }
}

Let's look at the GlassFish logs when adding the two appointments:

...
Info: before persistence: Appt[null, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Info: after persistence: Appt[null, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Query: INSERT INTO rv (DAY, CLIENT_ID, SLOT_ID) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Details: SELECT LAST_INSERT_ID()
Details: SELECT t1.ID, t1.DAY, t1.CLIENT_ID, t1.SLOT_ID FROM slots t0, rv t1 WHERE (((t0.DOCTOR_ID = ?) AND (t1.DAY = ?)) AND (t0.ID = t1.SLOT_ID))
    bind => [2 parameters bound]
Info: before persist: Appointment[null, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Info: after persist: Appt[null, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms,Marie,PELISSIER]], Client[1,Mr,Jules,MARTIN]]
Query: INSERT INTO rv (DAY, CLIENT_ID, SLOT_ID) VALUES (?, ?, ?)
    bind => [3 parameters bound]
Details: SELECT 1
Warning: Local Exception Stack: 
Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '2012-05-23-3' for key 'UNQ1_RV'
Error Code: 1062
...
  • line 2: before the first persist,
  • line 3: after the first persist,
  • line 4: the INSERT statement that will be executed. Note that it does not occur at the same time as the persist operation. If it did, this log would have appeared before line 2. The INSERT operation normally takes place at the end of the transaction in which the method is executed,
  • Line 6: EclipseLink asks MySQL for the last primary key used. It will retrieve the primary key of the newly added appointment. This value will populate the id field of the persisted [Rv] entity,
  • Lines 7–8: The SELECT query that will display the doctor’s appointments,
  • Lines 9–10: The screen displays for the second persistence,
  • Lines 11–12: The INSERT statement that will be executed. It should throw an exception. This exception appears on lines 15–16 and is clear. It is initially thrown by the MySQL JDBC driver due to a violation of the unique constraint on appointments. We can infer that we should see these exceptions in the JUnit test logs. However, this is not the case:
1
2
3
4
5
6
7
8
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe
java.rmi.MarshalException: CORBA MARSHAL 1330446347 Maybe; nested exception is: 
    org.omg.CORBA.MARSHAL: Warning: IOP00810011: Exception from readValue on ValueHandler in CDRInputStream  vmcid: OMG  minor code: 11 completed: Maybe

Let’s review the client/server architecture of the test:

When the [DAO] EJB throws an exception, it must be serialized to reach the client. It is likely that this operation failed for a reason I did not understand. Since our full application will not run in a client/server environment, we can ignore this issue.

Now that the [DAO] layer EJB is operational, we can move on to the [business] layer EJB.

3.5. The [business] layer

Let’s return to the architecture of the application we’re building:

We’re going to build a new Maven project for the [business] EJB. As shown above, it will depend on the Maven project that was built for the [DAO] and [JPA] layers.

3.5.1. The NetBeans project

We are building a new EJB-type Maven project. To do this, simply follow the procedure already used and described on page 174.

  • In [1], the Maven project for the [business] layer,
  • in [2], add a dependency,
  • in [3], select the Maven project for the [DAO] and [JPA] layers,
  • in [4], select the [provided] scope. Note that this means it is required for compilation but not for running the project. In fact, the [business] layer EJB will be deployed on the GlassFish server along with the [DAO] and [JPA] layer EJBs. So when it runs, the [DAO] and [JPA] layer EJBs will already be present,
  • in [6], the new project with its dependency.

Let’s now look at the source code for the [business] layer:

The [Business] EJB will have the following [IMetier] interface:


package rdvmedecins.metier.service;

import java.util.Date;
import java.util.List;

import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.TimeSlot;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Appointment;
import rdvmedecins.business.entities.DoctorDailySchedule;

public interface IBusiness {

    // DAO layer
    // list of clients
    public List<Client> getAllClients();

    // list of doctors
    public List<Doctor> getAllDoctors();

    // list of a doctor's time slots
    public List<TimeSlot> getAllTimeSlots(Doctor doctor);

    // list of a doctor's appointments on a given day
    public List<Appointment> getDoctorAppointmentsForDay(Doctor doctor, Date day);

    // find a client identified by their ID
    public Client getClientById(Long id);

    // Find a doctor identified by their ID
    public Doctor getDoctorById(Long id);

    // Find an appointment identified by its ID
    public Rv getRvById(Long id);

    // Find a time slot identified by its ID
    public Slot getSlotById(Long id);

    // add an appointment
    public Rv addRv(Date day, Slot slot, Client client);

    // delete an appointment
    public void deleteAppointment(Appointment appointment);
    
    // business logic
  public DoctorDailySchedule getDoctorDailySchedule(Doctor doctor, Date date);

}

To understand this interface, we must recall the project’s architecture:

We defined the [DAO] layer interface (Section 3.4.4) and specified that it addresses the needs of the [web] layer, i.e., user requirements. The [web] layer communicates with the [DAO] layer only via the [business] layer. This explains why all the methods of the [DAO] layer are found in the [business] layer. These methods will simply delegate the request from the [web] layer to the [DAO] layer. Nothing more.

During the analysis of the application, a requirement emerged: the ability to display a doctor’s schedule for a given day on a web page to show which time slots are booked and which are available. This is typically the case when a secretary takes a request over the phone. The caller asks for an appointment on a specific day with a specific doctor. To meet this need, the [business] layer provides the method in line 46.


    // business
  public DoctorDailySchedule getDoctorDailySchedule(Doctor doctor, Date date);

One might wonder where to place this method:

  • it could be placed in the [DAO] layer. However, this method does not really address a data access need but rather a business need;
  • we could place it in the [web] layer. That would be a bad idea. Because if we change the [web] layer to a [Swing] layer, we will lose the method even though the need still exists.

The method takes the doctor and the day for which we want the appointment schedule as parameters. It returns an [AgendaMedecinJour] object representing the doctor’s schedule for that day:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.jpa.Medecin;

public class DoctorAppointmentCalendar implements Serializable {

    private static final long serialVersionUID = 1L;
    // fields
    private Doctor doctor;
    private Date day;
    private DoctorAppointmentTime[] doctorAppointmentTimes;

    // constructors
    public DoctorAppointment() {

    }

    public DayDoctorAppointment() {
        this.doctor = doctor;
        this.day = day;
        this.doctorSlotList = doctorSlotList;
    }

    public String toString() {
        StringBuffer str = new StringBuffer("");
        for (DayDoctorSlot cr : dayDoctorSlots) {
            str.append(" ");
            str.append(cr.toString());
        }
        return String.format("Schedule[%s,%s,%s]", doctor, new SimpleDateFormat("dd/MM/yyyy").format(day), str.toString());
    }

    // getters and setters
...
  
}
  • line 12: the doctor whose schedule this is,
  • line 13: the day of the schedule,
  • line 14: the doctor's time slots for that day.
  • The class includes constructors (lines 17, 21) as well as a custom toString method (line 27).

The [DoctorTimeSlotDay] class (line 14) is as follows:


package rdvmedecins.metier.entites;

import java.io.Serializable;
import rdvmedecins.jpa.Creneau;

import rdvmedecins.jpa.Rv;

public class DayDoctorSlot implements Serializable {

    private static final long serialVersionUID = 1L;
    // fields
    private Creneau creneau;
    private Rv rv;

    // constructors
    public CreneauMedecinJour() {

    }

    public DayDoctorSlot(Slot slot, Appointment appointment) {
        this.slot = slot;
    this.rv = rv;
    }

    // toString
    @Override
    public String toString() {
        return String.format("[%s %s]", creneau, rv);
    }

    // getters and setters

  ...
}
  • line 12: a doctor's time slot,
  • line 13: the associated appointment, null if the slot is free.

We can see that the creneauxMedecinJour field on line 14 of the [AgendaMedecinJour] class allows us to retrieve all of the doctor's time slots with the "busy" or "free" status for each one. This was the purpose of the new [getAgendaMedecinJour] method of the [IMetier].

Our EJB [Metier] will have a local interface and a remote interface that simply extend the main interface [IMetier]:


package rdvmedecins.metier.service;
import javax.ejb.Local;

@Local
public interface ILocalBusiness extends IBusiness{

}

package rdvmedecins.business.service;
import javax.ejb.Remote;

@Remote
public interface IMetierRemote extends IMetier{

}

The [Metier] EJB implements these interfaces as follows:


package rdvmedecins.metier.service;

import java.io.Serializable;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.ejb.EJB;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import rdvmedecins.dao.IDaoLocal;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.TimeSlot;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Rv;
import rdvmedecins.business.entities.DoctorDailySchedule;
import rdvmedecins.business.entities.DoctorDailySlot;

@Singleton
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Business implements ILocalBusiness, IRemoteBusiness, Serializable {

  // DAO layer
  @EJB
  private IDaoLocal dao;

  public Metier() {
  }

  @Override
  public List<Client> getAllClients() {
    return dao.getAllClients();
  }

  @Override
  public List<Doctor> getAllDoctors() {
    return dao.getAllDoctors();
  }

  @Override
  public List<Slot> getAllSlots(Doctor doctor) {
    return dao.getAllSlots(doctor);
  }

  @Override
  public List<Appointment> getDoctorAppointment(Doctor doctor, Date date) {
    return dao.getRvMedecinJour(doctor, day);
  }

  @Override
  public Client getClientById(Long id) {
    return dao.getClientById(id);
  }

  @Override
  public Doctor getDoctorById(Long id) {
    return dao.getDoctorById(id);
  }

  @Override
  public Rv getRvById(Long id) {
    return dao.getRvById(id);
  }

  @Override
  public Slot getSlotById(Long id) {
    return dao.getTimeSlotById(id);
  }

  @Override
  public Rv addAppointment(Date date, Appointment appointment, Client client) {
    return dao.addRv(day, slot, client);
  }

  @Override
  public void deleteAppointment(Appointment appointment) {
    dao.deleteAppointment(rv);
  }

  @Override
  public DoctorDailySchedule getDoctorDailySchedule(Doctor doctor, Date date) {
    // list of the doctor's available time slots
    List<TimeSlot> timeSlots = dao.getAllTimeSlots(doctor);
    // list of reservations for this doctor on this day
    List<Appointment> reservations = dao.getDoctorAppointmentsForDay(doctor, day);
    // create a dictionary from the booked appointments
    Map<Long, Appointment> hReservations = new Hashtable<Long, Appointment>();
    for (Rv appointment : reservations) {
      hReservations.put(resa.getCreneau().getId(), resa);
    }
    // Create the schedule for the requested day
    DailyDoctorSchedule agenda = new DailyDoctorSchedule();
    // the doctor
    agenda.setDoctor(doctor);
    // the day
    agenda.setDay(day);
    // the appointment slots
    DoctorTimeSlots[] doctorTimeSlots = new DoctorTimeSlots[timeSlots.size()];
    calendar.setDoctorSlotDay(doctorSlotDay);
    // filling the appointment slots
    for (int i = 0; i < timeSlots.size(); i++) {
      // row i in the calendar
      daytimeDoctorSlots[i] = new DaytimeDoctorSlot();
      // slot ID
      daytimeDoctorSlots[i].setSlot(timeSlots.get(i));
      // Is the slot available or booked?
      if (hReservations.containsKey(timeSlots.get(i).getId())) {
        // the slot is occupied - record the reservation
        Appointment appointment = hReservations.get(timeSlots.get(i).getId());
        daytimeDoctorSlots[i].setAppointment(reservation);
      }
    }
    // return the result
    return agenda;
  }
}
  • line 22, the [Metier] class is a singleton EJB,
  • Line 23: Each EJB method runs within a transaction. This means that the transaction begins at the start of the method, in the [business] layer. This layer will call methods in the [DAO] layer. These methods will run within the same transaction,
  • line 24: the EJB implements its local and remote interfaces and is also serializable,
  • line 27: a reference to the EJB in the [DAO] layer,
  • line 29: this will be injected by the GlassFish server’s EJB container, thanks to the @EJB annotation. So when the methods of the [Business] class execute, the reference to the [DAO] layer’s EJB has been initialized,
  • lines 33–81: this reference is used to delegate the call made to the [Business] layer to the [DAO] layer,
  • line 84: the getAgendaMedecinJour method, which retrieves a doctor’s schedule for a given day. We’ll let the reader follow the comments.

3.5.2. Deployment of the [business] layer

The [business] layer depends on the [DAO] layer. Each layer has been implemented with an EJB. To test the [business] EJB, we need to deploy both EJBs. To do this, we need an enterprise project.

  • [1], create a new project,
  • of type Maven [2] and Enterprise Application [3],
  • and give it a name [4]. The suffix "ear" will be automatically added,
  • in [5], we select the GlassFish server and Java EE 6,
  • in [6], an enterprise application contains modules, typically EJB modules and web modules. Here, the enterprise application will contain the modules of the two EJBs we have built. Since these modules already exist, we do not check the boxes,
  • in [7,8], two projects have been created. [8] is the enterprise project we will use. [7] is a project whose purpose I am unsure of. I haven’t had to use it, and since I haven’t delved deeply into Maven, I don’t know what it’s for. So we’ll ignore it.

Now that the enterprise project has been created, we can define its modules.

  • In [1], we create a new dependency,
  • in [2], we select the EJB [DAO] project,
  • in [3], we declare that it is an EJB. Do not leave the type blank, because in that case the jar type will be used, and that type is not suitable here,
  • in [4], we use the [compile] scope,
  • In [5], the project with its new dependency,
  • in [6, 7, 8], repeat the process to add the EJB from the [business] layer,
  • in [9], the two dependencies,
  • in [10], we build the project,
  • in [11], we run it,
  • in [12], in the [Services] tab, we see that the project has been deployed to the GlassFish server. This means that both EJBs are now present on the server.

In the GlassFish server logs, you can find information about the deployment of the two EJBs:

  • in [1], the GlassFish logs tab.

The following logs are found there:

Info: rdvmedecins.jpa.Creneau was successfully transformed
Info: rdvmedecins.jpa.Medecin was actually transformed
Info: rdvmedecins.jpa.Person actually got transformed
Info: rdvmedecins.jpa.Client was actually transformed
Info: rdvmedecins.jpa.Rv was actually transformed
Info: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461
Info: file:/D:/data/istia-1112/netbeans/dvp/jsf2-pf-pfm/maven/netbeans/rdvmedecins-jsf2-ejb/mv-rdvmedecins-metier-dao/mv-rdvmedecins-metier-dao-ear/target/gfdeploy/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT_jar/_dbrdvmedecins2-PU login successful
Info: EJB5181:Portable JNDI names for EJB DaoJpa: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Info: EJB5181: Portable JNDI names for EJB Metier: [java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote, java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]
  • Lines 1-5: The JPA entities have been recognized,
  • line 7: indicates that the persistence unit [dbrdvmedecins2-PU] was successfully built and that the connection to the associated database was established,
  • line 8: the portable names of the remote and local interfaces of the EJB [DaoJpa]. "Portable" means recognized by all application servers,
  • line 9: the same thing but with GlassFish-specific names,
  • Lines 10–11: same for the EJB [Metier].

We will use the portable name of the remote interface of the EJB [Metier]:

java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote

We will need this when testing the [business] layer.

3.5.3. Testing the [business] layer

As we did for the [DAO] layer, we will test the [business] layer as part of a client/server application:

The client will test the remote interface of the [Business] EJB deployed on the GlassFish server.

We start by creating a new Maven project. To do this, we follow the same procedure used to create the [DAO] layer test project (see section 3.4.7), excluding the creation of the JUnit test. The resulting project is as follows

  • [1] shows the created project with its dependencies: regarding the [DAO] layer EJB, the [Business] layer EJB, and the [gf-client] library.

At this point, the project’s [pom.xml] file is as follows:


<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>istia.st</groupId>
  <artifactId>mv-client-rdvmedecins-ejb-metier</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-client-rdvmedecins-ejb-metier</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.glassfish.appclient</groupId>
      <artifactId>gf-client</artifactId>
      <version>3.1.1</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-dao-jpa</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>mv-rdvmedecins-ejb-metier</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>
</project>

Make sure you have the dependencies described in lines 17–33. The test will be a simple console class:

The code for the [ClientRdvMedecinsMetier] class is as follows:


package istia.st.client;

import java.util.Date;
import java.util.List;

import javax.naming.InitialContext;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.TimeSlot;

import rdvmedecins.jpa.Doctor;
import rdvmedecins.jpa.Appointment;
import rdvmedecins.business.entities.DoctorDailySchedule;
import rdvmedecins.metier.service.IMetierRemote;

public class ClientRdvMedecinsMetier {

  // the name of the EJB's remote interface [Metier]
  private static String IDaoRemoteName = "java:global/istia.st_mv-rdvmedecins-metier-dao-ear_ear_1.0-SNAPSHOT/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // today's date
  private static Date today = new Date();

  public static void main(String[] args) {
    try {
      // Glassfish server JNDI context
      InitialContext initialContext = new InitialContext();
      // reference to remote [business] layer
      IMetierRemote business = (IMetierRemote) initialContext.lookup(IDaoRemoteName);
      // Display clients
      List<Client> clients = business.getAllClients();
      display("List of clients:", clients);
      // Display doctors
      List<Doctor> doctors = business.getAllDoctors();
      display("List of doctors:", doctors);
      // Display a doctor's available slots
      Doctor doctor = doctors.get(0);
      List<Slot> slots = department.getAllSlots(doctor);
      display(String.format("List of slots for doctor %s", doctor), slots);
      // list of a doctor's appointments on a given day
      display(String.format("List of appointments for doctor %s on [%s]", doctor, day), profession.getDoctorAppointmentsDay(doctor, day));
      // display calendar
      DoctorDailyCalendar calendar = business.getDoctorDailyCalendar(doctor, day);
      System.out.println(calendar);
      // add an appointment
      Appointment appointment = null;
      TimeSlot timeSlot = timeSlots.get(2);
      Client client = clients.get(0);
      System.out.println(String.format("Adding an appointment on [%s] in slot %s for client %s", day, slot, client));
      rv = business.addAppointment(day, slot, client);
      System.out.println("Appointment added");
      display(String.format("List of appointments for doctor %s on [%s]", doctor, day), business.getDoctorAppointmentsForDay(doctor, day));
      // display calendar
      calendar = business.getDoctorCalendar(doctor, day);
      System.out.println(calendar);
      // delete an appointment
      System.out.println("Deleting the added appointment");
      profession.deleteAppointment(appointment);
      System.out.println("Appointment deleted");
      display(String.format("List of appointments for doctor %s on [%s]", doctor, day), business.getDoctorAppointmentsForDay(doctor, day));
      // display calendar
      agenda = business.getDoctorDailyAgenda(doctor, day);
      System.out.println(agenda);
    } catch (Throwable ex) {
      System.out.println("Error...");
      while (ex != null) {
        System.out.println(String.format("%s : %s", ex.getClass().getName(), ex.getMessage()));
        ex = ex.getCause();
      }
    }
  }

  // utility method - displays the elements of a collection
  private static void display(String message, List elements) {
    System.out.println(message);
    for (Object element : elements) {
      System.out.println(element);
    }
  }
}
  • line 18: the portable name of the remote interface of the EJB [Metier] was taken from the GlassFish logs,
  • lines 24–27: a reference to the remote interface of the EJB [Metier] is obtained,
  • lines 29–30: display the clients,
  • lines 32–33: display the doctors,
  • lines 35–37: display a doctor’s available time slots,
  • line 39: displays a doctor’s appointments on a given day,
  • lines 41-42: this doctor’s schedule for the same day,
  • lines 44-49: add an appointment,
  • line 50: displays the doctor's appointments. There should be one more,
  • lines 52-53: displays the doctor's schedule. The added appointment should be visible,
  • lines 55-57: delete the appointment you just added,
  • line 58: this should be reflected in the doctor's appointment list,
  • lines 60–61: and in their calendar.

We run the test:

 

The resulting screen displays are as follows:


List of clients:
Client[1,Mr,Jules,MARTIN]
Client[2,Ms.,Christine,GERMAN]
Client[3,Mr.,Jules,JACQUARD]
Client[4,Ms.,Brigitte,BISTROU]
List of doctors:
Doctor[1,Ms.,Marie,PELISSIER]
Doctor[2,Mr.,Jacques,BROMARD]
Doctor[3,Mr.,Philippe,JANDOT]
Doctor[4,Ms.,Justine,JACQUEMOT]
List of doctor's appointment slots Doctor[1,Ms.,Marie,PELISSIER]
Slot [1, 1, 8:00, 8:20,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [2, 1, 8:20, 8:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [4, 1, 9:00, 9:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [5, 1, 9:20, 9:40, Doctor [1, Ms. Marie PELISSIER]]
Slot [6, 1, 9:40, 10:00, Doctor [1, Ms. Marie PELISSIER]]
Slot [7, 1, 10:00, 10:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [8, 1, 10:20, 10:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [9, 1, 10:40, 11:00,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [10, 1, 11:00, 11:20,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [11, 1, 11:20, 11:40,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [12, 1, 11:40, 12:00,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [13, 1, 14:00, 14:20, Doctor [1, Ms. Marie PELISSIER]]
Slot [14, 1, 2:20 PM, 2:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Time slot [15, 1, 2:40 PM, 3:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [16, 1, 3:00 PM, 3:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [17, 1, 3:20 PM, 3:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [18, 1, 3:40 PM, 4:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [19, 1, 4:00 PM, 4:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [20, 1, 4:20 PM, 4:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Time slot [21, 1, 4:40 PM, 5:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [22, 1, 5:00 PM, 5:20 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [23, 1, 5:20 PM, 5:40 PM,Doctor[1,Ms.,Marie,PELISSIER]]
Slot [24, 1, 5:40 PM, 6:00 PM,Doctor[1,Ms.,Marie,PELISSIER]]
List of time slots for Doctor [1,Ms.,Marie,PELISSIER], on [Wed May 23 16:25:26 CEST 2012]
Calendar[Doctor[1,Ms.,Marie,PELISSIER],05/23/2012, [Time Slot [1, 1, 8:00, 8:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [2, 1, 8:20, 8:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [4, 1, 9:00, 9:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [5, 1, 9:20, 9:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [6, 1, 9:40, 10:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [7, 1, 10:0, 10:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [8, 1, 10:20, 10:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [9, 1, 10:40, 11:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [10, 1, 11:00, 11:20, Doctor [1, Ms. Marie PELISSIER]] null] [Slot [11, 1, 11:20, 11:40, Doctor [1, Ms. Marie PELISSIER]] null] [Slot [12, 1, 11:40, 12:00,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [13, 1, 14:00, 14:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [14, 1, 2:20 PM, 2:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [15, 1, 2:40 PM, 3:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [16, 1, 3:00 PM, 3:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [17, 1, 3:20 PM, 3:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [18, 1, 3:40 PM, 4:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [19, 1, 4:00 PM, 4:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [20, 1, 4:20 PM, 4:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [21, 1, 4:40 PM, 5:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [22, 1, 5:00 PM, 5:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [23, 1, 5:20 PM, 6:40 PM , Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [24, 1, 5:40 PM, 6:00 PM, Doctor[1,Ms.,Marie,PELISSIER]] null]]
Appointment added on [Wed May 23 16:25:26 CEST 2012] in slot Slot [3, 1, 8:40, 9:0,Doctor[1,Ms,Marie,PELISSIER]] for client Client[1,Mr,Jules,MARTIN]
Appointment added
List of appointments for Doctor[1,Ms.,Marie,PELISSIER], on [Wed May 23 16:25:26 CEST 2012]
Appointment[252, Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]], Client[1,Mr.,Jules,MARTIN]]
Calendar[Doctor[1,Ms.,Marie,PELISSIER],05/23/2012, [Time Slot [1, 1, 8:0, 8:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [2, 1, 8:20, 8:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]] Appointment[252, Time Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]], Client[1,Mr.,Jules,MARTIN]]] [Slot [4, 1, 9:0, 9:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [5, 1, 9:20, 9:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [6, 1, 9:40, 10:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [7, 1, 10:00, 10:20, Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [8, 1, 10:20, 10:40, Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [9, 1, 10:40, 11:00, Doctor[1, Ms. Marie PELISSIER]] null] [Slot [10, 1, 11:00, 11:20, Doctor[1, Ms. Marie PELISSIER]] null] [Slot [11, 1, 11:20, 11:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [12, 1, 11:40, 12:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [13, 1, 14:00, 14:20, Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [14, 1, 14:20, 14:40, Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [15, 1, 2:40 PM, 3:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [16, 1, 3:00 PM, 3:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [17, 1, 3:20 PM, 3:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [18, 1, 3:40 PM, 4:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [19, 1, 4:00 PM, 4:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [20, 1, 4:20 PM, 4:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [21, 1, 4:40 PM, 5:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [22, 1, 5:00 PM, 5:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [23, 1, 5:20 PM, 5:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [24, 1, 5:40 PM, 6:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null]]
Removal of added appointment
Appointment deleted
List of doctor's appointments Doctor[1,Ms.,Marie,PELISSIER], on [Wed May 23 16:25:26 CEST 2012]
Calendar[Doctor[1,Ms.,Marie,PELISSIER],05/23/2012, [Time Slot [1, 1, 8:0, 8:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [2, 1, 8:20, 8:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [3, 1, 8:40, 9:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [4, 1, 9:00, 9:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [5, 1, 9:20, 9:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [6, 1, 9:40, 10:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [7, 1, 10:0, 10:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [8, 1, 10:20, 10:40,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [9, 1, 10:40, 11:0,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [10, 1, 11:00, 11:20, Doctor [1, Ms. Marie PELISSIER]] null] [Slot [11, 1, 11:20, 11:40, Doctor [1, Ms. Marie PELISSIER]] null] [Slot [12, 1, 11:40, 12:00,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [13, 1, 14:00, 14:20,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [14, 1, 2:20 PM, 2:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [15, 1, 2:40 PM, 3:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [16, 1, 3:00 PM, 3:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [17, 1, 3:20 PM, 3:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [18, 1, 3:40 PM, 4:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [19, 1, 4:00 PM, 4:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [20, 1, 4:20 PM, 4:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [21, 1, 4:40 PM, 5:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Time Slot [22, 1, 5:00 PM, 5:20 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [23, 1, 5:20 PM, 5:40 PM,Doctor[1,Ms.,Marie,PELISSIER]] null] [Slot [24, 1, 5:40 PM, 6:00 PM,Doctor[1,Ms.,Marie,PELISSIER]] null]]
  • line 37: Ms. PELISSIER's schedule, May 23, 2012. No slots are reserved,
  • line 39: addition of an appointment,
  • line 42: Ms. PELISSIER's new schedule. A slot is now reserved for Mr. MARTIN,
  • line 44: the appointment has been deleted,
  • line 46: Ms. PELISSIER's calendar shows that no time slot is reserved.

We now consider the [CAD] and [business] layers to be operational. We still need to write the [web] layer using the JSF framework. To do this, we will use the knowledge acquired at the beginning of this document.

3.6. The [web] layer

Let’s return to the architecture currently under construction:

We are going to build the final layer, the [web] layer.

3.6.1. The NetBeans project

We are building a Maven project:

  • In [1], we create a new project,
  • in [2, 3], a Maven project of type [Web Application],
  • in [4], give it a name,
  • in [5], select the GlassFish server and Java EE 6 Web,
  • in [6], the project thus created,
  • in [7], the project after removing the [index.jsp] page and the package in [Source Packages],
  • in [8, 9], in the project properties, add a framework,
  • in [10], select Java Server Faces,
  • in [11], the Java Server Faces configuration. Leave the default values. Note that JSF 2 is being used,
  • in [12], the project is then modified in two ways: a [web.xml] file is generated, as well as an [index.html] page.

The [web.xml] file is as follows:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.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>

We have already encountered this file.

  • lines 7–11: define the servlet that will handle all requests made to the application. This is the JSF servlet,
  • lines 12–15: define the URLs handled by this servlet. These are URLs of the form /faces/*,
  • lines 21–23: define the [index.xhtml] page as the home page.

This page is as follows:


<?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://java.sun.com/jsf/html">
  <h:head>
    <title>Facelet Title</title>
  </h:head>
  <h:body>
    Hello from Facelets
  </h:body>
</html>

We've seen this before. We can run this project:

  • In [1], we run the project and get the result [2] in the browser.

We will now present the complete project and then go into detail about its various components.

  • in [1], the project's XHTML pages,
  • in [2], the Java code,
  • in [3], the message files, since the application is internationalized,
  • in [4], the project dependencies.

3.6.2. Project dependencies

Let’s return to the project architecture:

The JSF layer relies on the [business], [DAO], and [JPA] layers. These three layers are encapsulated in the two Maven projects we built, which explains the project dependencies [4]. Let’s simply show how these dependencies are added:

  • in [1], we’ll use ejb to indicate that the dependency is on an EJB project,
  • in [2], we’ll enter [provided]. This is because the web project will be deployed alongside the two EJB projects. Therefore, it doesn’t need to include the EJB JARs.

3.6.3. Project configuration

The project configuration is the same as that of the JSF projects we covered at the beginning of this document. We list the configuration files without re-explaining them.

 

[web.xml]: configures the web application.


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Production</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</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>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>

</web-app>

Note that on line 26, the [index.xhtml] page is the application's home page.

[faces-config.xml]: configures the JSF application


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

[beans.xml]: empty but required for the @Named annotation


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

[styles.css]: the application's style sheet


.reservationsHeaders {
   text-align: center;
   font-style: italic;
   color: Snow;
   background: Teal;
}

.creneau {
   height: 25px;
   text-align: center;
   background: MediumTurquoise;
}
.client {
   text-align: left;
   background: PowderBlue;
}

.action {
   width: 6em;
   text-align: left;
   color: Black;
   background: MediumTurquoise;
}
.errorHeaders {
   background: Teal;
   background-color: #ff6633;
   color: Snow;
   font-style: italic;
   text-align: center

}

.errorClass {
   background: MediumTurquoise;
   background-color: #ffcc66;
   height: 25px;
   text-align: center
}

.errorMessage {
   background: PowderBlue;
   background-color: #ffcc99;
   text-align: left
}

[messages_fr.properties]: the French message file


# layout
layout.header=Les Médecins Associés
layout.footer=ISTIA, University of Angers
layout.header.language1=French
layout.header.language2=English
# exception
exception.header=The following exception occurred
exception.httpCode=HTTP error code
exception.message=Exception message
exception.requestUri=URL requested when the error occurred
exception.servletName=Name of the servlet requested when the error occurred
# Form 1
form1.title=Reservations
form1.doctor=Doctor
form1.day=Day (dd/mm/yyyy)
form1.button.calendar=Calendar
form1.day.required=Date required
form1.day.error=Invalid date
# Form 2
form2.title={0}'s {1} {2} schedule on {3}
form2.title_detail={0}'s {1} {2} on {3}
form2.timeSlot=Time slot
form2.client=Client
form2.home=Home
form2.delete=Delete
form2.reserve=Reserve
# Form 3
form3.title=Book an appointment for {0} {1} {2} on {3} in the time slot {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.title_detail=Appointment booking for {0} {1} {2}, on {3} in the time slot {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.submit=Submit
form3.cancel=Cancel
# error
error.title=An error has occurred.
error.message=Error message
error.home=Home
error.class=Cause

[messages_en.properties]: the English message file


# layout
layout.header=Associated Doctors
layout.footer=ISTIA, Angers University
layout.header.language1=French
layout.header.language2=English
# exception
exception.header=The following exceptions occurred
exception.httpCode=HTTP error code
exception.message=Exception message
exception.requestUri=URL targeted when the error occurred
exception.servletName=Name of the servlet involved when the error occurred
# Form 1
form1.title=Reservations
form1.doctor=Doctor
form1.date=Date (dd/mm/yyyy)
form1.button.agenda=Calendar
form1.date.required = The date is required
form1.date.error=The date is invalid
# Form 2
form2.title={0} {1} {2}'' diary on {3}
form2.title_detail={0} {1} {2}'' diary on {3}
form2.time_slot=Time Period
form2.client=Client
form2.homepage=Welcome Page
form2.delete=Delete
form2.book=Book
# Form 3
form3.title=Reservation for {0} {1} {2}, on {3} during the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.title_detail=Reservation for {0} {1} {2}, on {3} during the time period {4,number,#00}:{5,number,#00} - {6,number,#00}:{7,number,#00}
form3.client=Client
form3.submit=Submit
form3.cancel=Cancel
# error
error.title=An error occurred
error.message=Error message
error.homepage=Welcome Page
error.class=Cause

3.6.4. Project Views

Let’s review how the application works. The home page is as follows:

 

From this initial page, the user (Administrative Staff, Doctor) will perform a number of actions. We present them below. The view on the left shows the view from which the user makes a request; the view on the right shows the response sent by the server.

Finally, an error page may also be displayed:

These different views are generated by the following pages of the web project:

  • in [1], the pages [basdepage, entete, layout] handle the formatting of all views,
  • in [2], the view generated by [layout.xhtml].

Facelets technology was used here. This was described in Section 2.11. We will simply provide the code for the XHTML pages used for the layout:

[entete.xhtml]


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <body>
    <h2><h:outputText value="#{msg['layout.entete']}"/></h2>
    <div align="left">
      <h:commandLink value="#{msg['layout.entete.langue1']}" actionListener="#{changeLocale.setFrenchLocale}"/>
      <h:outputText value=" "/>
      <h:commandLink value="#{msg['layout.header.language2']}" actionListener="#{changeLocale.setEnglishLocale}"/>
  </div>
  </body>
/html

Note lines 10–12, the two links for changing the application's language.

[basdepage.xhtml]


<?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://java.sun.com/jsf/html">
  <body>
    <h:outputText value="#{msg['layout.basdepage']}"/>
  </body>
</html>

[layout.xhtml]


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>DoctorAppointments</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="form">
        <table style="width: 1200px">
          <tr>
            <td colspan="2" bgcolor="#ccccff">
              <ui:include src="header.xhtml"/>
            </td>
          </tr>
          <tr>
            <td style="width: 100px; height: 200px" bgcolor="#ffcccc">
            </td>
            <td>
              <ui:insert name="content" >
                <h2>Content</h2>
              </ui:insert>
            </td>
          </tr>
          <tr bgcolor="#ffcc66">
            <td colspan="2">
              <ui:include src="footer.xhtml"/>
            </td>
          </tr>         
        </table>
      </h:form>
    </h:body>
  </f:view>
</html>

This page is the template for the [index.xhtml] 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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ui:composition template="layout.xhtml">
    <ui:define name="content">
      <h:panelGroup rendered="#{form.form1Rendered}">
        <ui:include src="form1.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form2Rendered}">
        <ui:include src="form2.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.form3Rendered}">
        <ui:include src="form3.xhtml"/>
      </h:panelGroup>
      <h:panelGroup rendered="#{form.errorRendered}">
        <ui:include src="error.xhtml"/>
      </h:panelGroup>
    </ui:define>
  </ui:composition>
</html>

Lines 8–21 define the area called "content" (line 8) in [layout.xhtml] (line 7). This is the central area of the views:

 

The [index.xhtml] page is the only page in the application. Therefore, there will be no navigation between pages. It displays one of the four pages [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. This display is controlled by four booleans [form1Rendered, form2Rendered, form3Rendered, errorRendered] from the form bean, which we will describe shortly.

3.6.5. The project’s beans

The classes in the [utils] package have already been presented:

  • the [ChangeLocale] class is the class that handles language switching. It has already been discussed (Section 2.4.4).
  • The [Messages] class is a class that facilitates the internationalization of an application’s messages. It was discussed in section 2.8.5.7.

3.6.5.1. The Application bean

The [Application] bean is as follows:


package beans;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import rdvmedecins.jpa.Client;
import rdvmedecins.jpa.Doctor;
import rdvmedecins.business.service.IMetierLocal;

@Named(value = "application")
@ApplicationScoped
public class Application implements Serializable{

  // business layer
  @EJB
  private ILocalBusinessLogic businessLogic;
  // cache
  private List<Doctor> doctors;
  private List<Client> clients;
  private Map<Long, Doctor> hDoctors = new HashMap<Long, Doctor>();
  private Map<Long, Client> hClients = new HashMap<Long, Client>();
  // errors
  private List<Error> errors = new ArrayList<Error>();
  private Boolean error = false;

  public Application() {
  }

  @PostConstruct
  public void init() {
    // cache doctors and clients
    try {
      doctors = business.getAllDoctors();
      clients = business.getAllClients();
    } catch (Throwable th) {
      // log the error
      error = true;
      errors.add(new Error(th.getClass().getName(), th.getMessage()));
      while (th.getCause() != null) {
        th = th.getCause();
        errors.add(new Error(th.getClass().getName(), th.getMessage()));
      }
      return;
    }
    // Checking the lists
    if (doctors.size() == 0) {
      // log the error
      error = true;
      errors.add(new Error("", "The list of doctors is empty"));
    }
    if (clients.size() == 0) {
      // log the error
      error = true;
      errors.add(new Error("", "The list of clients is empty"));
    }
    // Is there an error?
    if (error) {
      return;
    }

    // the dictionaries
    for (Doctor d : doctors) {
      hDoctors.put(m.getId(), m);
    }
    for (Client c : clients) {
      hClients.put(c.getId(), c);
    }
  }

  // getters and setters
  ...
}
  • lines 15-16: the [Application] class is an Application-scope bean. It is created once at the start of the JSF application lifecycle and is accessible to all requests from all users. We generally store read-only data in the application . Here, we will store the list of doctors and the list of clients. We therefore assume that these do not change often. The XHTML pages access them via the application name,
  • lines 20–21: a reference to the local interface of the [Business] EJB will be injected by the GlassFish EJB container. Let’s review the application architecture:

The JSF application and the [Metier] EJB will run in the same JVM (Java Virtual Machine). Therefore, the [JSF] layer will use the EJB’s local interface. Here, the application bean uses the [Business] EJB. Even if this were not the case, it would be normal to find a reference to it in the [business] layer. This is indeed information that can be shared by all requests from all users, and thus data with Application scope.

  • Lines 34–35: The init method is executed immediately after the [Application] class is instantiated (presence of the @PostConstruct annotation).
  • Lines 36–73: The method creates the following elements: the list of doctors on line 23, the list of clients on line 24, a dictionary of doctors indexed by their ID on line 25, and the same for clients on line 26. Errors may occur. These are logged in the list on line 28.

The [Error] class is as follows:


package beans;

public class Error {
  
  public Error() {
  }
  
  // field
  private String class;
  private String message;

  // constructor
  public Error(String class, String message) {
    this.setClass(class);
    this.message = message;
  }
  
  // getters and setters
...  
}
  • line 9: the name of an exception class if an exception was thrown,
  • line 10: an error message.

3.6.5.2. The [Form] bean

Its code is as follows:


package beans;

...

@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  public Form() {
  }

  // Application bean
  @Inject
  private Application application;

  // model
  private Long doctorId;
  private Date today = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean renderedError = false;
  private String form2Title;
  private String form3Title;
  private DoctorScheduleDay doctorScheduleDay;
  private Long slotId;
  private Doctor doctor;
  private Client client;
  private Long clientId;
  private DoctorSlotSlot selectedSlot;
  private List<Error> errors;

  @PostConstruct
  private void init() {
    // Did initialization succeed?
    if (application.getError()) {
      // retrieve the list of errors
      errors = application.getErrors();
      // The error view is displayed
      setForms(false, false, false, true);
    }
  }

  // display view
  private void setForms(Boolean form1Rendered, Boolean form2Rendered, Boolean form3Rendered, Boolean errorRendered) {
    this.form1Rendered = form1Rendered;
    this.form2Rendered = form2Rendered;
    this.form3Rendered = form3Rendered;
    this.errorRendered = errorRendered;
  }
.................................................
}
  • lines 5-7: The [Form] class is a bean named "form" with session scope. Note that the class must therefore be serializable.
  • lines 13-14: The form bean has a reference to the application bean. This reference will be injected by the servlet container in which the application runs (presence of the @Inject annotation).
  • lines 17-31: the page templates [form1.xhtml, form2.xhtml, form3.xhtml, error.xhtml]. The display of these pages is controlled by the booleans in lines 19-22. Note that by default, the [form1.xhtml] page is rendered,
  • lines 33–34: the init method is executed immediately after the class is instantiated (presence of the @PostConstruct annotation),
  • lines 35–41: the init method is used to determine which page should be displayed first: normally the [form1.xhtml] page (line 19) unless the application initialization failed (line 36), in which case the [error.xhtml] page will be displayed (line 40).

The [error.xhtml] page is as follows:


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['error.title']}"/></h2>
    <p>
      <h:commandButton value="#{msg['error.home']}" actionListener="#{form.home()}"/>
    </p>
    <hr/>
    <h:dataTable value="#{form.errors}" var="error" headerClass="errorsHeaders" columnClasses="errorClass,errorMessage">
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['error.class']}"/>
        </f:facet>
        <h:outputText value="#{error.class}"/>
      </h:column>
      <h:column>
        <f:facet name="header">
          <h:outputText value="#{msg['error.message']}"/>
        </f:facet>
        <h:outputText value="#{error.message}"/>
      </h:column>
    </h:dataTable>
  </body>
</html>

It uses an <h:dataTable> tag (lines 14–27) to display the list of errors. This results in a page similar to the following:

Image

We will now define the different phases of the application's lifecycle.

3.6.6. Interactions between pages and the model

3.6.6.1. Displaying the home page

If everything goes well, the first page displayed is [form1.xhtml]. This results in the following view:

 

The [form1.xhtml] page is as follows:


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{msg['form1.title']}"/></h2>
    <h:panelGrid columns="3">
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.doctor']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup>
      <div align="center"><h3><h:outputText value="#{msg['form1.day']}"/></h3></div>
      </h:panelGroup>
      <h:panelGroup/>
      <h:selectOneMenu value="#{form.idMedecin}">  
        <f:selectItems value="#{form.doctors}" var="doctor" itemLabel="#{doctor.title} #{doctor.firstName} #{doctor.lastName}" itemValue="#{doctor.id}"/>  
      </h:selectOneMenu>              
      <h:inputText id="day" value="#{form.day}"   required="true" requiredMessage="#{msg['form1.day.required']}" converterMessage="#{msg['form1.day.error']}">
        <f:convertDateTime pattern="dd/MM/yyyy"/>
      </h:inputText>
      <h:message for="day" styleClass="error"/>
    </h:panelGrid>
    <h:commandButton value="#{msg['form1.button.agenda']}" actionListener="#{form.getAgenda}"/>
  </body>
</html>

This page is powered by the following model:


@Named(value = "form")
@SessionScoped
public class Form implements Serializable {

  // Application bean
  @Inject
  private Application application;
  // model
  private Long doctorId;
  private Date today = new Date();
  
// list of doctors
  public List<Doctor> getDoctors() {
    return application.getDoctors();
  }
  // calendar
  public void getAgenda() {
    ...
}
  • The field on line 9 reads from and writes to the value of the list on line 18 of the page. When the page first loads, it sets the value selected in the combo box. On initial load, idMedecin is equal to null, so the first doctor will be selected.
  • the method on lines 13–15 generates the items for the doctors dropdown (line 19 of the page). Each generated option will have as its label (itemLabel) the doctor’s title, last name, and first name, and as its value (itemValue), the doctor’s ID,
  • the field on line 10 provides read/write access to the input field on line 21 of the page. Upon initial display, the current date is shown,
  • Lines 17–19: The getAgenda method handles the click on the [Agenda] button on line 26 of the page. Since there is no navigation (it is always the [index.html] page that is requested), we will often use the actionListener attribute instead of the action attribute. In this case, the method called in the model returns no result.

When the [Agenda] button is clicked,

  • values are posted: the value selected in the doctors dropdown is stored in the model’s idMedecin field, and the day selected in the day field;
  • the model’s getAgenda method is called.

The getAgenda method is as follows:


  // Application bean
  @Inject
  private Application application;

  // model
  private Long doctorId;
  private Date day = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean renderedError = false;
  private String form2Title;
  private DoctorSchedule agendaDoctorSchedule;
  private Doctor doctor;
  private List<Error> errors;

  // schedule
  public void getAgenda() {
    try {
      // retrieve the doctor
      doctor = application.getDoctors().get(doctorId);
      // form title 2
      form2Title = Messages.getMessage(null, "form2.title", new Object[]{doctor.getTitle(), doctor.getFirstName(), doctor.getLastName(), new SimpleDateFormat("dd MMM yyyy").format(day)}).getSummary();
      // the doctor's schedule for a given day
      doctorDailySchedule = application.getBusinessLayer().getDoctorDailySchedule(doctor, day);
      // Display Form 2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareErrorView(th);
    }
  }

  // prepare error view
  private void prepareErrorView(Throwable th) {
    // create the list of errors
    errors = new ArrayList<Error>();
    errors.add(new Error(th.getClass().getName(), th.getMessage()));
    while (th.getCause() != null) {
      th = th.getCause();
      errors.add(new Error(th.getClass().getName(), th.getMessage()));
    }
// the error view is displayed
    setForms(false, false, false, true);
}

Let’s review what the getAgenda method should display:

  • line 21: we retrieve the selected doctor from the doctors dictionary stored in the application bean. To do this, we use its ID, which was posted to idMedecin,
  • line 23: we prepare the title of the page [form2.xhtml] that will be displayed. This message is retrieved from the messages file so that it can be internationalized. This technique was described in section 2.8.5.7, page 135.
  • line 25: we call the [business] layer to calculate the selected doctor’s schedule for the selected day,
  • line 27: [form2.xhtml] is displayed,
  • line 28: if an exception occurs, an error list is constructed (lines 37–42) and the page [error.xhtml] is displayed (line 44).

3.6.6.2. Display a doctor's schedule

The page [form2.xhtml] corresponds to the following view:

The code for the [form2.xhtml] page is as follows:


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jsp/jstl/core">

  <body>
    <h2><h:outputText value="#{form.form2Titre}"/></h2>
    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />
    <h:dataTable value="#{form.doctorSchedule.doctorSlots}" var="doctorSlot" headerClass="reservationsHeaders" columnClasses="slot,client,action">
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.timeSlot']}"/> 
        </f:facet>  
        <h:outputText value="#{creneauMedecinJour.creneau.hdebut}:#{creneauMedecinJour.creneau.mdebut} - #{creneauMedecinJour.creneau.hfin}:#{creneauMedecinJour.creneau.mfin}" />  
      </h:column>  
      <h:column>  
        <f:facet name="header">  
          <h:outputText value="#{msg['form2.client']}"/>  
        </f:facet>  
        <c:if test="#{creneauMedecinJour.rv==null}">
          <h:outputText value=""/>
          <c:otherwise>
            <h:outputText value="#{creneauMedecinJour.rv.client.title} #{creneauMedecinJour.rv.client.firstName} #{creneauMedecinJour.rv.client.lastName}"/>
          </c:otherwise>
        </c:if>
      </h:column>  
      <h:column>  
        <f:facet name="header"/>
        <h:commandLink action="#{form.action()}" value="#{creneauMedecinJour.rv==null ? msg['form2.reserve'] : msg['form2.delete']}">
          <f:setPropertyActionListener value="#{creneauMedecinJour.creneau.id}" target="#{form.idCreneau}"/>
        </h:commandLink>
      </h:column>  
    </h:dataTable>
  </body>
</html>

Recall that the getAgenda method initialized two fields in the model:


// model
  private String form2Title;
private AgendaMedecinJour agendaMedecinJour;

These two fields populate the [form2.xhtml] page:

  • line 10, the page title,
  • line 12: the doctor's schedule is displayed using a three-column <h:dataTable> tag,
  • lines 13–18: the first column displays the time slots,
  • lines 19–30: the second column displays the name of the client who may have booked the time slot, or nothing if not. To make this selection, we use tags from the JSTL Core library referenced on line 7,
  • lines 30–35: the third column displays the [Book] link if the time slot is available, and the [Delete] link if the time slot is booked.

The links in the third column are linked to the following template:


// model
  private Long slotId;

  // action on appointment
  public void action() {
    ...
}
  • The action method is called when the user clicks the Reserve / Delete link (line 32). Note that the action attribute is used here. The method pointed to by this attribute should have the signature String action() because the method must then return a navigation key. However, here it is void action(). This did not cause an error, and we can assume that in this case there is no navigation. This is what was intended. Using actionListener instead of action caused a malfunction,
  • The `idCreneau` field in line 2 will retrieve the ID of the time slot associated with the link that was clicked (line 33 of the page).

3.6.6.3. Deleting an appointment

Let’s examine the code that handles the deletion of an appointment. This corresponds to the following sequence of views:

The code involved in this operation is as follows:


// Application bean
  @Inject
  private Application application;

  // model
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean errorRendered = false;
  private DailyDoctorSchedule dailyDoctorSchedule;
  private Long slotId;
  private DailyDoctorSlot selectedSlot;
  private List<Error> errors;

  // action on appointment
  public void action() {
    // search for the slot in the calendar
    int i = 0;
    Boolean found = false;
    while (!found && i < agendaMedecinJour.getCreneauxMedecinJour().length) {
      if (doctorSchedule.getDoctorSlot()[i].getSlot().getId() == slotId) {
        found = true;
      } else {
        i++;
      }
    }
    // Did we find it?
    if (!found) {
      // That's strange—redisplay form2
      setForms(false, true, false, false);
      return;
    }
    // we found it
    selectedSlot = doctorSchedule.getDoctorSlot()[i];
    // depending on the desired action
    if (selectedSlot.getAppointment() == null) {
      reserve();
    } else {
      delete();
    }
  }
  // reservation

  public void book() {
    ...
  }

  public void delete() {
    try {
      // Delete an appointment
      application.getBusinessLogic().deleteAppointment(selectedTimeSlot.getAppointment());
      // update the calendar
      doctorDailyCalendar = application.getBusinessLogic().getDoctorDailyCalendar(doctor, day);
      // display form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareErrorView(th);
    }
  }
  • line 16: when the action method starts, the ID of the selected time slot has been stored in idCreneau (line 11),
  • lines 18–26: we attempt to retrieve the time slot based on its ID (line 21). We search for it in the current calendar, agendaMedecinJour, from line 10. Normally, we should find it. If not, we do nothing (lines 28–32),
  • line 34: if the slot was found, we retrieve a reference for it and store it in line 12,
  • line 36: check if the selected time slot had an appointment. If so, delete it (line 39); otherwise, reserve one (line 37),
  • line 51: the appointment in the selected slot is deleted. The [business] layer handles this,
  • line 53: we request the doctor’s updated schedule from the [business] layer. We will, of course, see one fewer appointment there. But since the application is multi-user, we may see changes made by other users,
  • line 55: the page [form2.xhtml] is re-displayed,
  • line 58: since the [business] layer was called, exceptions may occur. In this case, we store the exception stack in the error list on line 13 and display them using the [error.xhtml] view.

3.6.6.4. Appointment Scheduling

Appointment scheduling follows this sequence:

The model involved in this action is as follows:


// model
  private Date day = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean errorRendered = false;
  private String form3Title;
  private DoctorSchedule agendaDoctorSchedule;
  private Doctor doctor;
  private DoctorSlotScheduled selectedSlot;
  private List<Error> errors;

  // action on appointment
  public void action() {
...
    // found
    selectedSlot = doctorSchedule.getDoctorSlots()[i];
    // depending on the desired action
    if (selectedSlot.getAppointment() == null) {
      reserve();
    } else {
      delete();
    }
  }
  // reservation

    public void book() {
    try {
      // form title 3
      form3Title = Messages.getMessage(null, "form3.title", new Object[]{doctor.getTitle(), doctor.getFirstName(), doctor.getLastName(), new SimpleDateFormat("dd MMM yyyy").format(day),
                selectedSlot.getSlot().getStartHour(), selectedSlot.getSlot().getStartMinute(), selectedSlot.getSlot().getEndHour(), selectedSlot.getSlot().getEndMinute()}).getSummary();
      // client selected from the dropdown
      idClient=null;
      // display form 3
      setForms(false, false, true, false);
    } catch (Throwable th) {
      // error view
      prepareErrorView(th);
    }
  }
  • line 14: if the selected time slot has no appointment, then it is a reservation,
  • line 30: we prepare the page title [form3.xhtml] using the same technique as for the page title [form2.xhtml],
  • line 34: in this form, there is a combo box whose value is populated by idClient. We set the value of this field to null so that no one is selected,
  • line 36: we display the page [form3.xhtml],
  • line 39: or the error page if an exception occurred.

The page [form3.xhtml] is as follows:


<?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://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <body>
    <h2><h:outputText value="#{form.form3Titre}"/></h2>
    <h:panelGrid columns="2">
      <h:outputText value="#{msg['form3.client']}"/>
      <h:selectOneMenu value="#{form.idClient}">
        <f:selectItems value="#{form.clients}" var="client" itemLabel="#{client.title} #{client.firstName} #{client.lastName}" itemValue="#{client.id}"/>
      </h:selectOneMenu>
      <h:panelGroup>
        <h:commandButton value="#{msg['form3.submit']}" actionListener="#{form.submitRv}" />
        <h:commandButton value="#{msg['form3.cancel']}" actionListener="#{form.cancelRv}"/>
      </h:panelGroup>
    </h:panelGrid>
  </body>
</html>

This page is powered by the following model:


// Application bean
  @Inject
  private Application application;

  // model
  private Long clientId;

  // list of clients
  public List<Client> getClients() {
    return application.getClients();
  }
  • line 6: the client ID populates the value attribute of the client combo box on line 12 of the page. It sets the selected combo box item,
  • lines 9–11: the getClients method populates the dropdown (line 13). The label (itemLabel) for each option is [Title First Name Last Name] for the client, and the associated value (itemValue) is the client’s ID. It is therefore this value that will be submitted.

3.6.6.5. Confirming an appointment

Confirming an appointment follows this sequence:

and corresponds to clicking the [Confirm] button:


        <h:commandButton value="#{msg['form3.valider']}" actionListener="#{form.validerRv}" />

The [Form].validerRv method will therefore handle this event. Its code is as follows:


  // Application bean
  @Inject
  private Application application;
  
  // model
  private Date day = new Date();
  private Boolean form1Rendered = true;
  private Boolean form2Rendered = false;
  private Boolean form3Rendered = false;
  private Boolean errorRendered = false;
  private Long slotId;
  private Long clientId;
  private List<Error> errors;

  // Appointment validation
  public void validateRv() {
    try {
      // retrieve an instance of the selected time slot
      TimeSlot timeSlot = application.getBusinessLogic().getTimeSlotById(timeSlotId);
      // add the appointment
      application.getMetier().addAppointment(day, timeSlot, application.getClients().get(clientId));
      // update the calendar
      doctorDayCalendar = application.getDepartment().getDoctorDayCalendar(doctor, day);
      // display form2
      setForms(false, true, false, false);
    } catch (Throwable th) {
      // error view
      prepareErrorView(th);
    }
}
  • line 12: Before the `validerRv` method executes, the `idClient` field has received the ID of the customer selected by the user,
  • line 19: using the time slot ID stored in a previous step (the bean is session-scoped), we request a reference to the time slot itself from the [business] layer,
  • line 21: the [business] layer is asked to add an appointment for the selected day (day), the selected time slot (slot), and the selected client (idClient),
  • line 23: we ask the [business] layer to refresh the doctor’s calendar. We will see the added appointment along with any changes other users of the application may have made,
  • line 25: the calendar [form2.xhtml] is re-displayed,
  • line 28: display the error page if an error occurs.

3.6.6.6. Canceling an appointment

This corresponds to the following sequence:

The [Cancel] button on the [form3.xhtml] page is as follows:


        <h:commandButton value="#{msg['form3.cancel']}" actionListener="#{form.cancelRv}"/>

The [Form].cancelAppointment method is therefore called:


  // Cancel appointment
  public void cancelAppointment() {
    // display form2
    setForms(false, true, false, false);
}

3.6.6.7. Back to the home page

There is one more action to look at, in the following sequence:

The code for the [Home] button on the [form2.xhtml] page is as follows:


    <h:commandButton value="#{msg['form2.accueil']}" action="#{form.accueil}" />

The [Form].accueil method is as follows:


  public void home() {
    // display the home page
    setForms(true, false, false, false);
}

3.7. Conclusion

We have built the following application:

We focused on the application’s functionality rather than its user interface. The latter will be improved using the PrimeFaces component library. We have built a basic application that nonetheless represents a layered Java EE architecture using EJBs. The application can be improved in various ways:

  • authentication is required. Not everyone is authorized to add or delete appointments,
  • we should be able to scroll the calendar forward and backward when looking for a day with available slots,
  • it should be possible to request a list of days when a doctor has available slots. Indeed, if the doctor is an ophthalmologist, appointments are typically booked six months in advance,
  • ...

3.8. Testing with Eclipse

3.8.1. The [DAO] layer

  • In [1], import the EJB project for the [DAO] layer and its client,
  • in [2], we select the [DAO] layer’s EJB project and run it [3],
  • in [4], run it on a server,
  • In [5], only the Glassfish server is offered because it is the only one with an EJB container,
  • In [6], the EJB module has been deployed,
  • In [7], the logs are displayed:
1
2
3
4
5
6
7
8
Info: Hibernate Validator 4.2.0.Final

Info: Created EjbThreadPoolExecutor with thread-core-pool-size 16 thread-max-pool-size 32 thread-keep-alive-seconds 60 thread-queue-capacity 2147483647 allow-core-thread-timeout false 
...

Info: EJB5181: Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoRemote, java:global/mv-rdvmedecins-ejb-dao-jpa/DaoJpa!rdvmedecins.dao.IDaoLocal]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Info: mv-rdvmedecins-ejb-dao-jpa was deployed in 5,523 ms.

These are the ones we had with NetBeans.

  • In [7A] [7B] we run the client's JUnit test,
  • in [8], the test passes,
  • in [9], the console logs.

In [10], the EJB application is unloaded.

3.8.2. The [business] layer

  • In [1], import the four Maven projects from the [business] layer,
  • In [2], select the enterprise project and run it In [3], on a GlassFish server [4] [5],
  • in [6], the enterprise project has been deployed on GlassFish,
  • In [7], we look at the GlassFish logs,
1
2
3
4
Info: EJB5181: Portable JNDI names for EJB DaoJpa: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-dao-jpa-1.0-SNAPSHOT/DaoJpa!rdvmedecins.dao.IDaoRemote]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB DaoJpa: [rdvmedecins.dao#rdvmedecins.dao.IDaoRemote, rdvmedecins.dao]
Info: EJB5181: Portable JNDI names for EJB Metier: [java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierLocal, java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote]
Info: EJB5182: GlassFish-specific (non-portable) JNDI names for EJB Metier: [rdvmedecins.metier.service.IMetierRemote#rdvmedecins.metier.service.IMetierRemote, rdvmedecins.metier.service.IMetierRemote]

On line 3, we note the portable name of the EJB [Metier] and paste it into the console client for this EJB:


public class ClientRdvMedecinsMetier {

  // the name of the EJB [Metier]'s remote interface
  private static String IDaoRemoteName = "java:global/mv-rdvmedecins-metier-dao-ear/mv-rdvmedecins-ejb-metier-1.0-SNAPSHOT/Metier!rdvmedecins.metier.service.IMetierRemote";
  // today's date
private static Date today = new Date();
  • In [8], we run the console client,
  • in [9], its logs.
  • in [10], the enterprise application is unloaded;

3.8.3. The [web] layer

  • In [1], we import the three Maven projects from the [web] layer. The one with the .ear extension is the enterprise project that needs to be deployed on GlassFish,
  • in [2], we run it,
  • on the GlassFish server [3],
  • in [4], the enterprise application has been successfully deployed,
  • in [5], we enter the application’s URL in Eclipse’s internal browser.