Création d'un blog avec Web Services + client Ajax

De $1

Introduction

Dans ce TP nous allons partir d'un projet existant, le faire tourner, et l'améliorer. Il s'agit d'un petit projet simulant un blog. Nous allons commencer par créer le projet, créer une classe entité représentant les articles du blog, ajouter des web services permettant de manipuler ces données via des URIs, et enfin développer un client en HTML + JavaScript utilisant la bibliothèque jQuery pour la manipulation du DOM et pour les requêtes Ajax.

Création du projet, d'une classe entité Article, et d'un session bean façade

  1. Créez un projet de type web.
     
  2. Créez à l'aide du gestionnaire de base de données de NetBeans une base de données intitulée "blogWS" avec comme login/password admin/admin.
     
  3. Ajoutez au projet une classe entité Article, vous indiquerez au gestionnaire de persistence qu'il travaille sur une nouvelle source de données et indiquerez comme source de données celle que vous venez de créer. Autorisez le drop and create pour le moment, car nous risquons de modifier le modèle de données.
     
  4. Vous mettrez comme propriétés dans la classe Article au moins le titre de l'article, son contenu et la date à laquelle il a été posté. Rappel : une propriété est un attribut avec des getters/setters.
     
  5. Rajoutez deux constructeurs : un vide et un prenant en paramètre un titre et un contenu. Dans le constructeur vous initialiserez la date avec dateCreation = new Date() par exemple.
     
  6. Maintenant, vous allez ajouter une couche façade sous la forme d'un session bean stateless permettant de gérer des Articles (faire le CRUD) et exposant ses méthodes sous la forme de Web Services REST.

    Pour cela nous allons utiliser un wizard bien pratique de NetBeans. Faire clic droit/new/RESTful Web Services for entity classes et indiquez la classe Article comme entité. Je vous conseille d'indiquer comme package pour cette classe facade un nom comme "api" ou "services". Regardez le code généré, vous avez une classe ArticleFacadeREST qui hérite de AbstractFacade.
     
  7. Etudiez le code de ces deux classes, remarquez que le session bean stateless (il y a bien @Stateless avant la délclaration de la classe) est annoté de "chemin" (@Path) devant le nom de la classe et devant les noms des méthodes. Je vous conseille, pour des raisons pratiques, de modifier le premier chemin dans l'annotation @Path juste en dessous de @Stateless pour mettre quelque chose de plus parlant, par exemple @Path("articles")

Nous devrons par la suite certainement améliorer/modifier/ajouter des choses dans cette classe (de nouveaux Web Services par exemple, accepter de nouveaux formats d'entrée ou de sortie, etc.).

Création d'un petit client pour ajouter des données dans la base via un web service.

Dans la page index.html, nous allons ajouter un formulaire permettant d'ajouter des articles. Mettez donc un formulaire de ce type :

<form method="post" enctype="application/x-www-form-urlencoded" action="resources/articles">
            <p>
                <label for="titre">Titre</label>
                <input type="text" name="titre"/>
            </p>
            <p>
                <label for="content">Texte</label>
                <textarea id="content" name="contenu"></textarea>
            </p>

            <p>
                <input name="soumettre" type="submit">
            </p>
</form>

Ce formulaire va nous permettre de remplir la base de données sans trop se fatiguer. Il va falloir néanmoins ajouter un Web Service indiquant que l'on va travailler avec en entrée le type de données provenant d'un formulaire HTTP. Ce type n'est ni XML ni JSON...

Notez que le formulaire fait un POST (action="POST"), ce qui dans le vocabulaire REST signifie "insère dans la collection des Articles" un nouvel article, puisque l'URI de la collection sur laquelle on pointe ("resources/articles") est bien "articles". Voir le cours.

 Snap2.jpg

Ajoutez donc dans le fichier .java de vos Web Services un truc qui ressemble à cela :

@POST
    @Consumes({MediaType.APPLICATION_FORM_URLENCODED})
    public Response createFromFormulaire(@FormParam("titre") String titre,
                                         @FormParam("contenu") String contenu) {
        Article a = new Article(titre, contenu);
        super.create(a);

        // Create URI for Response  
        UriBuilder b = uriInfo.getBaseUriBuilder();
        URI u = null;
        try {
            u = new URI(b.path(ArticleFacadeREST.class).build() + "/" + a.getId());
            System.out.println("Donnée créée avec comme URI : " + u);
            System.out.println("Dans create titre: " + a.getTitre() + " content: " + a.getContenu());

        } catch (URISyntaxException ex) {
            Logger.getLogger(ArticleFacadeREST.class.getName()).log(Level.SEVERE, null, ex);
        }
        return Response.created(u).build();
    }

Rajoutez également comme attribut de cette classe une variable de type UriInfo, annotée par @Context. Cette variable nous permettra dans le code de connaitre l'URI du web service invoqué.

@Context
 UriInfo uriInfo;

Dans le fichier ApplicationConfig.java, changez le chemin "de base" d'accès à vos Web Services (ligne 14)

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package service;

import java.util.Set;
import javax.ws.rs.core.Application;

/**
 *
 * @author tirius
 */
// MICHEL BUFFA : changer la ligne ci-dessous !
@javax.ws.rs.ApplicationPath("resources")
public class ApplicationConfig extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> resources = new java.util.HashSet<Class<?>>();
        // following code can be used to customize Jersey 2.0 JSON provider:
        try {
            Class jsonProvider = Class.forName("org.glassfish.jersey.jackson.JacksonFeature");
            // Class jsonProvider = Class.forName("org.glassfish.jersey.moxy.json.MoxyJsonFeature");
            // Class jsonProvider = Class.forName("org.glassfish.jersey.jettison.JettisonFeature");
            resources.add(jsonProvider);
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(getClass().getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        addRestResourceClasses(resources);
        return resources;
    }

    /**
     * Do not modify addRestResourceClasses() method.
     * It is automatically re-generated by NetBeans REST support to populate
     * given list with all resources defined in the project.
     */
    private void addRestResourceClasses(Set<Class<?>> resources) {
        resources.add(service.ArticleFacadeREST.class);
    }
    
}

Quelques remarques :

  1. Ce web service est appelé par un @POST, il est néanmoins différent de celui qui avait été généré (que l'on n'a pas modifié, il est toujours là).
     
  2. Il renvoie un objet de type Response (ce qui est dans la RFC). Nous verrons que dans cet objet on mettra l'URI de la donnée créée.
     
  3. Il accepte en entrée un formulaire encodé. Pour celà on utilise MediaType.APPLICATION_FORM_URLENCODED qui est une constante fournie par la librairie Jersey. Elle correspond au "application/x-www-form-urlencoded" du formulaire.
     
  4. On construit l'URI de la donnée crée à l'aide des classes URIinfo, URIBuilder et URI, et on met l'URI créé dans la réponse (dernière ligne).

Exécutez votre application, utilisez le formulaire pour créer des articles, regardez à l'aide des devtools de votre navigateur la réponse envoyée chaque fois que l'on soumet le formulaire. Vous devriez avoir Location: suivi de l'URI de la donnée ajoutée.

 Snap1.jpg

Avant d'aller plus loin, nous allons tester rapidement les autres Web Services, maintenant que nous avons des données...

Test de vos Web Services

Nous allons maintenant rapidement tester les Web Services. Pour cela il faut créer un second projet de test, de type Web application.

Créez donc un second projet appelé testBlogWS, de type Web. Ne mettez rien dedans.

Revenez sur le premier projet, celui du blog et faites clic droit/testRestFul Web Services. Une fenêtre de dialogue apparait, proposant de générer un client de test pour vos Web services. Ne prenez pas la première option, mais la seconde, qui vous demande de choisir parmis les projets, celui dans lequel le client de test va être généré. Choisissez le projet testBlogWS. Cliquez sur ok, attendez un peu...

Normalement une fenêtre permettant de tester vos web services va apparaitre. Essayez de lister les données que vous avez créées dans la section précédente.

Implémentation d'un blog à l'aide d'appels AJAX

Nous allons utiliser la librairie jQuery pour ajouter/modifier/supprimer/mettre à jour les données que nous avons créées. Je vous propose de récupérer dans son intégralité un TP que m'a rendu un étudiant de M2 Miage. Tout n'est pas parfait dans ce code (notamment, la manière dont les web services renvoient des réponses).

Snap3.jpg

Récupérez le projet, dézippez le, ouvrez-le dans netbeans, regardez le fichier de configuration de la base de données utilisée (glassfish-resources.xml), créez la base, lancez le projet, étudiez-le !

Comparez les classes Article.java, les web services que vous avez écrits dans votre projet, etc. avec ceux de ce projet.

Regardez les fichiers index.html et function.js où se passent tous les appels Ajax vers les WebServices. Le fichier function.js utilise massivement la librairie JavaScript jQuery qui fournit des sélecteurs pour manipuler le DOM vraiment très pratiques. jQuery permet également d'utiliser les quatre ordres HTTP pour parler à des web services, tout en précisant les types de données envoyées et acceptées en retour.

Pour ceux qui ne connaissent pas jQuery

Si vous ne connaissez pas Ajax et jQuery, allez faire un tour sur la page de ressources jQuery que j'ai réalisée :Ressources JQuery. Voir aussi pour de nombreux tutoriaux interactifs sur jQuery : http://www.w3schools.com/jquery/default.asp les sections "jquery selectors" et "ajax".

Je fais un TP en M1 sur jQuery : TP jQuery, support de cours recommandé pour débuter : http://ejohn.org/apps/workshop/intro/

Upload de fichiers images avec le formulaire pour poster 

Testé avec NetBeans 8.0.2 et Glassfish 4.1. Glassfish 4.1.1 a des soucis de déploiement de source de données, il est buggé, donc évitez de l'utiliser pour le moment.

Supports de cours

Je vous propose dans cette partie d'implémenter l'upload d'images avec les posts dans le blog.

Regarder dans les transparents du MOOC de M.Buffa les chapitres 4 (formulaires) et chapitre 6 (persistence, File API et XmlHttpRequest level 2) les techniques proposées par HTML5 :

Le MOOC HTML5 part 2, Week 3 contient également des informations sur le drag'n'drop et l'upload de fichiers.

Ajouter un Web Service permettant la réception d'un formulaire avec des fichiers attachés

  1. Allez dans le fichier JerseyFileUpload.java et changez le chemin de destination pour le répertoire docroot du domaine par défaut de votre serveur glassfish. Regardez dans le menu tools/outils puis "server/serveur" dans NetBeans, ça vous dira où se trouve ce répertoire sur votre disque dur, il peut changer selon l'operating system et votre configuration. Ce répertoire docroot expose tous les fichiers qu'il contient via des URLs. Par exemple, un fichier test.jpg uploadé dans ce répertoire, sera accessible via http://localhost:8080/test.jpg

  2. Déployez le projet, normalement la base de données doit exister sur votre système si vous aviez déjà fait tourner le projet lors des précédentes séances.

uploadReussi.jpg 

Entrez des données dans les champs du formulaire puis sélectionnez un ou plusieurs fichiers. Cliquez sur "Submit Form", regardez les traces dans la console de NetBeans, onglet GlassFish. Si vous ne vous êtes pas trompés eur le chemin de destination, les fichiers sont uploadés. Vérifiez que tout est ok en ouvrant l'URL de vos fichiers http://localhost:8080/fichiers....

Il faut que le projet contienne les fichiers jars nécessaires au support pour le MultiPart

Attention, l'ajout de la fonctionalité permettant à Jersey (le framework de Web Services REST de JavaEE) de gérer le multipart demande à rajouter quelques fichiers jars dans votre projet, sinon les classes MultiPart et FormDataMultiPart ne seront pas reconnues, et aussi altérer légèrement le fichier de config de vos Web Services dans votre projet:

libsGlassfish.jpg

Voir la vidéo que je vous ai préparée, qui montre comment refaire cette partie du projet, pas à pas...

Code source des différentes fichiers

Code source du formulaire d'envoi (on le soumettra en ajax, voir le fichier form.js qui suit):

ajaxForm.html:

<!DOCTYPE html>

<html lang="en">
    <head>
        <title>Form with file selector + ajax / Xhr2</title>
        <link rel="stylesheet" href="form.css"/>
        <script src="form.js"></script>
    </head>
    <body>
        <form onsubmit="return sendForm();" id="myForm">
            <fieldset>
                <legend>Poster un nouveau message dans le Blog</legend>

                <label for="titre">Titre</label>
                <input type="text" id="titre" required name="titre">

                <br>

                <label for="contenu">Contenu</label>
                <input type="text" id="contenu" required name="contenu">
                <br>
                <label for="fileSelector">Images à uploader</label>
                <input type="file" id="fileSelector" multiple>
                <br>
                Progression de l'upload des fichiers: <progress value=0 id="progress"></progress>
                <p>
                    <button>Submit form</button>
            </fieldset>
        </form>
    </body>
</html>
 
 

form.js:

var myForm, fileSelector, progress;
var url = "/BlogWS2015/resources/files/upload";

window.onload = function () {
    myForm = document.querySelector("#myForm");
    fileSelector = document.querySelector("#fileSelector");
    progress = document.querySelector("#progress");

}
function sendForm() {
    console.log("in sendForm()");

    // SEND THE FORM USING AJAX

    var myForm = document.querySelector("#myForm");
    var fileSelector = document.querySelector("#fileSelector");

    // On remplit un objet FormData pour envoyer le formulaire
    // (y compris les fichiers attachés) en multipart
    var data = new FormData(myForm);

    var files = fileSelector.files;
    for (var i = 0; i < files.length; i++) {
        var name = "file";
        data.append(name, fileSelector.files[i]);
    }

    sendFormDataWithXhr2(url, data);

    return false;
}

function sendFormDataWithXhr2(url, data) {
    // ajax request
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url); // With FormData,
    // POST is mandatory

    xhr.onload = function () {
        console.log('Upload complete !');

    };

    xhr.onerror = function () {
        console.log("erreur lors de l'envoi");
    }
    
    xhr.upload.onprogress = function (e) {
        progress.value = e.loaded; // number of bytes uploaded
        progress.max = e.total;    // total number of bytes in the file
    };

    // send the request
    xhr.send(data);
}

Code source du Web Service JerseyFileUpload.java:

package service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.media.multipart.ContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;

@Path("/files")
public class JerseyFileUpload {

    //private static final String SERVER_UPLOAD_LOCATION_FOLDER = "/Users/mbuffa/Desktop/";
    private static final String SERVER_UPLOAD_LOCATION_FOLDER = "/Applications/NetBeans/glassfish-4.1/glassfish/domains/domain1/docroot/uploadedImages/";
 
    /**
     * Upload a File
     * @param form
     * @return 
     */
    @POST
    @Path("upload")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response uploadFile(FormDataMultiPart form) {
        // Get all parts
        //List<BodyPart> parts = form.getBodyParts();        
        
        System.out.println("UPLOAD MULTIPART FORM");
        // GETTING THE FORM FIELDS
        String titre  = form.getField("titre").getValue();
        String contenu  = form.getField("contenu").getValue();
        
        System.out.println("Titre = " + titre);
        System.out.println("Contenu = " + contenu);
        
        // GETTING THE FILES
        List<FormDataBodyPart> files = form.getFields("file");
        
        System.out.println("J'ai récupéré et je sauvegarde les fichiers suivants : ");

        for (FormDataBodyPart filePart : files) {
            ContentDisposition headerOfFilePart = filePart.getContentDisposition();  
            String filePath = SERVER_UPLOAD_LOCATION_FOLDER + headerOfFilePart.getFileName();
            
            InputStream fileInputStream = filePart.getValueAs(InputStream.class);
             
            System.out.println("Fichier : " + filePath);
            
            // Get the inputStream for the file and save it
            saveFile(fileInputStream, filePath);
        }

        String output = "Files saved to server location using FormDataMultiPart ! ";

        return Response.status(200).entity(output).build();
    }

    // save uploaded file to a defined location on the server
    private void saveFile(InputStream uploadedInputStream, String serverLocation) {

        try {
            OutputStream outpuStream = null;
            int read = 0;
            byte[] bytes = new byte[1024];

            outpuStream = new FileOutputStream(new File(serverLocation));
            while ((read = uploadedInputStream.read(bytes)) != -1) {
                outpuStream.write(bytes, 0, read);
            }

            outpuStream.flush();
            outpuStream.close();

            uploadedInputStream.close();
        } catch (IOException e) {

            System.out.println("Erreur dans la sauvegarde du fichier : " + serverLocation);
        }
    }
}

 Et le fichier de config des Web Services ApplicationConfig.java:

package service;

import java.util.Set;
import javax.ws.rs.core.Application;
import org.glassfish.jersey.media.multipart.MultiPartFeature;

@javax.ws.rs.ApplicationPath("resources")
public class ApplicationConfig extends Application {

    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> resources = new java.util.HashSet<>();

        // ### LA LIGNE A AJOUTER !!!!! ###
        // --------------------------------
        resources.add(MultiPartFeature.class);

        addRestResourceClasses(resources);

        return resources;
    }

    /**
     * NE PAS TOUCHER, GENERE AUTOMATIQUEMENT
     */
    private void addRestResourceClasses(Set<Class<?>> resources) {
        resources.add(service.ArticleFacadeREST.class);
        resources.add(service.JerseyFileUpload.class);
    }

}

 Travail à rendre pour la rentrée en Janvier

Faire fonctionner le projet Blog avec les contraintes suivantes:

  • Un blog bien formé au niveau HTML, comme celui présenté dans le MOOC avec des sections, articles, etc...
  • On veut pouvoir poster des messages dans le blog contenant une ou plusieurs images. Prévoir des relations 1-n entre messages et images attachées (onstockera dans la base juste le noms des fichiers, les URLs seront calculés pour afficher les images dans le blog au niveau du rendu des articles).
  • Preview des images et drag'n'drop possible dans le formulaire
  • Bonus: un canvas pour dessiner ou pour modifier les images avant l'envoi.
  • Bien entendu: Web Services REST Jersey derrière.

En binôme. Vous le rendrez le projet lors de la première séance du prochain cours que j'ai avec vous.