GestionnaireLivreJEE

De $1

Version de 19:31, 19 Avr 2024

cette version.

Revenir à liste des archives.

Voir la version actuelle

TechnoWeb, TP JEE

Ce tp est une "simplification" de Applications_web_Master_1_Miage_TP_1

Ce TP était prévu pour la version NetBeans 8.0.2 version JavaEE (avec Glassfish 4.1.0). Après la simplification, la partie "buggée" de glassfish n'est plus solicitée. Le TP est également réalisable avec IntelliJ (simplement il n'incorporte pas la BD Derby, du coup, il faudra une autre BD par exemple MySQL). Attention, vers la fin du TP nous aurons besoin également d'e deux bases : Derby et MySQL. Si vous n'en avez qu'une la fin du TP sera abrégée. 

Partie 1 du TP : affichage d'un listing à l'aide d'une Servlet, d'une page JSP et de code métier

Créer un projet Web

Vous allez lancer NetBeans puis créer un projet de type "Java Web/Web application", donnez lui pour nom BookRepository1.

Dans IntelliJ : c'est Java Enterprise > Web Application et vous pouvez déjà choisir CDI (il faudra peut-être télécharger GlassFish et pour CDI il faut choisir create et allez dans le dossier de GlassFish : par exemple  D:\Program Files\glassfish-4.1\glassfish\modules\ pour choisir : cdi-api.jar)

Ajouter un fichier de configuration beans.xml pour CDI (Context and Dependency Injection, une technique d'injection de code que nous allons utiliser)

  1. Faites clic droit / Ajouter sur le projet, dans le menu déroulant, choisissez "autres".
  2. Dans la liste de gauche choisissez "Context and Depedency Injection",
  3. Dans la liste de droite choisissez "beans.xml (CDI confioguration file)".

Ca devrait créer dans Web Pages/WEB-INF un fichier beans.xml contenant ces lignes:

 

<?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>

Nous reviendrons sur l'injection un peu plus tard. Si vous n'insérez pas ce fichier dans le projet, certaines annotations de code que nous allons utiliser ne fonctionneront pas.

Executez/déployez le projet

Faites clic droit sur le projet, puis "Run", ça doit lancer GlassFish et au bout d'un moment, afficher dans votre navigateur par défaut le code HTML correspondant à l'éxécution de la page index.

Ajout de fonctionalités dans le projet

Doucement nous allons écrire un gestionnaire de livres, capables de faire les opérations classiques (Create, Research, Update, Delete) sur les données. Nous allons définir tout d'abord un modèle pour un livre et aussi une interface décrivant les fonctionnalités d'un gestionnaire de livre, et enfin une première implémentation pour un gestionnaire de livres basé sur des Collections Java (en mémoire, donc).

Création d'un modèle pour représenter un livre

Ajoutez une nouvelle classe Java au projet, intitulée Book et située dans le package com.bookstore. En général les modèles ont des noms correspondant aux données (Book, Person, Product, etc...).

Propriétés: les modèles ont des "propriétés", c'est à dire des attributs privés auxquels on associe des méthodes get/set. Une propriété est définie par sa méthode get, pas par le fait qu'il existe un attribut dans la classe. 

Une fois la classe créée, remplacez son contenu par celui-ci. Regardez-bien comment on a défini un modèle de livre, on a défini un Id unique, qui va agir comme une "clé primaire".

package com.bookstore;

import java.math.BigDecimal;
import java.util.Date;

public class Book implements Cloneable {

    private String title;
    private String description;
    private BigDecimal price;
    private Date pubDate;
    private String id;

    public Book(String id, String title, String description, BigDecimal price, Date pubDate) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.price = price;
        this.pubDate = pubDate;
    }

    public Book() {
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Date getPubDate() {
        return pubDate;
    }

    public void setPubDate(Date pubDate) {
        this.pubDate = pubDate;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Book cloneMe() {
        try {
            return (Book) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }

    @Override
public String toString() { return "Book [title=" + title + ", description=" + description
+ ", price=" + price + ", pubDate=" + pubDate + ", id=" + id
+ "]"; } }

Voilà, on a un modèle ! Il est temps de faire un "gestionnaire". Mais avant nous allons d'abord définir l'interface, c'est-à-dire les fonctionnalités que devra implémenter le gestionnaire de livres.

Ajout d'une interface pour un gestionnaire de livre. On parle aussi de "facade" dans le jargons des design patterns...

L'interface d'une "facade" doit proposer les fonctionnalités classiques pour créer, modifier, rechercher et supprimer des livres. Ajoutez donc une interface (une classe Java) intitulée BookRepository dans votre projet, dans le package com.bookstore. Modifiez son code pour qu'il ressemble à ceci :

package com.bookstore;

import java.util.List;

public interface BookRepository {
    Book lookupBookById(String id);

    void addBook(String title, String description, String price, String pubDate);

    void updateBook(String id, String title, String description, String price, String pubDate);

    void removeBook(String id);

    List<Book> listBooks();

}

Création de la librairie nécessaire à l'injection de code, appelée "CDI" (Context and Dependency Injection)

Choisissez dans NetBeans le menu Outils/bibliothèques ou Tools/Library. Cela va ouvrir une boîte de dialogue permettant de rajouter des "librairies" Java, en d'autres termes, des fichiers .jar que l'on pourra facilement ajouter à notre projet.

Dans l'étape suivante nous allons utiliser une annotation de code @ApplicationScoped qui vient d'une API Java nommée "CDI" qui n'est pas par défaut configurée dans NetBeans. En revanche le fichier .jar est dans le serveur GlassFish que nous utilisons.

Pour NetBeans : ajouter la librairie CDI au projet.

Pour IntelliJ : soit vous l'avez fait au début, soit vous faites clic droit sur le projet, "add framework support..." et choisissez "CDI" dans Java EE > icreate et allez dans le dossier de GlassFish : par exemple  D:\Program Files\glassfish-4.1\glassfish\modules\ pour choisir : cdi-api.jar

Ajout d'une implémentation "mémoire" de la facade/gestionnaire de livres

Ajoutez au projet une classe Java intitulée BookRepositoryImpl toujours dans le package com.bookstore, remplacez le code par celui-ci :

package com.bookstore;

import javax.enterprise.context.ApplicationScoped;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.math.BigDecimal;


@ApplicationScoped
public class BookRepositoryImpl implements BookRepository { private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); private int count; private Map<String, Book> idToBookMap = new HashMap<String, Book>(); public BookRepositoryImpl() { synchronized (this) { books(book("War and Peace", "blah blah blah", "5.50", "5/29/1970"), book("Pride and Prejudice", "blah blah blah", "5.50", "5/29/1960"), book("book1", "blah blah blah", "5.50", "5/29/1960"), book("book2", "blah blah blah", "5.50", "5/29/1960"), book("book3", "blah blah blah", "5.50", "5/29/1960"), book("book4", "blah blah blah", "5.50", "5/29/1960"), book("book5", "blah blah blah", "5.50", "5/29/1960"), book("book6", "blah blah blah", "5.50", "5/29/1960"), book("book7", "blah blah blah", "5.50", "5/29/1960"), book("book8", "blah blah blah", "5.50", "5/29/1960"), book("book9", "blah blah blah", "5.50", "5/29/1960"), book("Java for dummies", "blah blah blah", "1.99", "5/29/1960")); } } private Book book(String title, String description, String aPrice, String aPubDate) { Date pubDate = null; BigDecimal price = null; try { price = new BigDecimal(aPrice); }catch (Exception ex) { } try { pubDate = dateFormat.parse(aPubDate); }catch (Exception ex) { } return new Book("" + (count++), title, description, price, pubDate); } private void books(Book... books) { for (Book book : books) { doAddBook(book); } } private void doAddBook(Book book) { synchronized (this) { this.idToBookMap.put(book.getId(), book); } } @Override
public Book lookupBookById(String id) { synchronized (this) { return this.idToBookMap.get(id).cloneMe(); } } @Override
public void addBook(String title, String description, String price, String pubDate) { doAddBook(book(title, description, price, pubDate)); } @Override
public void updateBook(String id, String title, String description, String price, String pubDate) { Book book = book(title, description, price, pubDate); synchronized (this) { book.setId(id); this.idToBookMap.put(id, book); } } private List<Book> doListBooks() { List<Book> books; synchronized (this) { books = new ArrayList<Book>(this.idToBookMap.size()); for (Book book : this.idToBookMap.values()) { books.add(book.cloneMe()); } } return books; } public List<Book> listBooks() { List<Book> books = doListBooks(); Collections.sort(books, new Comparator<Book>() { public int compare(Book bookA, Book bookB) { return bookA.getId().compareTo(bookB.getId()); } }); return books; } @Override
public void removeBook(String id) { synchronized(this) { this.idToBookMap.remove(id); } } }

 

Bon qu'avons nous-là ?

  1. On gère la base de données des livre à l'aide d'une collection Java, plus précisément d'une HashMap Java (clé = l'id d'un livre) : private Map<String, Book> idToBookMap = new HashMap<String, Book>();

  2. Des "synchronized" un peu partout dès qu'il s'agit de modifier le contenu de la collection des livres. Les collections Java n'étant pas thread-safe, et les servlets étant multi-threadées (on va appeler le code de ce gestionnaire de livres depuis une servlet), il vaut mieux protéger les accès concurrents à notre "base de données/collection".
     
  3. Regardez comment on ajoute, recherche, remplis la base... cela vous fera réviser un peu les HashMaps !
     
  4. On a une annotation de code bizarre au début de cette classe : @ApplicationScoped

L'annotation du point 4 indique que cette classe, lorsqu'on en aura besoin, sera instanciée une seule fois et aura comme durée de vie celle de l'application Web. En gros, quand on utilisera ce gestionnaire, il sera créé comme un Singleton par le serveur et durera aussi longtemps que l'application web. Cette annotation fait partie de Context And Dependency Injection (CDI, une API de Java) et évite d'avoir à faire des "new" pour allouer des objets. Vous en saurez plus pendant le cours sur CDI.

Bon, maintenant que nous avons un modèle de livre et un gestionnaire de livres, il reste à ajouter au projet un contrôleur Web (une Servlet) et une vue (une JSP).

Ajout d'une servlet / controleur web

Faites clic droit/new Servlet dans votre projet (sur src dans IntelliJ) :

  1. Appelez la Servlet BookListServlet,
  2. Mettez-là dans le package com.bookstore.web
  3. Changez le mapping de l'URL pour /book/ 

On reprend ici le nommage des URLs des Web Services RESTful que nous verrons en M2. Un URL qui se termine par "/" en général se réfère à une liste d'objets. Comme dans cette première partie du TP on veut afficher un listing contenant l'ensemble des livres dans la base de données, cela tombe bien.

Remplacez le code de la servlet par :

package com.bookstore.web;

import java.io.IOException;

import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.bookstore.BookRepository;

@WebServlet("/book/")
public class BookListServlet extends HttpServlet {

    @Inject
private BookRepository bookRepo; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("books", bookRepo.listBooks()); getServletContext().getRequestDispatcher("/book-list.jsp").forward(request, response); } }

Une Servlet est une classe qui traite des requêtes HTTP et renvoie une réponse (ici la méthode doGet est appelée en cas de requête HTTP GET et possède comme premier paramètre la requête et comme second la réponse). Dans une architecture MVC "model 2" on va utiliser une page JSP pour le traitement de la réponse.

Regardez ce que fait cette servlet :

  1. Elle a un attribut "injecté", c'est le gestionnaire de livres à qui elle va sous-traiter la partie "métier" du traitement : @Inject  private BookRepository bookRepo; 

  2. Le @Inject indique "Serveur, débrouille toi mais donne moi un gestionnaire de livres !", le @Inject ne fonctionne que si on a bien un fichier beans.xml dans le répertoire WEB-INF. Si ce n'est pas le cas, alors le gestionnaire ne sera pas injecté et vaudra null.
     
  3. Dans la méthode doGet on appelle la méthode listBooks du gestionnaire, qui renvoie une collection de livres, et on met ce résultat dans la "requête" : request.setAttribute("books", bookRepo.listBooks());

  4. Pourquoi on met la liste dans la reqête HTTP ? Parcequ'on a pas fini de traiter la requête !!! On va "passer la requête" à un autre élément, la page book-list.jsp, qui elle va se charger de finir le travail, en produisant le HTML de la réponse (un tableau HTML avec les livres). C'est ce que fait la ligne : getServletContext().getRequestDispatcher("book-list.jsp").forward(request, response);

getServletContext() renvoie "le contexte d'exécution", c'est à dire un objet qui sait parler avec le serveur qui exécute l'application. Cet objet possède une méthode getRequestDispatcher(...) qui permet de chainer divers traitements pour une même requête. On passe en paramètre l'URL relatif d'une page JSP intitulée book-list.jsp.

 

Ajout d'une page JSP "vue" pour l'affichage des livres

Les pages JSPs sont des Servlets en réalité, elles sont traduites en .java et compilées comme des Servlets lors de leur première invocation. Ca c'est pour ce qu'il se passe en coulisse.

Du point du vue du développeur, une page JSP est une page HTML augmentée d'expressions de type ${book.name} pour substituer des variables dans le code HTML, et par des ordres comme des if, then, else, boucles for, etc pour donner un aspect dynamique aux pages. Exemple : <c:forEach var="book" items="${books}">.....</c:foreach> pour parcourir la liste des livres dans une boucle. Le langage des expressions s'appelle EL (Expression language) et le langage permettant des actions dynamiques s'appelle JSTL (Java Standard Tag Library). 

Ajoutez donc une page JSP à votre projet (clic droit sur web pour IntelliJ), et intitulez-la book-list.jsp, remplacez son contenu par :

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix ="c" %>
<!DOCTYPE HTML>

<html>
<head>
    <title>Book listing</title>
</head>
<body>


<table>
    <tr>
        <th>Title</th>
        <th>Description</th>
        <th>Price</th>
        <th>Publication Date</th>
    </tr>

    <c:forEach var="book" items="${books}">
        <tr>
            <td>${book.title}</td>
            <td>${book.description}</td>
            <td>${book.price}</td>
            <td>${book.pubDate}</td>
        </tr>
    </c:forEach>
</table>

</body>
</html>

Ce code est assez classique :

  • La première ligne indique quel est le type de données que va produire cette page (du HTML encodé en UTF-8)
  • La seconde ligne est obligatoire si on utilise des tags de la librairie JSTL comme <c:foreach...> Si on l'oublie cette boucle ne sera pas exécutée.
  • Le reste est du HTML à part le foreach et les expressions commençant par ${...}

Regardons juste la boucle foreach:

<c:forEach var="book" items="${books}">
    <tr>
        <td>${book.title}</td>
        <td>${book.description}</td>
        <td>${book.price}</td>
        <td>${book.pubDate}</td>
    </tr>
</c:forEach>

 

La boucle indique qu'on va chercher un objet intitulé "books", dans la page, dans la requête, dans la session HTTP, etc.... on va chercher dans les divers "scopes" de la page. Ca tombe bien, avant d'appeler cette JSP on était dans une Servlet qui a mis une liste de livres intitulée "books" dans la requête. Petit rappel du code de la Servlet: request.setAttribute("books", bookRepo.listBooks());

Donc la boucle va itérer sur chaque livre. La variable de boucle s'appelle "book" (var="book").

A l'intérieur de la boucle, pour accèder aux propriétés de livre correspondant à la variable de boucle (le livre courant), on utiliser une expression EL : ${book.title} par exemple revient à appeler book.getTitle(), ${book.description} revient à appeler book.getDescription() etc. 

Dans l'expression ${book.description}, description est une propriété, pour que cela marche, la classe Book doit avoir une méthode getDescription(), sinon vous aurez une erreur "EL cannot access property named description...."

Execution du projet

Si tout s'est bien passé vous devriez voir s'afficher :

Partie 2 : ajout de fonctionnalités pour faire le CRUD sur les livres

Réflechissons un peu aux URLs relatifs que l'on va utiliser pour les différentes opérations...

  • On a déjà /book/ qui liste les livres, cet URL étant mappé sur la servlet BookListServlet
     
  • On va définir /book?id=1 pour éditer le livre d'id=1 et pour ajouter un livre on va utiliser juste /book sans le / à la fin...

    • On va donc créer une nouvelle Servlet BookEditorServlet qui sera mappée sur l'URL /book (sans le / à la fin) et dont le rôle sera d'appeler une JSP avec un formulaire
       
    • Si la servlet est appelée avec /book?id=1 alors on va utiliser le gestionnaire de livres pour récupérer les données du livre d'id = 1, on va le mettre dans la requête et passer la main à une JSP (book-form.jsp) qui s'occupera d'afficher le formulaire de modification pré-rempli avec le livre,
       
    • Si la servlet est appelée par /book sans paramètre id, alors on appellera la même JSP mais le formulaire ne sera pas rempli.

Bien, mais qui va traiter la soumission du formulaire ? Tant qu'à faire ce sera la même Servlet ! En général on traite les soumissions de formulaire par une requête HTTP POST, on fera ce traitement dans la méthode doPost() de la Servlet, soit pour mettre à jour un livre existant, soit pour insérer un nouveau livre.

Ok, commençons par voir ce que l'on va modifier...

Ajout d'un lien au début de la page JSP d'affichage, pour ajouter un livre

On va simplement ajouter une ligne au début de la page book-list.jsp :

<a href="${pageContext.request.contextPath}/book">Add Book</a>

Ce que fait la ligne avec le <a href...> c'est de rajouter un lien qui appelle la Servlet mappée sur /book, sans paramètres, ce qui correspond dans notre liste d'URLs au dernier cas, celui où on veut ajouter un nouveau livre. La partie ${pageContext.request.contextPath} correspond à http://localhost:8080/BookRepository1

Ajout d'un lien sur le titre des livres dans le tableau des livres

Ce lien appellera la nouvelle Servlet dont on vient de parler, pour pouvoir mettre à jour un livre existant.

On va modifier la page book-list.jsp et remplacer le début de la boucle for par  :

<c:forEach var="book" items="${books}">
    <tr>
        <td><a href="${pageContext.request.contextPath}/book?id=${book.id}">${book.title}</a></td>

La ligne ${pageContext.request.contextPath}/book?id=${book.id} permet de générer un URL correspondant à la Servlet d'édition. La partie ${pageContext.request.contextPath} correspond à http://localhost:8080/BookRepository1

Si vous exécutez le projet, vous devez voir (les liens font des erreurs 404) :