Le monde du Java EE propose différentes stratégies pour exposer une couche de données aux applications qui vont venir la consommer. Parmi les plus utilisés, on peut citer les web services Soap ou Rest, les files JMS, les bus de services ou encore les EJB, chaque solution apportant son lot d’avantages mais aussi de contraintes. La couche modèle JPA mise en place dans le billet précédent reposait déjà sur un mécanisme d’EJB, les entités, c’est donc naturellement cette solution de services que nous allons privilégier dans un premier temps.

Dans cet article, l’objectif va être de greffer sur la couche modèle JPA construite précédemment une brique de services métiers EJB. Une fois encore, nous allons créer et outiller notre projet EJB dans Eclipse puis nous y ajouterons des tests JUnit que nous ferons fonctionner dans un contexte de serveur Glassfish embarqué.
Dans la continuité du précédent article, nous allons constituer la couche de services métiers élémentaires nécessaires à une application de gestion de résultats de rencontres sportives.
Archi_javaEE_EJB

Détails techniques

L’outillage utilisé ici est identique à celui de l’article précédent et ne nécessite aucun ajout. Je le liste ici pour rappel :
– Eclipse Standard 4.3.1 : site d’Eclipse
– Java SE Development Kit 7u45 : site d’Oracle
– Oracle 12c, installé comme décrit dans ce précédent billet
– Glassfish 4, dont nous nous servirons pour disposer d’une implémentation de JPA 2.1 : site de GlassFish
– le plugin WTP 3.5.2 (Web Tools Plateform) pour Eclipse depuis le repository suivant : http://download.eclipse.org/webtools/repository/kepler/

Prérequis sur le modèle

Le projet JPA avait été construit pour un usage simplifié en développement et dans ce contexte il était alors avantageux de conserver le contrôle sur les transactions de façon à les gérer à la demande depuis nos tests JUnit. Maintenant que nous allons exposer ce modèle à une couche de service, il est préférable que la gestion transactionnelle soit déléguée à cette dernière, ou plus précisément au container dans lequel elle est exécutée.
Pour faire cela nous allons modifier le fichier « persistence.xml » pour changer le type de transaction de « RESOURCE_LOCAL » vers « JTA » (pour « Java Transaction API ») en précisant le nom JNDI de la source de données à utiliser. Je vous conseille de conserver les deux configurations dans des fichiers séparés sauvegardés dans le répertoire « META-INF », vous pourrez ainsi facilement basculer d’une configuration à l’autre selon que vous travailliez sur le modèle ou sur la couche de service.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
	xmlns="http://xmlns.jcp.org/xml/ns/persistence" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
		http://xmlns.jcp.org/xml/ns/persistence 
		http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
	<persistence-unit name="G2S_JPA" transaction-type="JTA">
		<!-- Eclipse link JPA implementation -->
		<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>

		<!-- Data source definition -->
		<jta-data-source>java:app/jdbc/Oracle12c</jta-data-source>
		<class>g2s.model.Participant</class>
		<class>g2s.model.Rencontre</class>
		<class>g2s.model.Resultat</class>
		<properties>
			<property name="eclipselink.target-database" value="Oracle" />
			<property name="eclipselink.ddl-generation" value="create-tables" />
		</properties>
	</persistence-unit>
</persistence>

Vous noterez qu’à cet instant la source de données identifiée par le nom JNDI « java:app/jdbc/Oracle12c » n’existe pas. Nous allons y remédier par la suite.

Création du projet EJB dans Eclipse

L’environnement Eclipse est déjà configuré à la suite du précédent article, aucun ajout n’est nécessaire.

Le projet EJB qui va être constitué aura pour finalité de devenir la couche de services métier de notre gestionnaire de scores, nous allons donc le nommer « G2S_Services ». Depuis la vue « Project Explorer », faites un clic droit puis « New > Project… » et sélectionnez « EJB Project », puis remplissez les champs du formulaire comme sur l’écran suivant :
G2S_EJB_1

Nous allons compléter immédiatement les dépendances de notre projet. Comme précédemment, nous aurons besoin des bibliothèques pour JUnit que vous allez ajouter depuis le « Java Build Path ». Nous pourrions nous en tenir à ce simple ajout puisque les références à notre serveur GlassFish ont été ajoutées automatiquement à la création du projet. Mais la façon dont nous allons exécuter nos tests JUnit va nécessiter un second ajout au classpath.

En effet, si les tests de notre modèle étaient directement pris en charge par Eclipse, ici nous allons nous approcher du contexte opérationnel, qui sera à terme le serveur GlassFish, en utilisant sa version dite « embedded » ou « embarqué ». Il s’agit d’une version allégée du serveur qui peut être démarré par Eclipse pour, à la volée, déployer les ressources serveurs dont les EJB, réaliser les tests et s’arrêter. Pour que notre projet dispose de cette possibilité, ajoutez au classpath le jar « glassfish-embedded-static-shell.jar » qui se trouve dans un répertoire « lib/embedded » du serveur :
G2S_EJB_2

Enfin, puisque notre couche de service va venir exploiter la couche modèle, il faut permettre à la première de voir la seconde.

Il est possible de créer ce lien de différentes façons, par exemple en produisant un jar du modèle et en l’intégrant au classpath des services. Mais ici, puisque nous travaillons avec les deux projets dans le même workspace, nous allons simplement indiquer au projet EJB qu’il doit référencer le projet JPA comme dans la capture ci-après.
G2S_EJB_3

Création des classes d’objets des services

Notre projet est opérationnel dans Eclipse, il va être temps d’y créer les classes de notre couche de service. Pour garantir le cloisonnement entre les couches modèle et présentation, notre brique EJB va venir consommer les DAO tout en présentant à la façade ses propres classes logiques. Nous allons donc commencer par constituer les classe suffixées par « _so » pour « Service Object ».
Dans un souci de simplification, je conseille ici de copier les classes à partir de l’archive mise à disposition, mais vous pouvez aussi choisir de lire les instructions puis de coder par vous même les classes attendues. Ici nous allons créer trois classes « Participant_so », « Rencontre_so » et « Resultat_so » dans un nouveau package « g2s.ejb.so ».
Ces classes sont relativement proche des entités constituant le modèle puisqu’elles possèdent sensiblement les mêmes attributs sans l’aspect persistance. Par exemple, ici « Participant_so » contient une liste de résultats, un peu comme l’entité « Participant » pouvait accéder aux « Resultat » au travers de la relation « OneToMany » :

package g2s.ejb.so;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import g2s.model.Participant;
import g2s.model.Resultat;

/**
 * Classe de service pour gestion de participant
 * 
 * @author Tif
 * 
 */
public class Participant_so {

	public Long identifiant;
	public String nom;
	public List<Resultat_so> listeResultats;

	/**
	 * Constructeur
	 * 
	 * @param pParticipant
	 */
	public Participant_so(Participant pParticipant) {
		this.identifiant = pParticipant.getPart_id();
		this.nom = pParticipant.getPart_nom();

		List<Resultat_so> listeResultatsTemp = new ArrayList<Resultat_so>();
		if (pParticipant.getResultat() != null
				&& !pParticipant.getResultat().isEmpty()) {
			Iterator<Resultat> iter = pParticipant.getResultat().iterator();
			while (iter.hasNext()) {
				listeResultatsTemp.add(new Resultat_so(iter.next()));
			}
		}
		listeResultats = listeResultatsTemp;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringBuffer toS = new StringBuffer();
		toS.append("participant ").append(this.getIdentifiant());
		toS.append(" nomme ").append(this.getNom());
		toS.append(" : ").append("\n");

		Resultat_so resulTemp;
		Iterator<Resultat_so> iterListRes = this.getListeResultats().iterator();
		while (iterListRes.hasNext()) {
			resulTemp = iterListRes.next();
			toS.append(" a obtenu ");
			toS.append(resulTemp.getScore());
			toS.append(" a ");
			toS.append(resulTemp.getRencontreLieu());
			toS.append(" le ");
			toS.append(resulTemp.getRencontreDate());
			toS.append("\n");
		}

		return toS.toString();
	}

	/**
	 * @return identifiant
	 */
	public Long getIdentifiant() {
		return this.identifiant;
	}

	/**
	 * @param pIdentifiant
	 *            , set identifiant
	 */
	public void setIdentifiant(Long pIdentifiant) {
		this.identifiant = pIdentifiant;
	}

	/**
	 * @return nom
	 */
	public String getNom() {
		return this.nom;
	}

	/**
	 * @param pNom
	 *            , set nom
	 */
	public void setNom(String pNom) {
		this.nom = pNom;
	}

	/**
	 * @return listeResultats
	 */
	public List<Resultat_so> getListeResultats() {
		return this.listeResultats;
	}

	/**
	 * @param pListeResultats
	 *            , set listeResultats
	 */
	public void setListeResultats(List<Resultat_so> pListeResultats) {
		this.listeResultats = pListeResultats;
	}

}

En revanche, sur la classe « Resultat_so », il a été préféré de conserver le nom du participant plutôt qu’un identifiant afin d’exploiter des objets dont les informations seront directement utilisables par la couche de présentation.

package g2s.ejb.so;

import java.util.Date;

import g2s.model.Resultat;

/**
 * Classe de service pour gestion de resultat
 * 
 * @author Tif
 *
 */
public class Resultat_so {

	public Long identifiant;
	public Double score;
	public String rencontreLieu;
	public Date rencontreDate;
	public String participantNom;

	/**
	 * Constructeur
	 * @param pResultat
	 */
	public Resultat_so(Resultat pResultat) {
		this.identifiant = pResultat.getRes_id();
		this.score = pResultat.getRes_score();
		this.rencontreLieu = pResultat.getRencontre().getRenc_lieu();
		this.rencontreDate = pResultat.getRencontre().getRenc_date();
		this.participantNom = pResultat.getParticipant().getPart_nom();
	}

	/**
	 * @return identifiant
	 */
	public Long getIdentifiant() {
		return this.identifiant;
	}

	/**
	 * @param pIdentifiant
	 *            , set identifiant
	 */
	public void setIdentifiant(Long pIdentifiant) {
		this.identifiant = pIdentifiant;
	}

	/**
	 * @return score
	 */
	public Double getScore() {
		return this.score;
	}

	/**
	 * @param pScore
	 *            , set score
	 */
	public void setScore(Double pScore) {
		this.score = pScore;
	}

	/**
	 * @return rencontreLieu
	 */
	public String getRencontreLieu() {
		return this.rencontreLieu;
	}

	/**
	 * @param pRencontreLieu
	 *            , set rencontreLieu
	 */
	public void setRencontreLieu(String pRencontreLieu) {
		this.rencontreLieu = pRencontreLieu;
	}

	/**
	 * @return rencontreDate
	 */
	public Date getRencontreDate() {
		return this.rencontreDate;
	}

	/**
	 * @param pRencontreDate
	 *            , set rencontreDate
	 */
	public void setRencontreDate(Date pRencontreDate) {
		this.rencontreDate = pRencontreDate;
	}

	/**
	 * @return participantNom
	 */
	public String getParticipantNom() {
		return this.participantNom;
	}

	/**
	 * @param pParticipantNom
	 *            , set participantNom
	 */
	public void setParticipantNom(String pParticipantNom) {
		this.participantNom = pParticipantNom;
	}

}

On notera que pour ces trois classes, nous avons mis en place un constructeur prenant en paramètre une classe d’entité du modèle. Ces méthodes vont permettre de faire un mapping rapide pour passer d’une entité à un objet de la couche service qui sera par la suite exposé à la couche de présentation.

Création du service

Pour simplifier, nous allons créer une unique classe de services, « G2SService », contenant les quelques méthodes élémentaires nécessaires à une gestion de scores de rencontres sportives. Ainsi nous mettrons en place des services stratégiques permettant de créer ou retrouver, selon le besoin, des participants, des rencontres et des résultats, plus des méthodes de listing (et une méthode de suppression pour exemple). Créez la classe suivante dans le package « g2s.ejb » :

package g2s.ejb;

import g2s.dao.ParticipantDAO;
import g2s.dao.RencontreDAO;
import g2s.dao.ResultatDAO;
import g2s.ejb.so.Participant_so;
import g2s.ejb.so.Rencontre_so;
import g2s.model.Participant;
import g2s.model.Rencontre;
import g2s.model.Resultat;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;

/**
 * Service de gestion de scores
 * 
 * @author Tif
 * 
 */
@Stateless(mappedName = "G2SService")
public class G2SService {

	@PersistenceContext
	private EntityManager entManager;

	private static ParticipantDAO particpantDAO = new ParticipantDAO();
	private static RencontreDAO rencontreDAO = new RencontreDAO();
	private static ResultatDAO resultatDAO = new ResultatDAO();
	
	private final static String ERR_NON_TROUVE = "Objet recherche non trouve";
	private final static String ERR_MAJ_IMPOSSIBLE = "MAJ impossible";

	/**
	 * Trouve tous les participants enregistres
	 * 
	 * @return la liste des participants
	 */
	@Transactional
	public List<Participant_so> listerLesParticipants() {
		List<Participant_so> listePartSo = new ArrayList<>();
		particpantDAO.setEntityManager(entManager);

		// Recuperation du participant
		List<Participant> listeParticipant = particpantDAO
				.trouverTousLesParticipants();
		if (!listeParticipant.isEmpty()) {
			Iterator<Participant> iter = listeParticipant.iterator();
			while (iter.hasNext()) {
				listePartSo.add(new Participant_so(iter.next()));
			}
		}

		return listePartSo;
	}

	/**
	 * Trouve toutes les rencontres enregistrees
	 * 
	 * @return la liste des rencontres
	 */
	@Transactional
	public List<Rencontre_so> listerLesRencontres() {
		List<Rencontre_so> listeRencSo = new ArrayList<>();
		rencontreDAO.setEntityManager(entManager);

		// Recuperation du participant
		List<Rencontre> listeRencontre = rencontreDAO
				.trouverToutesLesRencontres();
		if (!listeRencontre.isEmpty()) {
			Iterator<Rencontre> iter = listeRencontre.iterator();
			while (iter.hasNext()) {
				listeRencSo.add(new Rencontre_so(iter.next()));
			}
		}

		return listeRencSo;
	}

	/**
	 * Trouve la liste des rencontres auxquelles est inscrit le participant dont
	 * le nom est transmis
	 * 
	 * @param nomParticipant
	 * @return la liste des rencontres
	 */
	@Transactional
	public List<Rencontre_so> trouverRencontresParParticipant(
			String nomParticipant) {
		List<Rencontre_so> listeRencontres = new ArrayList<>();
		particpantDAO.setEntityManager(entManager);

		// Recuperation du participant
		Participant participant = particpantDAO
				.participantParNom(nomParticipant);
		if (participant != null && !participant.getResultat().isEmpty()) {
			// Identification des rencontres pour haque resultat
			Iterator<Resultat> iter = participant.getResultat().iterator();
			Resultat resTemp;
			while (iter.hasNext()) {
				resTemp = iter.next();
				listeRencontres.add(new Rencontre_so(resTemp.getRencontre()));
			}
		}

		return listeRencontres;
	}

	/**
	 * Enregistre ou met a jour si il existe le participant pour le nom transmis
	 * 
	 * @param nomParticipant
	 * @return
	 * @throws Exception 
	 */
	@Transactional
	public Participant_so enregistreOuMettreAjourParticipant(
			Long idpart, String nomParticipant) throws Exception {
		particpantDAO.setEntityManager(entManager);
		Participant participant;
		Participant participantParParam = particpantDAO.participantParNom(nomParticipant);
		if (idpart == null) {
			// aucun id tansmis -> recherche ou creation
			if (participantParParam == null) {
				// pas de participant trouve pour le nom
				participant = new Participant();
				participant.setPart_nom(nomParticipant);
				particpantDAO.creer(participant);
			} else {
				// une rencontre existe pour le lieu et la date
				participant = participantParParam;
			}
		} else {
			// id transmis -> maj si possible
			if (participantParParam == null) {
				// lecture par id
				participant = particpantDAO.lire(idpart);
				if (participant == null) {
					// pas de participant trouve
					throw new Exception(ERR_NON_TROUVE);
				} else {
					// participant trouve -> maj
					participant.setPart_nom(nomParticipant);
					particpantDAO.editer(participant);
				}
			} else {
				// un participant existe -> maj impossible
				throw new Exception(ERR_MAJ_IMPOSSIBLE);
			}
		}

		return new Participant_so(participant);
	}

	/**
	 * Enregistre ou met a jour si elle existe la rencontre pour le lieu et la
	 * date transmis
	 * 
	 * @param lieu
	 * @param date
	 * @param idrenc
	 * @return la rencontre
	 * @throws Exception 
	 */
	@Transactional
	public Rencontre_so enregistreOuMettreAjourRencontre(Long idrenc, String lieu,
			Timestamp date) throws Exception {
		rencontreDAO.setEntityManager(entManager);
		Rencontre rencontre;
		Rencontre rencontreParParam = rencontreDAO.rencontreParLieuEtDate(lieu, date);
		if (idrenc == null) {
			// aucun id tansmis -> recherche ou creation
			if (rencontreParParam == null) {
				// pas de rencontre trouvee pour le lieu et la date
				rencontre = new Rencontre();
				rencontre.setRenc_date(date);
				rencontre.setRenc_lieu(lieu);
				rencontreDAO.creer(rencontre);
			} else {
				// une rencontre existe pour le lieu et la date
				rencontre = rencontreParParam;
			}
		} else {
			// id transmis -> maj si possible
			if (rencontreParParam == null) {
				// lecture par id
				rencontre = rencontreDAO.lire(idrenc);
				if (rencontre == null) {
					// pas de rencontre trouvee
					throw new Exception(ERR_NON_TROUVE);
				} else {
					// rencontre trouvee -> maj
					rencontre.setRenc_date(date);
					rencontre.setRenc_lieu(lieu);
					rencontreDAO.editer(rencontre);
				}
			} else {
				// une rencontre existe -> maj impossible
				throw new Exception(ERR_MAJ_IMPOSSIBLE);
			}
		}

		return new Rencontre_so(rencontre);
	}

	/**
	 * Enregistre le resultat pour 1 participant a une rencontre
	 * 
	 * @param idRencontre
	 * @param idPart1
	 * @param score1
	 */
	@Transactional
	public void enregistreResultatsParticipant(Long idRencontre, Long idPart1,
			double score1) {
		rencontreDAO.setEntityManager(entManager);
		resultatDAO.setEntityManager(entManager);

		// recuperation rencontre et participants
		Rencontre rencontre = rencontreDAO.lire(idRencontre);
		Participant participant1 = particpantDAO.lire(idPart1);

		Resultat resultatParticipant1 = new Resultat();
		resultatParticipant1.setRencontre(rencontre);
		resultatParticipant1.setParticipant(participant1);
		resultatParticipant1.setRes_score(score1);
		resultatDAO.creer(resultatParticipant1);
	}

	/**
	 * Enregistre le resultat pour une rencontre entre 2 participants
	 * 
	 * @param idRencontre
	 * @param idPart1
	 * @param idPart2
	 * @param score1
	 * @param score2
	 */
	@Transactional
	public void enregistreResultatsMatch(Long idRencontre, Long idPart1,
			Long idPart2, double score1, double score2) {
		enregistreResultatsParticipant(idRencontre, idPart1, score1);
		enregistreResultatsParticipant(idRencontre, idPart2, score2);
	}

	/**
	 * Trouve et supprime le participant dont le nom est passe en parametre si
	 * il existe
	 * 
	 * @param nomParticipant
	 */
	@Transactional
	public void supprimerParticipant(String nomParticipant) {
		particpantDAO.setEntityManager(entManager);
		Participant participant = particpantDAO
				.participantParNom(nomParticipant);
		if (participant != null) {
			particpantDAO.supprimer(participant);
		}
	}

}

Les points clés de cette classe sont :
– l’annotation « @Stateless(mappedName = « G2SService ») » qui déclare la classe comme étant un EJB de type « stateless » identifié par « G2SService ».
– les annotations « @PersistenceContext » au dessus de l’EntityManager et « @Transactional » au dessus de chaque méthode, qui permettent la délégation de la gestion des transactions au container de l’EJB.
– les signatures des méthodes qui exploitent des objets simples en entrée mais retourne des objets « _so » spécifiques de la couche de service.

Tests JUnit

Notre couche de service semble opérationnelle et il est temps de la tester pour s’en assurer. Pour cela nous allons créer des tests JUnit que nous exécuterons à l’aide du serveur GlassFish embarqué. En théorie, il serait préférable de réaliser un test autonome par méthode, mais selon l’expansion du projet, cette stratégie peut vite se révéler couteuse en temps de maintenance des tests. Pour ma part, je trouve aussi simple de réaliser quelques tests stratégiques permettant de passer en une seule fois plusieurs des méthodes à tester.
Ici, nous allons reprendre le cas de test du tournoi des six nations. Créez la classe suivante « TournoiSixNationsServiceTest » dans un nouveau package « g2s.ejb.tests » :

/**
 * 
 */
package g2s.ejb.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import g2s.dao.tests.TournoiSixNationsDAOTest;
import g2s.ejb.G2SService;
import g2s.ejb.so.Participant_so;
import g2s.ejb.so.Rencontre_so;

import java.util.Iterator;
import java.util.List;

import javax.ejb.embeddable.EJBContainer;

import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

/**
 * @author Tif
 * 
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TournoiSixNationsServiceTest {

	private static EJBContainer container;

	@Before
	public void initTests() {
		container = EJBContainer.createEJBContainer();
	}

	@After
	public void cleanTests() throws InterruptedException {
		container.close();
	}

	@Test
	public void creerTournoi() throws Exception {
		G2SService service = (G2SService) container.getContext().lookup(
				"java:global/classes/G2SService!g2s.ejb.G2SService");
		Long idRencontre;
		Long idPart1;
		Long idPart2;
		double score1 = 0d;
		double score2 = 0d;
		for (String keyRencontre : TournoiSixNationsDAOTest.RENCONTRES.keySet()) {
			// recuperation rencontre
			idRencontre = service.enregistreOuMettreAjourRencontre(null,
					keyRencontre.split("_")[1],
					TournoiSixNationsDAOTest.RENCONTRES.get(keyRencontre))
					.getIdentifiant();

			// recherche participants et scores
			idPart1 = 0l;
			idPart2 = 0l;
			for (String keyResultat : TournoiSixNationsDAOTest.RESULTATS
					.keySet()) {
				if (keyResultat.split("_")[0].equalsIgnoreCase((keyRencontre
						.split("_")[0]))) {
					if (idPart1 == 0l) {
						idPart1 = service.enregistreOuMettreAjourParticipant(
								null, keyResultat.split("_")[1])
								.getIdentifiant();
						score1 = TournoiSixNationsDAOTest.RESULTATS
								.get(keyResultat);
					} else {
						idPart2 = service.enregistreOuMettreAjourParticipant(
								null, keyResultat.split("_")[1])
								.getIdentifiant();
						score2 = TournoiSixNationsDAOTest.RESULTATS
								.get(keyResultat);
					}
				}
			}
			service.enregistreResultatsMatch(idRencontre, idPart1, idPart2,
					score1, score2);
			System.out.println("Enregistrement Resultats Rencontre "
					+ idRencontre + " : participant 1 : " + idPart1 + " => "
					+ score1 + " : participant 2 : " + idPart2 + " => "
					+ score2);
		}
	}

	@Test
	public void testerTournoi() throws Exception {
		G2SService service = (G2SService) container.getContext().lookup(
				"java:global/classes/G2SService!g2s.ejb.G2SService");

		Participant_so partFrance = service.enregistreOuMettreAjourParticipant(
				null, "France");
		assertNotNull("participant France", partFrance);
		assertEquals("nombre de resultats France", 5, partFrance
				.getListeResultats().size());
		System.out.println(partFrance.toString());

		List<Rencontre_so> listeRencontresFrance = service
				.trouverRencontresParParticipant("France");
		assertNotNull("liste de rencontres France", listeRencontresFrance);
		assertEquals("nombre de rencontres France", 5,
				listeRencontresFrance.size());
		Iterator<Rencontre_so> iterListeRencFrance = listeRencontresFrance
				.iterator();
		while (iterListeRencFrance.hasNext()) {
			System.out.println(iterListeRencFrance.next().toString());
		}

		List<Rencontre_so> listeRencontres = service.listerLesRencontres();
		assertNotNull("liste de rencontres", listeRencontres);
		assertEquals("nombre de rencontres", 15, listeRencontres.size());
		Iterator<Rencontre_so> iterListeRenc = listeRencontres.iterator();
		while (iterListeRenc.hasNext()) {
			System.out.println(iterListeRenc.next().toString());
		}
	}

}

Les éléments particuliers ici sont :
les annotations spécifiques JUnit : "@FixMethodOrder(MethodSorters.NAME_ASCENDING)", "@Before", "@After" et "@Test"
Ces annotations avaient déjà été rencontrées lors de la mise en place de couche modèle. Elles déclarent les méthodes comme étant des tests JUnit, à exécuter dans l’ordre alphabétique et dont certains traitements sont à effectuer systématiquement avant et après chaque test.

"(javax.ejb.embeddable.EJBContainer)EJBContainer container = EJBContainer.createEJBContainer();" et "container.close();"
Il s’agit des commandes de démarrage et d’arrêt du serveur GlassFish embarqué.

G2SService service = (G2SService) container.getContext().lookup("java:global/classes/G2SService!g2s.ejb.G2SService");
Notre classe de service « G2SService » a été prise en charge par le container du fait de l’annotation « @Stateless(mappedName = « G2SService ») ». Il s’agit ici de la façon dont nous allons en récupérer une instance dans le container à l’aide du nom JNDI.

Gestion du data-source par « glassfish-resources.xml »

Si vous essayez de lancer le test à ce stade, le serveur GlassFish embarqué démarre mais une erreur bloquante est levée qui correspond à la non reconnaissance du data-source paramétré pour le fonctionnement JTA : java.lang.RuntimeException: Invalid resource : { ResourceInfo : (jndiName=java:app/jdbc/Oracle12c__pm).
Pour que votre serveur embarqué soit capable de reconnaitre le nom JNDI fournit, vous pouvez choisir de démarrer le serveur GlassFish pour vous rendre sur la console d’administration et y ajouter le pool de connexion et la source de données; le serveur embarqué profitant des ressources de l’installation complète, il sera alors capable de l’utiliser. Mais une autre solution est d’indiquer, depuis le projet, que les ressources spécifiées doivent être créées lorsque celui-ci est déployé. Pour cela, nous allons créer un fichier « glassfish-resources.xml » dans le répertoire « META-INF » du projet. Copiez y ce qui suit :

<?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"
		associate-with-thread="false" connection-creation-retry-attempts="0"
		connection-creation-retry-interval-in-seconds="10"
		connection-leak-reclaim="false" connection-leak-timeout-in-seconds="0"
		connection-validation-method="auto-commit"
		datasource-classname="oracle.jdbc.pool.OracleConnectionPoolDataSource"
		fail-all-connections="false" idle-timeout-in-seconds="300"
		is-connection-validation-required="false"
		is-isolation-level-guaranteed="true" lazy-connection-association="false"
		lazy-connection-enlistment="false" match-connections="false"
		max-connection-usage-count="0" max-pool-size="32"
		max-wait-time-in-millis="60000" name="Oracle12cPool"
		non-transactional-connections="false" pool-resize-quantity="2"
		res-type="javax.sql.ConnectionPoolDataSource"
		statement-timeout-in-seconds="-1" steady-pool-size="8"
		validate-atmost-once-period-in-seconds="0" wrap-jdbc-objects="false">
		<property name="serverName" value="localhost" />
		<property name="portNumber" value="1521" />
		<property name="databaseName" value="orcl12c" />
		<property name="User" value="user_test" />
		<property name="Password" value="user_test" />
		<property name="URL" value="jdbc:oracle:thin:@localhost:1521:orcl12c" />
		<property name="driverClass" value="oracle.jdbc.OracleDriver" />
	</jdbc-connection-pool>
	<jdbc-resource enabled="true" jndi-name="jdbc/Oracle12c"
		object-type="user" pool-name="Oracle12cPool" />
</resources>

Je ne reviens pas ici en détail sur les différents attributs utilisés, mais vous constaterez que nous retrouvons les paramètres classiques de connexion : serveur, port, nom du schéma, utilisateur, etc. A noter aussi que ce fichier aurait pu être créé ailleurs, par exemple dans le projet modèle ou encore dans un EAR si nous avions choisi un tel packaging (dans répertoire « META-INF » dans les deux cas).
Vous pouvez maintenant faire tourner vos tests JUnit en faisant un clic droit « Run as > JUnit test ». Le serveur embarqué est démarré par l’exécution de la méthode annotée par « Before ». Il exploite alors le fichier de ressources xml pour créer le pool de connexion et la source de données spécifiée. Et celle-ci est donc disponible via son nom JNDI lors de l’instanciation du service et en particulier de l’injection de l’EntityManager qui déclenche la prise en charge de la couche JTA par le container.
Enfin, vous noterez que les tests proposés peuvent être améliorés : « testerTournoi() » ne peut être être résolu avec succès que si « creerTournoi() » a été exécuté, et « creerTournoi() » lèvera une exception de violation de contrainte unique au delà de la première exécution car, l’insertion des participants et rencontres est contrôlée par le service, mais celle des résultats est laissée relativement brute. Ce cas est volontaire ici, il permettra par exemple de tester la stratégie de propagation des exceptions lorsque celle-ci sera mise en place. Il serait évidemment possible d’intercepter l’exception dans le test comme nous avions fait pour les tests de la couche modèle, ou encore de compléter la méthode du service pour qu’elle vérifie si l’insertion est possible avant de la réaliser.

Conclusion

Notre couche de service permet désormais de maitriser l’exposition de notre modèle de données au travers de méthodes métiers élémentaires. Cette approche architecturale, complété par l’outillage projet, assure une relative autonomie à notre nouvelle brique permettant de la faire évoluer et de l’enrichir indépendamment des couches modèle et présentation. Dans un prochain article, nous verrons comment ajouter une couche de présentation Struts 2 sur ce montage EJB-JPA afin d’obtenir un premier jeu d’interfaces utilisateurs.

Laisser un commentaire

Aucun commentaire pour le moment

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>