Applications web Master 1 Miage TP 1

De $1

Table des matières
  1. 1. Introduction
  2. 2. Partie 1: affichage d'un listing à l'aide d'une Servlet, d'une page JSP et de code métier
      1. 2.1.1. Créer un projet Web
      2. 2.1.2. Executez/déployez le projet
      3. 2.1.3. Ajout de fonctionalités dans le projet
      4. 2.1.4. Création d'un modèle pour représenter un livre
      5. 2.1.5. Ajout d'une interface pour un gestionnaire de livre. On parle aussi de "facade" dans le jargons des design patterns...
      6. 2.1.6. Ajout d'une implémentation "mémoire" de la facade/gestionnaire de livres
      7. 2.1.7. Ajout d'une servlet / contrôlleur web
      8. 2.1.8. Ajout d'une page JSP "vue" pour l'affichage des livres
      9. 2.1.9. Execution du projet
    1. 2.2. Partie 2 : ajout de fonctionnalités pour faire le CRUD sur les livres
      1. 2.2.1. Réflechissons un peu aux URLs relatifs que l'on va utiliser pour les différentes opérations...
      2. 2.2.2. Ajout d'un lien au début de la page JSP d'affichage, pour ajouter un livre
      3. 2.2.3. Ajout d'un lien sur le titre des livres dans le tableau des livres
      4. 2.2.4. Ajout d'une servlet pour l'édition / insertion de livres
      5. 2.2.5. Ajout de la page JSP d'affichage du formulaire de modification
      6. 2.2.6. Executez le projet et testez
      7. 2.2.7. Ajout d'une méthode doPost() à la servlet BookEditorServlet
    2. 2.3. Troisième partie : de la mise en page (template, CSS etc.)
      1. 2.3.1. Ajout d'un template de page
      2. 2.3.2. Modification des pages book-form.jsp et book-list.jsp
      3. 2.3.3.  Les pages header.jsp et footer.jsp
      4. 2.3.4. Ajout d'une feuille CSS
      5. 2.3.5. Amélioration de l'affichage du tableau, du prix et de la date
    3. 2.4. Quatrième partie : utilisation d'une base de données via JDBC, injection avec Qualifier
      1. 2.4.1. Création d'une base de données Derby/JavaDB
      2. 2.4.2. Ajout d'une table "book" dans la base bookstore
      3. 2.4.3. Rendre cette base visible par Glassfish, lui associer un nom JNDI jdbc/bookstore
      4. 2.4.4. Ecriture d'une seconde implémentation d'un gestionnaire de livre, utilisant JDBC et SQL pour se connecter à la base de donnée bookstore
      5. 2.4.5. Ajout d'une classe JDBC.java et InMemory.java pour définir les qualifiers pour l'injection
      6. 2.4.6. Exécutez le projet dans sa version JDBC
      7. 2.4.7. Exécutez le projet dans sa version InMemory
    4. 2.5. Cinquième partie : utilisation de MySQL à la place de Derby/JavaDB

Version de 21:59, 18 Avr 2024

cette version.

Revenir à liste des archives.

Voir la version actuelle

  1. 1. Introduction
  2. 2. Partie 1: affichage d'un listing à l'aide d'une Servlet, d'une page JSP et de code métier
      1. 2.1.1. Créer un projet Web
      2. 2.1.2. Executez/déployez le projet
      3. 2.1.3. Ajout de fonctionalités dans le projet
      4. 2.1.4. Création d'un modèle pour représenter un livre
      5. 2.1.5. Ajout d'une interface pour un gestionnaire de livre. On parle aussi de "facade" dans le jargons des design patterns...
      6. 2.1.6. Ajout d'une implémentation "mémoire" de la facade/gestionnaire de livres
      7. 2.1.7. Ajout d'une servlet / contrôlleur web
      8. 2.1.8. Ajout d'une page JSP "vue" pour l'affichage des livres
      9. 2.1.9. Execution du projet
    1. 2.2. Partie 2 : ajout de fonctionnalités pour faire le CRUD sur les livres
      1. 2.2.1. Réflechissons un peu aux URLs relatifs que l'on va utiliser pour les différentes opérations...
      2. 2.2.2. Ajout d'un lien au début de la page JSP d'affichage, pour ajouter un livre
      3. 2.2.3. Ajout d'un lien sur le titre des livres dans le tableau des livres
      4. 2.2.4. Ajout d'une servlet pour l'édition / insertion de livres
      5. 2.2.5. Ajout de la page JSP d'affichage du formulaire de modification
      6. 2.2.6. Executez le projet et testez
      7. 2.2.7. Ajout d'une méthode doPost() à la servlet BookEditorServlet
    2. 2.3. Troisième partie : de la mise en page (template, CSS etc.)
      1. 2.3.1. Ajout d'un template de page
      2. 2.3.2. Modification des pages book-form.jsp et book-list.jsp
      3. 2.3.3.  Les pages header.jsp et footer.jsp
      4. 2.3.4. Ajout d'une feuille CSS
      5. 2.3.5. Amélioration de l'affichage du tableau, du prix et de la date
    3. 2.4. Quatrième partie : utilisation d'une base de données via JDBC, injection avec Qualifier
      1. 2.4.1. Création d'une base de données Derby/JavaDB
      2. 2.4.2. Ajout d'une table "book" dans la base bookstore
      3. 2.4.3. Rendre cette base visible par Glassfish, lui associer un nom JNDI jdbc/bookstore
      4. 2.4.4. Ecriture d'une seconde implémentation d'un gestionnaire de livre, utilisant JDBC et SQL pour se connecter à la base de donnée bookstore
      5. 2.4.5. Ajout d'une classe JDBC.java et InMemory.java pour définir les qualifiers pour l'injection
      6. 2.4.6. Exécutez le projet dans sa version JDBC
      7. 2.4.7. Exécutez le projet dans sa version InMemory
    4. 2.5. Cinquième partie : utilisation de MySQL à la place de Derby/JavaDB

Introduction

Les cours sur JSP/JDBC n'ont pas encore eu lieu, néanmoins nous allons vous faire plonger dans le grand bain en réalisant aujourd'hui une application complète mettant en oeuvre JSPs + Servlets + JDBC dans une approche MVC appelée dans le monde Java EE "model 2". Il s'agit d'une approche dans laquelle les servlets font office de contrôleurs web, les pages JSPs de vues (formulaires de saisie, affichage de résultats) et des classes Java standards (agrémentées d'interfaces pour plus d'abstraction) se chargeront de la partie métier et accès aux données.

Le TP est inspiré et adapté de ce tutorial Java EE . 

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

Vous utiliserez le logiciel Netbeans dans sa version 7.2.1 entreprise (la plus grosse). Pour  TPs à venirnous aurons besoin également d'une base MySQL que vous devez être capable d'administrer.

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, cliquez sur le bouton "suivant" et là dans la dernière étape du wizard de création du projet, n'oubliez pas de cocher la case "Enable Context and Dependency Injection". Si vous oubliez de cocher cette case, vous aurez quelques soucis car l'injection d'instances ne fonctionnera pas dans votre projet.

Si vous avez déjà validé sans avoir coché cette case, il suffit de créer dans le répertoire Web Pages/WEB-INF du projet un fichier beans.xml vide ou mieux, contenant le code suivant:

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

Dans "Web pages", ouvrez le fichier index.jsp et modifiez le pour qu'il affiche autre chose que "Hello World"

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.jsp.

Ajout de fonctionalités dans le projet

Doucement nous allons écrire ensembles un gestionnaire de livres, capables de faire les opérations classiques (Créate, 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). Plus tard dans le TP nous ferons une seconde implentation à l'aide d'une base de données  et de l'API Java Database Connectivity (JDBC) de Java.

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...). 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éfinit 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éfinir l'interface, c'est-à-dire les fonctionnalités que devront implémenter des gestionnaires de livre.

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();

}

 

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

import javax.enterprise.context.ApplicationScoped;

@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 / contrôlleur web

Faites clic droit/new Servlet dans votre projet:

  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). 

Nous étudierons les JSPs/EL/JSTL en cours plus tard. Pour le moment, si vous comprenez les principes, cela suffit.

Ajoutez donc une page JSP à votre projet, 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 :

Snap1.jpg

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 :

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

        <table>

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 code :

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

Par :

<c:forEach var="book" items="${books}">               
<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 :

Snap2.jpg

On a les titres qui sont des liens vers des URLs de type /book?id=... (note le lien en jaune est faux dans le screenshot, il manque BookRepository1/ avant book?id=1)

Ajout d'une servlet pour l'édition / insertion de livres

On va maintenant ajouter au projet la Servlet BookEditorServlet qui va traiter ce type d'URLs, réglez le nom de la Servlet, le mapping, mettez la dans le même package que la servlet existante. Le code de la Servlet doit ressembler à ceci :

 

package com.bookstore.web;

import com.bookstore.Book;
import com.bookstore.BookRepository;
import java.io.IOException;
import java.text.SimpleDateFormat;
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;

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

    @Inject
    private BookRepository bookRepo;
    private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {

        // On récupère le paramètre id
        String id = request.getParameter("id");

        if (id != null && !id.isEmpty()) {
            // Allons chercher le livre correspondant à l'id
            Book book = bookRepo.lookupBookById(id);
            // On met le livre dans la reqête
            request.setAttribute("book", book);
            // Et la date de publication aussi
            request.setAttribute("bookPubDate", dateFormat.format(book.getPubDate()));
        }

        /* On appelle la JSP pour affichage du formulaire */
        getServletContext().getRequestDispatcher("/book-form.jsp").forward(
                request, response);
    }
}

 

Cette Servlet regarde si elle a été appelée avec un paramètre id, si c'est le cas c'est qu'on souhaite modifier un livre existant (on a appelé la servlet en cliquant sur le titre d'un livre dans la liste), elle va récupérer le livre dans la base de données en utilisant le gestionnaire de livres : Book book = bookRepo.lookupBookById(id); puis elle met le livre récupéré dans la requête avant de passer la main pour la suite du traitement à la page book-form.jsp qui va générer le formulaire pré-rempli.

Ajout de la page JSP d'affichage du formulaire de modification

Ajoutez au projet maintenant la page book-form.jsp, voici à quoi doit ressembler le code :

<%@ 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 Form</title>
    </head>

    <body>

        <h1>Book Form</h1>


        <form method="post" action="${pageContext.request.contextPath}/book">
            <fieldset>
                <legend>
                    <c:choose>
                        <c:when test="${not empty book.id }">
                            Updating Book
                        </c:when>
                        <c:otherwise>
                            Adding Book
                        </c:otherwise>
                    </c:choose>
                </legend>

                <div>
                    <label for="title">Title</label> 
                    <input type="text" name="title"
                           id="title" value="${book.title}" />
                </div>

                <div>
                    <label for="description">Description</label>
                    <textarea name="description" id="description" rows="2" cols="60">
                        ${book.description}
                    </textarea>
                </div>

                <div>
                    <label for="price">Price $</label> 
                    <input name="price" id="price" 
                           value="${book.price}" />
                </div>

                <div>
                    <label for="pubDate">Publication Date</label> 
                    <input name="pubDate" id="pubDate" 
                           value="${bookPubDate}" />
                    <label class="after">(MM/DD/YYYY)</label>
                </div>

                <c:if test="${not empty book.id}">
                    <input type="hidden" name="id" value="${book.id}" />
                </c:if>

            </fieldset>

            <div class="button-row">
                <a href="${pageContext.request.contextPath}/book/">Cancel</a> or  <input type="submit" value="Submit" />
            </div>
        </form>
    </body>
</html>

Prenez le temps d'examiner le code de cette page. Elle contient un formulaire HTML qui lorsqu'il est soumis, invoque par un HTTP POST l'URL /book, soit celui de la servlet que nous venons d'écrire. 

Par ailleurs, on trouve des tests pour afficher "Update Book" ou "Insert Book" selon que book.id vaut null ou pas. En d'autres termes:

Si il existe dans la requête une variable book et si son id est non null, alors on est en mode "Update", voir le bout de code :

 

<c:choose>
   <c:when test="${not empty book.id }">
      Updating Book
   </c:when>
   <c:otherwise>
      Adding Book
   </c:otherwise>
</c:choose>

 

Remarquons également la manière dont le formulaire sera sera soumis. Vers la fin on voit :

 

<c:if test="${not empty book.id}">
      <input type="hidden" name="id" value="${book.id}" />
 </c:if>

 

Il s'agit ici d'une astuce très courante pour ajouter artificiellement un paramètre au formulaire, ici un champs de nom id et de valeur l'id du livre que l'on vient de modifier... A quoi cela sert-il ? Et bien lors de la soumission d'un site modifié on va appeler la Servlet /book avec un POST, on va passer les nouvelles valeurs de titre, description, etc.... mais comme on a pas proposé l'id à la modification, il faut quand même le passer à la servlet pour qu'elle déclenche la mise à jour. On passe ainsi par un élement HTML <input> de type hidden, c'est à dire "non visible".

Executez le projet et testez

Exécutez le projet, si tout se passe bien, vous devriez pouvoir cliquer sur un titre de livre et cela devrait appeler la servlet mappée sur /book avec comme paramètre l'id du livre à modifier. La Servlet doit déclencher la recherche du livre dans la base de données, le livre doit être mis dans la requête et la page book-form.jsp doit être appelée. On doit voir :

Snap3.jpg

Et lorsqu'on clique sur un lien :

Snap4.jpg

Si vous soumettez le formulaire vous devriez avoir une erreur car comme le formulaire est soumis par un HTTP POST, la servlet /book doit avoir une méthode doPost(...), or elle n'en a pas encore. Nous allons y remédier !

Ajout d'une méthode doPost() à la servlet BookEditorServlet

Cette méthode sera appelée chaque fois que le formulaire de la page book-form.jsp sera soumis (et il s'agit d'une soumission par POST, donc il faut une méthode doPost).

Rappelons qu'on peut appeler cette méthode dans deux cas : 

  1. Le formulaire a servi à la modification d'un livre (page book-form.jsp appelée par la servlet avec un paramètre id)
  2. Le formulaire a servi à l'ajout d'un livre (clic sur le lien "Add Book", pas de passage de paramètre id)

Nous avons vu que la page book-form.jsp teste dans quel cas elle se trouve, et si elle se trouve dans le premier cas, elle passe à son tour à la servlet qui va traiter la soumission, le paramètre id :

<c:if test="${not empty book.id}">
      <input type="hidden" name="id" value="${book.id}" />
 </c:if>

Voici donc la méthode doPost() à ajouter à la servlet BookEditorServlet.java, mappée sur /book :

protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {

        // On récupère le contenu du formulaire
        String title = request.getParameter("title");
        String description = request.getParameter("description");
        String price = request.getParameter("price");
        String pubDate = request.getParameter("pubDate");

        // le champ "hidden" du formulaire qui contient l'id
        String id = request.getParameter("id");
        
        // Si l'id est null alors on est dans le cas d'un ajout de livre
        if (id == null || id.isEmpty()) {
            bookRepo.addBook(title, description, price, pubDate);
        } else {
            // Si l'id n'est pas nul on fait un update
            bookRepo.updateBook(id, title, description, price, pubDate);
        }

        // Enfin, on redirige vers la servlet qui affiche la liste des livres
        response.sendRedirect(request.getContextPath() + "/book/");
    }

 Regardez les commentaires que nous avons mis dans cette méthode, vous comprendrez comment on peut lui faire exécuter deux actions distinctes selong que le paramètre HTTP "id" est présent ou non.

  • Si le paramètre est présent, on met à jour le livre : bookRepo.updateBook(id, title, description, price, pubDate);
  • Si le paramètre est absent, on ajoute un livre : bookRepo.addBook(title, description, price, pubDate);

Troisième partie : de la mise en page (template, CSS etc.)

Dans cette partie nous allons utiliser plusieurs choses pour rendre l'application plus présentable : des "templates JSP" pour définir des en-têtes et pieds de page, une feuille de style CSS pour améliorer le rendu des divers éléments qui composent nos vues.

Ajout d'un template de page

Pour définir un "template", nous allons juste créer une page template.jsp qui va inclure systématiquement un en-tête et un pied de page (ce seront eux-même des pages jsp, qui auront accès aux mêmes paramètres, requête, session etc que la page principale).

Ajouter une page template.jsp à votre projet et copiez-collez le code suivant :

 

<%@ 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>${param.title}</title>
<link rel="stylesheet" type="text/css"
	href="${pageContext.request.contextPath}/resources/style.css" />
</head>
<body>
	<jsp:include page="header.jsp"/>

	
	<h1>${param.title}</h1>

	<jsp:include page="${param.content}.jsp"/>
	
	<jsp:include page="footer.jsp"/>
	
	
</body>
</html>

 

Modification des pages book-form.jsp et book-list.jsp

  1. Renommez les pages book-list.jsp et book-form.jsp en book-list-content et book-form-content.jsp,
  2. Créez deux pages book-list.jsp et book-form.jsp qui vont appeler le template (voir code ci-dessous) 

Nouvelle page book-list.jsp :

 

<jsp:include page="template.jsp">
	<jsp:param name="content" value="book-list-content"/>
	<jsp:param name="title" value="Book Listing"/>
</jsp:include>

 

Cette page inclut le template avec le tag <jsp:include> et lui passe des paramètres (les deux lignes avant le </jsp:include>) :

  • Le paramètre "content" vaut le nom de la page jsp qui contient réellement le code qui affiche la liste des livres, regardez comment ce paramètre est récupéré dans template.jsp : dans template.jsp on a la ligne : <jsp:include page="${param.content}.jsp"/> qui va en réalité inclure la page book-list-content.jsp
  • Le paramètre "title" est également passé au template, qui va s'en servir pour initialiser le titre de la page : <title>${param.title}</title>

En fait la page jsp qui a le nom "normal" book-list.jsp appelle un template qui va inclure un en-tête, inclure l'ancienne version de la page jsp, renommée en book-list-content.jsp, inclure un pied de page, etc.

On fait de même avec la page book-form.jsp :

 

<jsp:include page="template.jsp">
	<jsp:param name="content" value="book-form-content"/>
	<jsp:param name="title" value="Book Form"/>
</jsp:include>

 

 Les pages header.jsp et footer.jsp

Notre template inclut ces deux pages. Je vous propose le code suivant :

header.jsp :

<div>
    <a href="${pageContext.request.contextPath}">
        <img src="${pageContext.request.contextPath}/resources/logo.jpg"/>
    </a>
</div>

Vous devrez copier l'image suivante dans le répertoire resources sous Web Pages :

logo.jpg :

logo.jpg

footer.jsp :

<div id="footer">
    <ul class="bottom">
        <li><a href="http://miage.unice.fr/">Miage HOME</a> |
        <li><a href="http://miageprojet2.unice.fr/Intranet_de_Michel_Buffa/">Michel Buffa's intranet</a> |
        <li><a href="http://miageprojet2.unice.fr/Intranet_de_Michel_Buffa/">Web page of this TP</a>
    </ul>

    <div class="about">Copyright (c) 2020-2020 IUP Miage Nice.</div>

    <div class="about">
        This project is the first step towards becoming a real Web Developer ! 
    </div>
</div>

Comme vous pouvez le constater, on a utilisé des attributs class="about" pour styler la présentation de ces blocs. La page template.jsp inclut aussi le fichier style.css, il nous reste à le rajouter au projet !

Ajout d'une feuille CSS

Ajoutez à votre projet une feuille de style nommée style.css dans Web Pages/resources (un seul "s" !) : <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/resources/style.css" />

Pour ce faire faites clic droit/New/Web/cascading style sheet. Appelez le fichier style.css et dans le champs "répertoire" mettez "resources". Normalement vous devriez voir dans la hiérarchie de votre projet un fichier resources/style.css dans Web Pages.

Contenu de style.css :

 

/* The CSS for the table was based of an example at wc3 schools. 
http://www.w3schools.com/css/tryit.asp?filename=trycss_table_fancy
*/
table.listing 
{
font-family: Arial, Helvetica, sans-serif;
width:80%;
border-collapse:collapse;
}


.listing td, th 
{
font-size:1em;
border:1px solid #98bf21;
padding:3px 7px 2px 7px;
}

.listing th
{
font-size:1.1em;
text-align:left;
padding-top:5px;
padding-bottom:4px;
background-color:#A7C942;
color:#ffffff;
}

.listing tr.alt td 
{
color:#000000;
background-color:#EAF2D3;
}


/* unvisited link */
a:link 
{
	text-decoration:none;
	color:#000000; 
}

/* visited link */
a:visited 
{
	text-decoration:none; 
	color: #707070 ;
}

a:hover {text-decoration:underline;}   /* mouse over link */
a:active {text-decoration:underline;}  /* selected link */

/* The CSS for the form was taken from this project. 
https://github.com/pmcelhaney/semantic-form.css/blob/master/index.html
*/

form.semantic {
	width:70%;
}

form.semantic fieldset
{
	clear: both;
	margin: 1em 0 0 0;
	padding: 10px;
	overflow: auto;
	background-color: #f8f8f8;
	border: 1px solid #888;
}

form.semantic legend
{
	font-weight: bold;
}

form.semantic div
{
	clear: both;
	margin: 0;
	padding: 0.5em 0 0 0;
	overflow: visible;
}


form.semantic label 
{
	display: block;
	float: left;
	width: 120px;
	text-align: right;
	padding: 2px 1ex 6px 0;
	vertical-align: baseline;
}
          
form.semantic label.after 
{
	width: auto;
	text-align: left; 
	display: inline; 
	float: none;
}

form.semantic label.long 
{
	clear: both;
	width: auto;
	text-align: left;
	float: none;
}

form.semantic input
, form.semantic select
, form.semantic textarea
{
	float: left;
}

form.semantic input.date {
	width : 100px;
}


form.semantic input.money {
	width : 100px;
}

form.semantic input[type=radio]
, form.semantic input[type=checkbox]
{
	vertical-align: text-bottom;
}

form.semantic ul {
	list-style-type: none; 
	float: left;
	padding: 0;
	margin: 0;
}

form.semantic li {
	clear: both;
	padding: 0.2em 0;
}

form.semantic li label {
	width: auto;
	text-align: left;
	padding: 0;
}   

form.semantic div.field-row 
{
	clear: none;
	float: left;
	margin: 0;
	padding: 0;
	overflow: visible;
}

form.semantic div.field-row *
{
	float: none;
	display: inline;
}

form.semantic .button-row
{
	text-align: right;
}

form.semantic .button-row input
{
	float: none;
}

form.semantic div.long label 
{
	width: auto;
	text-align: left;  
	float: none;
}

form.semantic div.long textarea 
{
	width: 100%;
}

/* Errors -- for use with JQuery Validate, etc. */

form.semantic input.error
, form.semantic select.error
, form.semantic textarea.error
{
	background-color: #77002a;
	color: white;
}                                                

form.semantic label.error
{
	width: auto;
	color: #77002a;
	text-align: left;
}       

/* from caucho.com */
#footer hr
{
  width: 100%;
}
#footer
{
  font-size: 80%;
  text-align: right;
}

/* bottom nav */

.bottom {
  font-family: Verdana, Helvetica, Arial, sans-serif;
  font-size : 90%;
  color : #999;
}

ul.bottom {
  list-style-type : none;
  padding : 0em;
  margin : 0;
  margin-top : 3em;
  text-align : center;
}

.bottom li {
  display : inline;
  margin : 0.1em 0.1em;
  padding : 0.0em;
}

.bottom a {
  color : #999;
}

.bottom a:hover {
  color : #ffcc33;
}

/* bottom */

.about {
  color : #999;
  font-family : Helvetica, Arial, sans-serif;
  text-align : center;
  font-size : 90%;
}

div.about {
  margin-top : 1em;
}

Ce n'est pas grave si vous ne comprenez pas encore toutes les subtilités...

Exécutez le projet, vous devriez obtenir :

 Snap6.jpg

Amélioration de l'affichage du tableau, du prix et de la date

Pour cela on va faire appel à la librairie JSTL et à de nouveaux tags que nous étudieront en cours par la suite. Voici le résultat qu'on va obtenir :

Snap8.jpg

Remplacez donc le code de book-list-content.jsp par le suivant :

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix ="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix ="fmt" %>

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

<table class="listing">
    <tr>
        <th>Title</th>
        <th>Description</th>
        <th>Price</th>
        <th>Publication Date</th>
    </tr>

    <c:forEach var="book" items="${books}" varStatus="status">
        <tr class="${status.index%2==0 ? 'alt' : ''}">
            <td><a href="${pageContext.request.contextPath}/book?id=${book.id}">${book.title}</a></td>
            <td>${book.description}</td>
            <td>
                <fmt:formatNumber value="${book.price}" type="currency" currencySymbol="EUR"
                                  pattern="¤ #,##0.00;¤ -#,##0.00"/>
            </td>
            <td>
                <fmt:formatDate value="${book.pubDate}" type="both" dateStyle ="short" timeStyle ="short"/>
            </td>
        </tr>
    </c:forEach>
</table>

Quelques lignes ont changé... La première concerne l'affichage de la table elle-même : <table class="listing">. la table a comme classe "listing".

Regardez aussi la boucle : <c:forEach var="book" items="${books}" varStatus="status">, cet attribut varStatus permet d'avoir une variable de "statut de boucle". Notamment, dans la boucle, ${status.index} vaudra le numéro courant de l'itération (0, 1, 2, 3, etc.).

On utilise cet index pour générer un attribut class= différent selon que la ligne du tableau est paire ou impaire :

<tr class="${status.index%2==0 ? 'alt' : ''}">

Ce code dit que selon le modulo 2 de la valeur de l'index on va générer <tr class="alt"> ou <tr class=""/>, et dans la feuille CSS on a une règle :

.listing tr.alt td {

  color:#000000;

  background-color:#EAF2D3;

}

qui s'applique donc aux éléments de la classe "listing" (rappel : notre élément table est de cette classe), aux éléments <tr> ayant pour classe "alt"  et contenus dans un element de classe listing (ah, ça nous concerne) et aussi  les <td> contenus dans des tr.listing contenus dans des listings. En CSS quand on a un "selecteur CSS" qui contient plusieurs valeurs séparées par des blancs c'est l'inclusion, pas forcément directe.

Là, dans notre cas, c'est td inclus dans tr.alt (mais il peut y avoir des intermédiaires), et aussi tr.alt inclus dans un élément de classe .listing (mais il peut y avoir des intermédiaires), et aussi les éléments de la classe .listing.

Les plus malins ou les plus cultivés savent qu'on aurait pu faire beaucoup plus simple avec les nouveaux sélecteurs de CSS3 pour afficher une table avec des lignes en couleurs alternées. On y reviendra aussi. Là c'est surtout un prétexte pour voir la librairie JSTL.

Les autres lignes qui ont changé sont celles concernant l'affichage du prix :

<fmt:formatNumber value="${book.price}" type="currency" currencySymbol="EUR"
    pattern="¤ #,##0.00;¤ -#,##0.00"/>

Là on utilise un tag JSTL appartenant à  une sous partie de la lib JSTL dédiée au formattage. Les tags <c:....> font partie de la partie "Core" (coeur) de la JSTL. Nous y reviendront en cours. Dans le cas qui nous intéresse, on a indiqué que ce que l'on veut afficher (le prix : ${book.price}) est une monnaie, qu'on veut un symbole EUR et que l'on veut afficher le symbole avant la valeur ( le caractère "¤" dans la pattern = le symbole), et que l'on souhaite un format spécial pour les valeurs positives, et négatives (ici une virgule après les miulliers, et toujours .00 à la fin si c'est une valeur entière).

Et l'affichage de la date :

<fmt:formatDate value="${book.pubDate}" type="both" dateStyle ="short" timeStyle ="short"/>

Qui indique que l'on veut une forme courte de la date (short) et de l'heure.

Quatrième partie : utilisation d'une base de données via JDBC, injection avec Qualifier

Nous allons maintenant ajouter une base de données au serveur glassfish, lui donner un nom JNDI afin d'y accèder depuis notre application web. Enfin, nous implémenterons une seconde version de notre gestionnaire de livre, utilisant JDBC/SQL au lieu d'une HashMap.

Création d'une base de données Derby/JavaDB

Nous allons pour cet exercice utiliser le SGBD Derby qui est  la base de données livrée avec le JDK. Tout ce que nous ferons avec ce SGBD peut également être fait avec MySQL, seule les configurations changeront, mais vous verrez que le code de votre application demeurera inchangé.

Nous allons utiliser l'outil d'administration de base de données intégré à Netbeans. Allez dans l'onglet "services", faites clic droit sur JavaDB, faites d'abord "start server", puis "create database". 

Vous entrerez les informations suivantes :

  • Nom : bookstore
  • user : app
  • password : app

Voilà, vous avez créé une base dans le SGBD JavaDB/Derby, qui s'appelle bookstore et qui a pour login/password app/app.

Ajout d'une table "book" dans la base bookstore

Vous devriez voir dans la liste des BD connectées la base que vous venez de créer. Faites clic droit puis "connect". Ensuite, clic droit/execute command et entrez le code SQL suivant pour ajouter une table "book" à la base :

CREATE TABLE book ("id" INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1), 

        title VARCHAR(80), description VARCHAR(2000), pubdate DATE, price DECIMAL);

Cliquez ensuite sur la première petite icône sur la doite (celle avec le tooltip "execute SQL"). Cela va exécuter les ordres de création. Ouvrez sur la gauche l'entrée correspondant à la base bookstore, sous noeud APP/Tables et vous devriez voir la table "book".

Rendre cette base visible par Glassfish, lui associer un nom JNDI jdbc/bookstore

Maintenant nous allons "connecter cette base" à glassfish afin qu'il puisse rendre la connexion accessible par les applications web qu'il fait tourner.

Dans l'onglet services/serveur clic droit sur glassfish et "view admin console". La console d'administration de glassfish doit s'afficher dans un navigateur. Les logins / passwords, si vous ne les avez pas changés doivent être app et appapp. J'espère pour vous que vous ne les avez pas changés et que vous ne les avez pas oubliés...

Dans la console glassfish, vous devez d'abord créer un "pool de connexion" pour la base bookstore.  Une fois la page de la console affichée, regardez dans le menu de gauche JDBC, ouvrez ce noeud et cliquez sur "pool de connexions JDBC".

  • Cliquez sur le bouton "nouveau" dans la table des connexions existantes,
  • nom du pool : bookstore
  • type de ressource : javax.sql.Datasource
  • fournisseur de base de données : "Derby", cliquez sur "suivant" pour accèder à la page suivante.
  • Dans la page suivante, ne touchez à rien, mais en bas supprimez toutes les propriétés à part user, databaseName, password, ServerName, PortNumber
  • Vous mettre dans l'ordre app, bookstore, app, localhost, 1527
  • (Facultatif, ça marche sans...) Vous ajouterez une propriété URL avec pour valeur l'URL de votre base, si vous avez suivi tout ce que nous avons dit, l'URL doit être jdbc:derby://localhost:1527/bookstore
  • Enregistrez.
  • Dans la table vous devriez avoir bookstore comme entrée, cliquez dessus. En haut de la page cliquez sur "ping", vous devriez avoir un message "commande ping réussie", ce qui signifie que glassfish s'est bien connecté à la base.

 Snap9.jpg

Snap10.jpg

Vérification que tout est ok :

Ping.jpg

Maintenant, on va associer à ce "pool de connexion" un nom JNDI. Le nom JNDI est une chaine de caractères du type "jdbc/nom..." par convention, dans le cas d'une BD. On appelle "pool de connexion"  une base de données connectée à un serveur car ce dernier garde ouvert en permanence plusieurs connexions avec la base de données et les allouera à la demande aux diverses applications qui les demandent.

Nous avons créé le pool de connexion, il reste maintenant à associer le nom JNDI "jdbc/bookstore" à ce pool :

  • Dans la console d'administration de glassfish, cliquez sur "Ressources JDBC", puis faites cliquez sur le bouton "nouveau",
  • Dans "nom JNDI" entrez jdbc/bookstore,
  • Selectionnez "bookstore" dans le menu déroulant permettant de choisir le pool de connexion.
  • Sauvegardez. C'est fini !

Bon, qu'avons nous : 

  1. Une base de donnée "bookstore" visible par glassfish,
  2. Cette base est exposée sous la forme d'un objet dont le nom JNDI est "jdbc/bookstore" et le type "Datasource".

Il reste maintenant à l'utiliser depuis un gestionnaire de livre !

Ecriture d'une seconde implémentation d'un gestionnaire de livre, utilisant JDBC et SQL pour se connecter à la base de donnée bookstore

Avant de rentrer dans les détails, créez une nouvelle classe nommée : BookRepositoryJDBCImpl, située dans le package com.bookstore. 

Remplacez son contenu par le suivant BookRepositoryJDBCImpl.java :

package com.bookstore;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;
import java.sql.Date;

@ApplicationScoped
@JDBC
public class BookRepositoryJDBCImpl implements BookRepository {

    @Resource(lookup = "jdbc/bookstore") // Ce doit être le nom JNDI de la base !
    private DataSource dataSource;
    private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");

    @Override
    public Book lookupBookById(final String id) {
        return withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("select * from book where \"id\" = ?");
                prepareStatement.setLong(1, Long.parseLong(id));
                ResultSet rs = prepareStatement.executeQuery();

                if (rs.next()) {
                    Book book = new Book();
                    book.setId("" + rs.getLong("id"));
                    book.setPrice(rs.getBigDecimal("price"));
                    book.setPubDate(rs.getDate("pubdate"));
                    book.setTitle(rs.getString("title"));
                    // Missing from online tutorial !
                    book.setDescription(rs.getString("description"));
                    return book;
                } else {
                    return null;
                }

            }
        });
    }

    @Override
    public void addBook(final String title, final String description,
            final String price, final String pubDate) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("insert into book (title, description, price, pubdate) values (?,?,?,?)");
                prepareStatement.setString(1, title);
                prepareStatement.setString(2, description);
                prepareStatement.setBigDecimal(3, new BigDecimal(price));

                Calendar calendar = Calendar.getInstance();
                calendar.setTime(dateFormat.parse(pubDate));
                prepareStatement.setDate(4,
                        new Date(calendar.getTimeInMillis()));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to insert book into bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public void updateBook(final String id, final String title,
            final String description, final String price, final String pubDate) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("update book set  \"TITLE\"=?, \"DESCRIPTION\"=?, \"PRICE\"=?, \"PUBDATE\"=? where \"id\" = ?");
                prepareStatement.setString(1, title);
                prepareStatement.setString(2, description);
                prepareStatement.setBigDecimal(3, new BigDecimal(price));

                Calendar calendar = Calendar.getInstance();
                calendar.setTime(dateFormat.parse(pubDate));
                prepareStatement.setDate(4,
                        new Date(calendar.getTimeInMillis()));

                prepareStatement.setLong(5, Long.parseLong(id));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to update book into bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public void removeBook(final String id) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("delete from book where \"id\" = ?");
                prepareStatement.setLong(1, Long.parseLong(id));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to remove book from bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public List<Book> listBooks() {
        System.out.println("DANS LIST BOOKS");
        return doList(null);
    }

    static interface JDBCRunner<T> {

        T run(Connection connection) throws Exception;
    }

    private List<Book> doList(final String orderBy) {
        return withDB(new JDBCRunner<List<Book>>() {
            @Override
            public List<Book> run(Connection connection) throws Exception {
                List<Book> listing = new ArrayList<Book>();
                Statement statement = connection.createStatement();
                final String query = "select * from book"
                        + (orderBy != null ? " ORDER BY " + orderBy + "" : "");
                ResultSet rs = statement.executeQuery(query);
                System.out.println("SELECT * nb=");
                while (rs.next()) {
                    System.out.println("FOUND NEW BOOK");
                    Book book = new Book();
                    book.setId("" + rs.getLong("id"));
                    book.setPrice(rs.getBigDecimal("price"));
                    book.setPubDate(rs.getDate("pubdate"));
                    book.setTitle(rs.getString("title"));
                    // Missing from online tutorial !
                    book.setDescription(rs.getString("description"));
                    listing.add(book);
                }
                return listing;
            }
        });
    }

    private <T> T withDB(JDBCRunner<T> runner) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            boolean auto = connection.getAutoCommit();
            connection.setAutoCommit(false);

            T result = runner.run(connection);

            connection.commit();
            connection.setAutoCommit(auto); // set it to what it was previously.
            return result;
        } catch (Exception ex) {
            ex.printStackTrace();
            try {
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

Voici la classe BookStoreException.java utilisée :

package com.bookstore;

public class BookstoreException extends Exception {

    public BookstoreException() {
    }

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

    public BookstoreException(Exception e) {
        super(e);
    }
}

 

Ouah ! C'est long et ça a l'air compliqué. Oui, faire proprement du JDBC c'est assez verbeux... Nous verrons plus tard comment diviser par dix ce code, mais ce sera l'object d'un nouveau chapitre du cours.

Que remarquons-nous dans ce code ? Tout d'abord il implémente l'interface BookRepository.java il possède donc les mêmes méthodes que l'implémentation précédente:

@ApplicationScoped @JDBC
public class BookRepositoryJDBCImpl implements BookRepository {

Par ailleurs on a ajouté un "Qualifier" (le qualifier c'est @JDBC ici), terme utilisé par l'API Java Context and Dependency Injection (CDI). Un qualifier est un label facile à retenir qui sera utile pour différencier plusieurs implémentations lors de l'injection. On y revient un peu plus loin quand on détaille la manière dont on va injecter ce gestionnaire qui utilise JDBC.

Deuxièmement, regardons comment on accède à la base de données :

@Resource(lookup = "jdbc/bookstore")
    private DataSource dataSource;

On y accède avec une annotation de code. C'est l'annotation @Resource qui va obtenir la connexion, auprès du serveur une fois l'application déployée. Pas besoin de driver, login, password, etc...

Le reste du code utilise plusieurs astuces de conception. En effet, pour chaque opération impliquant un accès à la base de données, les étapes sont toujours les mêmes :

  1. On ouvre la connextion, on démarre une transaction (begin...)
  2. On envoie la requête SQL, on récupère et traite les résultats,
  3. On commit la transaction et on ferme la connexion.

Bien entendu il faut faire des try... catch... finally pour traiter tous les cas d'erreur, etc... Plutot que de re-écrire le code qui ne change pas dans chaque méthode d'ajout, recherche, suppression de livres, on a opté pour une conception plus générique. Tout le salle travail est en réalité effectué dans la méthode suivante :

private <T> T withDB(JDBCRunner<T> runner) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            boolean auto = connection.getAutoCommit();
            connection.setAutoCommit(false);

            T result = runner.run(connection);

            connection.commit();
            connection.setAutoCommit(auto); // set it to what it was previously.
            return result;
        } catch (Exception ex) {
            ex.printStackTrace();
            try {
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

La seule ligne qui varie dans ce code est :  T result = runner.run(connection); qui revient à appeler la méthode run() d'un objet de type JDBCRunner passé en paramètre. Le reste du code c'est de "la tambouille JDBC".

Cette méthode est très classique auprès des programmeurs JDBC expérimentés.

JDBCRunner est juste une interface privée que l'on trouve dans le même fichier :

static interface JDBCRunner<T> {

        T run(Connection connection) throws Exception;
    }

En gros, toute classe qui implémente ou qui dérive de JDBCRunner doit possèder une méthode run() qui renvoie un objet de type T.

Pour comprendre comment ces éléments s'emboîtent, regardons simplement le code de la méthode qui renvoie la liste des livres :

private List<Book> doList(final String orderBy) {
        return withDB(new JDBCRunner<List<Book>>() {
            @Override
            public List<Book> run(Connection connection) throws Exception {
                List<Book> listing = new ArrayList<Book>();
                Statement statement = connection.createStatement();
                final String query = "select * from book"
                        + (orderBy != null ? " ORDER BY " + orderBy + "" : "");
                ResultSet rs = statement.executeQuery(query);
                System.out.println("SELECT * nb=");
                while (rs.next()) {
                    System.out.println("FOUND NEW BOOK");
                    Book book = new Book();
                    book.setId("" + rs.getLong("id"));
                    book.setPrice(rs.getBigDecimal("price"));
                    book.setPubDate(rs.getDate("pubdate"));
                    book.setTitle(rs.getString("title"));
                    // Missing from online tutorial !
                    book.setDescription(rs.getString("description"));
                    listing.add(book);
                }
                return listing;
            }
        });
    }

Cette méthode renvoie l'exécution de la méthode withDB que nous avons déjà, vu, celle qui fait la tambouille JDBC. Le paramètre passé à cette fonction est une instance d'une classe interne anonyme :

 

(new JDBCRunner<List<Book>>() {
            @Override
            public List<Book> run(Connection connection) throws Exception {
                ...
            }
       });

 

 

Le code ci-dessus est semblable à celui des écouteurs de boutons en Swing/Awt... (new JDBCRunner<List<Book>>() {...}); est une instance anonyme (on a pas mis le résultat dans une variable, on l'a juste passé en paramètre à withDB), qui implémente JDBCRunner et donc possède une méthode run. Ca tombe, bien, la méthode withDB appelle la méthode run de l'objet qu'elle reçoit en paramètres !!!

Donc que fait la méthode doList ?

  1. Elle appelle withDB, lui passe en paramètre un objet qui a la méthode run() à appeler juste après qu'elle ait ouvert la connexion et démarré une transaction, et juste avant de fermer la connexion et comiter la transaction.
  2. Elle renvoie (si tout s'est bien passé, s'il n'y a pas eu de rollback) le résultat renvoyé par la méthode run()...

Regardons donc le code de la méthode run. Nous verrons les détails lors du cours JDBC.

  • En JDBC on exécute les requêtes au travers d'objets spéciaux qu'on appelle des "statements". On obtient un statement à partir de la connexion à la BD : Statement statement = connection.createStatement();
  • Quand on exécute une requête SQL à travers un statement on récupère un ResultSet : ResultSet rs = statement.executeQuery(query);
  • On itère sur le ResultSet pour récupérer chaque ligne de résultat : while (rs.next()) {... }
  • On récupère les valeurs des colonnes en faisant dans l'ordre des getXXX ou XXX = le type de la colonne.
  • On reconstruit un livre quand on a les différentes valeurs des colonnes + on le rajoute à une liste.

C'est tout pour la leçon JDBC d'aujourd'hui, la suite dans le cours dédié !

 

Ajout d'une classe JDBC.java et InMemory.java pour définir les qualifiers pour l'injection

Vous avez du remarquer que le terme @JDBC fait une erreur, en effet, cette annotation qu'on appelle un "qualifier" demande à être définie dans un fichier .java correspondant. Ajoutez donc au projet les deux qualifiers suivants :

JDBC.java :

package com.bookstore;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import javax.inject.Qualifier;


@Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface JDBC {
}

InMemory.java

package com.bookstore;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import javax.inject.Qualifier;


@Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface InMemory {
}

Nous verrons là aussi le détail plus tard, quand nous parlerons de CDI. Ces bouts de code définissent deux annotations spéciales, des qualifiers d'injection, qui sont utilisables à côté de @ApplicationScope

Nous avons déjà le qualifier @JDBC qui apparait dans notre gestionnaire utilisant JDBC/SQL :

 

@ApplicationScoped @JDBC
public class BookRepositoryJDBCImpl implements BookRepository {...}

 

Nous allons modifier de même l'ancienne version de notre gestionnaire de livres, celle qui fonctionnait en mémoire :

@ApplicationScoped @InMemory
public class BookRepositoryImpl implements BookRepository {...}

 

Nous avons ainsi deux implémentations, une avec @JDBC et une avec @InMemory. Ceci va nous permettre d'injecter à la volée la version qui nous intéresse :

Dans les deux Servlets du projet, on mettra :

@Inject @JDBC
 private BookRepository bookRepo;

Pour utiliser le gestionnaire de livres qui utilise JDBC...

et :

 

@Inject @InMemory
 private BookRepository bookRepo;

Pour utiliser le gestionnaire de livres qui travaille en mémoire.

Exécutez le projet dans sa version JDBC

Modifiez les deux servlets pour injecter le gestionnaire de livres utilisant JDBC. Executez le projet. Par défaut la liste des livres est vide, il faudra cliquer sur le lien "Add book" pour en ajouter.

Exécutez le projet dans sa version InMemory

Modifiez simplement les qualifiers lors de l'injection (remplacez @JDBC par @InMemory dans les servlets), exécutez, comparez ! C'est puissant n'est-ce pas ?

On peut faire beaucoup plus de choses avec CDI, vous n'avez vu que le début !

Cinquième partie : utilisation de MySQL à la place de Derby/JavaDB

Vous l'aurez peut-être remarqué, Derby/JavaDB est moins courant que MySQL. Vous pouvez très bien à titre d'exercice, créer une base MySQL appelée bookstoreMySQL, associée au nom JNDI jdbc/bookstoreMySQL dans glassfish (procédez de la même manière que pour la base que nous avons utilisée dans le TP).

Code de création de la base et de la table pour MySQL (légèrement différent de pour Derby/JavaDB) :

 

 $ mysql -u root -p

 

Puis pour la création de la base bookstoreMySQL :

 

mysql> create database bookstoreMySQL;  Output: Query OK, 1 row affected (0.00 sec)  


Utilisation de la BD  bookstoreMySQL et création de la table book.

mysql> use bookstoreMySQL;

Output:
Database changed

Création de la table book.

mysql> CREATE TABLE book (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, 
        title VARCHAR(80), description VARCHAR(2000), pubdate DATE, price DECIMAL);

Output:
Query OK, 0 rows affected (0.01 sec)
 

Voilà ! Le code pour l'implémentation JDBC est légèrement différent, car Derby oblige à quelques trucs non standards (comme mettre des double côtes devant les noms de colonnes, etc) :

package com.bookstore;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;
import java.sql.Date;

@ApplicationScoped
@JDBCMySQL
public class BookRepositoryJDBCMySQLImpl implements BookRepository {

    @Resource(lookup = "jdbc/bookstoreMySQL")
    private DataSource dataSource;

    private SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy");

    @Override
    public Book lookupBookById(final String id) {
        return withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("select * from book where id = ?");
                prepareStatement.setLong(1, Long.parseLong(id));
                ResultSet rs = prepareStatement.executeQuery();

                if (rs.next()) {
                    Book book = new Book();
                    book.setId("" + rs.getLong("id"));
                    book.setPrice(rs.getBigDecimal("price"));
                    book.setPubDate(rs.getDate("pubdate"));
                    book.setTitle(rs.getString("title"));
                    // Missing from online tutorial !
                    book.setDescription(rs.getString("description"));
                    return book;
                } else {
                    return null;
                }

            }
        });
    }

    @Override
    public void addBook(final String title, final String description,
            final String price, final String pubDate) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("insert into book (title, description, price, pubdate) values (?,?,?,?)");
                prepareStatement.setString(1, title);
                prepareStatement.setString(2, description);
                prepareStatement.setBigDecimal(3, new BigDecimal(price));

                Calendar calendar = Calendar.getInstance();
                calendar.setTime(dateFormat.parse(pubDate));
                prepareStatement.setDate(4,
                        new Date(calendar.getTimeInMillis()));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to insert book into bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public void updateBook(final String id, final String title,
            final String description, final String price, final String pubDate) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("update book set  title=?, description=?, price=?, pubdate=? where id = ?");
                prepareStatement.setString(1, title);
                prepareStatement.setString(2, description);
                prepareStatement.setBigDecimal(3, new BigDecimal(price));

                Calendar calendar = Calendar.getInstance();
                calendar.setTime(dateFormat.parse(pubDate));
                prepareStatement.setDate(4,
                        new Date(calendar.getTimeInMillis()));

                prepareStatement.setLong(5, Long.parseLong(id));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to update book into bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public void removeBook(final String id) {
        withDB(new JDBCRunner<Book>() {
            @Override
            public Book run(Connection connection) throws Exception {

                PreparedStatement prepareStatement = connection
                        .prepareStatement("delete from book where id = ?");
                prepareStatement.setLong(1, Long.parseLong(id));

                int rowCount = prepareStatement.executeUpdate();
                if (rowCount != 1) {
                    throw new BookstoreException(
                            "Unable to remove book from bookstore");
                }
                return null;
            }
        });

    }

    @Override
    public List<Book> listBooks() {
        System.out.println("DANS LIST BOOKS");
        return doList(null);

    }

    static interface JDBCRunner<T> {

        T run(Connection connection) throws Exception;
    }

    private List<Book> doList(final String orderBy) {
        return withDB(new JDBCRunner<List<Book>>() {
            @Override
            public List<Book> run(Connection connection) throws Exception {
                List<Book> listing = new ArrayList<Book>();
                Statement statement = connection.createStatement();
                final String query = "select * from book"
                        + (orderBy != null ? " ORDER BY " + orderBy + ";" : ";");
                ResultSet rs = statement.executeQuery(query);
                while (rs.next()) {
                    Book book = new Book();
                    book.setId("" + rs.getLong("id"));
                    book.setPrice(rs.getBigDecimal("price"));
                    book.setPubDate(rs.getDate("pubdate"));
                    book.setTitle(rs.getString("title"));
                    // Missing from online tutorial !
                    book.setDescription(rs.getString("description"));
                    listing.add(book);
                }
                return listing;
            }
        });
    }

    private <T> T withDB(JDBCRunner<T> runner) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            boolean auto = connection.getAutoCommit();
            connection.setAutoCommit(false);

            T result = runner.run(connection);

            connection.commit();
            connection.setAutoCommit(auto); // set it to what it was previously.
            return result;
        } catch (Exception ex) {
            try {
                connection.rollback();
            } catch (SQLException e) {
                // should log this as a warn or info
            }
            return null;
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    // should log this as a warn or info
                }
            }
        }
    }

}

Pensez à ajouter aussi le code qu'il faut pour l'annotation @JDBCMySQL et bien sûr à ajouter le driver JDBC MySQL à votre projet (clic droit/propriétés/librairies/add library et MySQL).

JDBCMySQL.java :

 

package com.bookstore;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import javax.inject.Qualifier;


@Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface JDBCMySQL {
}