Archives mensuelles : mai 2012

P#|@#%* de Validations !

Dans nos applications web et back office du monde Java EE, l’un des problèmes récurrents les plus difficiles est la validation des données. Même les données « simples », genre nom, prénom, email posent en fait pas mal de problèmes.

Nom, prénom ?

Imaginons un développeur, Bertrand, qui développe un formulaire d’inscription à une newsletter pour un site. Dans les spécifications, il y a écrit :

« Les noms et prénoms sont constitués uniquement de caractères alphabétiques »

Bertrand, dont c’est le premier formulaire (et qui n’a pas d’accent dans son nom), utilise donc une regex du type [a-zA-Z]+ pour les deux champs. Et là, le lendemain, l’aMOA qui recette le site lui dit :

« Je n’arrive pas à utiliser le formulaire, il me dit que mon prénom est invalide »

« Mélanie ? Ah oui je suis bête je n’ai pas autorisé les accents. »

Mélanie saisit donc « Melanie » (Meuhlanie), et ça passe. Bertrand se creuse la tête et ajoute à sa regex tous les accents qu’il a déjà vu dans un prénom ou un nom. Au test suivant l’aMOA est satisfaite, et time to market oblige, on envoie ça en prod.

Et là, bim, les utilisateurs appellent, ils ne peuvent pas s’inscrire. Eh oui, impossible quand on s’appelle Jacques-Henri de la Roche Meunière. Les espaces et les tirets sont interdits.

Bertrand et Mélanie s’en veulent d’avoir laissé passer cela. On implémente le fix et en 15 minutes, la nouvelle regex (du style [a-zA-Zéèêëàù -]+) est en place.

Quelques semaines plus tard, un nouvel ingénieur arrive dans la société. Un américain. Bertrand lui demande comment il s’appelle :

« James E. Smith Jr. »

Je ne sais pas vous, mais moi ça m’énerve beaucoup quand un site m’empêche de saisir le ë dans mon prénom, parce que sans diacritique, ça ne se prononce pas pareil, ce n’est plus mon prénom, c’est autre chose.

Il y a des gens qui pensent qu’il ne faut pas valider les données de type nom/prénom. Mais dans ce cas on laisse passer des gens qui s’appellent <em>#h$nr| t@t@. C’est un choix… Pour moi valider est important, mais comme le montre l’histoire ci-dessus, ce n’est pas forcément garanti d’arriver du premier coup  à la bonne version.

Confronté au problème, comme tous les deux mois, j’ai créé en décembre de 2011 cette regex : [a-zA-ZÀ-ÖØ-öø-ÿ -]+, en me basant sur la table des caractères unicode, et en essayant d’inclure tous les caractères avec diacritiques.

En fait cette version ne comprend pas les points pour notre ami américain avec son E. qui est juste là pour faire classe, mais je les ai ajoutés ensuite.

Et bien sur elle ne reconnaît que l’alphabet latin et ne convient pas pour des utilisateurs russes ou chinois. Pour valider владимир, ça n’ira pas.

La bonne réponse, comme toujours dépend du contexte. Si on s’adresse à un public anglophone, on peut refuser tous les diacritiques et rester sur l’ascii (encore que), pour des occidentaux on peut utiliser la regex ci-dessus, pour une cible mondiale, …

Email

Ah mais l’adresse email par contre c’est facile me direz-vous, c’est standard, il y a des RFC, etc, …

Ok, quelle est la bonne regex pour valider une adresse mail ?

La plupart des temps les développeurs valident quelque chose du style [a-zA-Z0-9-_\.]+@[a-zA-Z0-9.-_ \.]+\.[a-zA-Z]{2,4} : des alphanumérique et quelques caractères spéciaux, un @, et un nom de domaine assez libre, qui finit par .truc. Eh bien c’est faux.

Si on regarde sur Wikipédia :

  • 0@a est une adresse valide. Il n’y a pas forcément de .truc à la fin d’un nom de domaine.
  • !#$%&amp;'*+-/=?^_`{}|~@example.org est une adresse valide.
  • " une 4dresse biz@rre £"."@&amp;#"@test.com est valide aussi. En fait on peut mettre ce qu’on veut tant que c’est entre guillemets.
  • Et même user@[IPv6:2001:db8:1ff::a0b:dbd0] est valide.

En plus la regex naïve ci dessus laisse passer des cas qui ne devraient pas être autorisés, comme ab..ab..a@test..com : on ne peut pas avoir deux points d’affilée (et word reconnaît cela comme une adresse mail et ajoute le lien, funny).

Donc, on doit pouvoir saisir test@test. Mais dans 99,99% des cas, un utilisateur qui saisit une adresse comme celle-ci (valide) se plante.

Il saisit par exemple fleurymichou@hotmail ou abc123@yahoo, sans terminer le nom de domaine, et d’un point de vue ergonomique, il est plus raisonnable de ne pas lui valider son mail. Sauf que ce serait faux, et que la personne qui a de bonne foi un email qui ne finit pas par .truc est rejetée.

Ici on peut mettre en place un warning, en javascript par exemple, du type :

« êtes vous sur que cette adresse est bonne ? ».

Epilogue

Le chef de projet de Bertrand et Mélanie arrive et dit :

« Bon, maintenant on va aussi enregistrer l’adresse postale. »

PS : Pour moi bean validation [jsr 303] est une des meilleures idées d’API de ces dernières années : quelque chose de relativement simple, bien intégré dans les outils, qui traite un problème récurrent et peut vraiment faciliter la vie.

Le facteur pi pour les prototypes (et donc les logiciels).

J’ai rencontré aujourd’hui l’expression « facteur pi », utilisée un peu sarcastiquement pour les projets ambitieux technologiquement, les prototypes, comme  les fusées, ou les réacteurs nucléaires.

L’idée est que, pour un projet innovant, il faut multiplier l’estimation initiale des coûts par 3,14[1592653585…].

J’ai immédiatement pensé à la sous-estimation chronique des projets informatiques, avec le bon vieil argument « tous les logiciels sont des prototypes  », même si je pense qu’en général on est pas d’un facteur trois en dessous.

Bien sur quelqu’un d’autre y a pensé avant moi, avec même une jolie démonstration graphique, même si elle est un peu artificielle :

Je ne pense pas, comme Anna Forss le dit dans l’article sus lié, que la différence dans l’estimation provient des externalités, comme l’environnement de développement, de build etc, …  Ces sujets sont aujourd’hui facilement gérables, tant qu’on ne fait pas de trucs bizarres. La différence provient bien de la nouveauté du produit, des tâtonnements, de la mise au point, de la communication, et ainsi de suite, et ce depuis toujours et pour encore un bon moment.

Et pour quelqu’un qui a vu quelques projets, se trouve à un niveau de détail et d’information suffisant et connait les capacité de son équipe, un facteur trois d’erreur dans l’estimation est assez énorme. Quand on se plante, on est plutôt plus proche de 1,5, ce qui tiens, fait penser au nombre d’or 🙂

Exposer un service SOAP avec Play ! 2.0

Ok SOAP n’est plus trop à la mode et Play est fait pour du REST, mais il peut arriver que le client demande un service de ce type, et dans ce cas il faut bien en créer un.

Depuis Java 6, les services SOAP peuvent être écrit en annotant simplement une classe avec @WebService, avec éventuellement des @WebMethod et @WebParam pour préciser l’interface.

Pour l’exposer, en Java SE on peut faire un appel à Endpoint.publish(), mais on ne peut le faire ici, en tout cas pas sur le même port que pour le reste de l’application.

En Java EE, il suffit que la classe portant l’annotation @WebService soit déclarée dans le conteneur (par exemple dans le fichier web.xml) pour que le service soit déployé, mais il n’y a pas de tel mécanisme dans Play.

Mais bon, comme les appels SOAP se font via http, implémenter les réponses aux appels que le client peut effectuer suffit.

J’ai trouvé un exemple en Play 1, dont je me suis inspiré pour créer un service d’exemple en Play 2. L’idée est d’avoir une classe Controller, avec deux handlers, un pour retourner la wsdl et un  pour répondre aux requêtes.

Voici donc le tutoriel complet.

Création du service.

Commençons par créer le WebService :

1
2
3
4
5
6
7
8
9
10
11
12
package ws;

import javax.jws.WebService;
import javax.xml.ws.Endpoint;

@WebService
public class TheWS {

    public String hello(String message) {
        return "hello " + message;
    }
}

Cette classe ne sera pas utilisée pour le service final en Play, mais on peut ainsi générer la wsdl et des exemples de requêtes et de réponses en xml, avec un outil type SOAP UI.

1
2
3
4
public static void main(String[] args) {
    // Publishing the service (to have the wsdl)
    Endpoint.publish("http://localhost:8081/TheWS", new TheWS());
}

La wsdl :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is
    JAX-WS RI 2.1.6 in JDK 6. -->
<!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is
    JAX-WS RI 2.1.6 in JDK 6. -->
<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://ws/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://ws/"
    name="TheWSService">
    <types>
        <xsd:schema>
            <xsd:import namespace="http://ws/" schemaLocation="http://localhost:8081/TheWS?xsd=1" />
        </xsd:schema>
    </types>
    <message name="hello">
        <part name="parameters" element="tns:hello" />
    </message>
    <message name="helloResponse">
        <part name="parameters" element="tns:helloResponse" />
    </message>
    <portType name="TheWS">
        <operation name="hello">
            <input message="tns:hello" />
            <output message="tns:helloResponse" />
        </operation>
    </portType>
    <binding name="TheWSPortBinding" type="tns:TheWS">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
            style="document" />
        <operation name="hello">
            <soap:operation soapAction="" />
            <input>
                <soap:body use="literal" />
            </input>
            <output>
                <soap:body use="literal" />
            </output>
        </operation>
    </binding>
    <service name="TheWSService">
        <port name="TheWSPort" binding="tns:TheWSPortBinding">
            <soap:address location="http://localhost:8081/TheWS" />
        </port>
    </service>
</definitions>

Notez que le schéma est dans un fichier externe à la wsdl :

1
schemaLocation="http://localhost:8081/TheWS?xsd=1"

Pour simplifier, je modifie la wsdl pour avoir le schéma inclut directement, sans dépendance :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<definitions
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy"
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://ws/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://ws/"
    name="TheWSService">
    <types>
        <xsd:schema version="1.0" targetNamespace="http://ws/">
            <xsd:element name="hello" type="tns:hello" />
            <xsd:element name="helloResponse" type="tns:helloResponse" />
            <xsd:complexType name="hello">
                <xsd:sequence>
                    <xsd:element name="arg0" type="xsd:string" minOccurs="0" />
                </xsd:sequence>
            </xsd:complexType>
            <xsd:complexType name="helloResponse">
                <xsd:sequence>
                    <xsd:element name="return" type="xsd:string" minOccurs="0" />
                </xsd:sequence>
            </xsd:complexType>
        </xsd:schema>
    </types>

Vous pouvez aussi télécharger le schéma (à l’adresse http://localhost:8081/TheWS?xsd=1 donc) et l’exposer avec la wsdl dans le controller Play.

Dans tous les cas, le schéma est obligatoire pour que le client puisse être généré.

Le controller play.

Enregistrez la wsdl avec un suffixe en .scala.xml afin qu’elle soit détectée comme vue et puisse être utilisée.

On peut ensuite écrire le controller suivant :

1
2
3
4
5
6
7
8
public class WSController extends Controller {

    @BodyParser.Of(play.mvc.BodyParser.Xml.class)
    public static Result wsdl(String wsdlParam) {
        return ok(TheWSwsdl.render());
    }
        // ...
}

Et la route associée :

1
GET         /services/TheWS         controllers.WSController.wsdl(wsdl)

Répondre aux requêtes

Il faut maintenant répondre aux requêtes. Nous recevons et renvoyons directement du xml brut.

On peut utiliser n’importe quelle API xml pour parser la requête (dom, xpath, …).

Pour la réponse, le plus simple est de créer, comme pour la wsdl, un template en .scala.xml à partir de la réponse générée par SOAP UI, que l’on remplit comme une page web.

La méthode du controller :

1
2
3
4
5
6
7
8
9
10
11
12
@BodyParser.Of(play.mvc.BodyParser.Xml.class)
public static Result router() {
    try {
        // Récupération de la requpete xml.
        Document dom = request().body().asXml();
        Logger.info(dom.toString());
        return ok(helloResponse.render("Salut"));
    } catch (Exception e) {
        Logger.error(e.toString(), e);
        return internalServerError(e.toString());
    }
}

Le template de réponse :

1
2
3
4
5
6
7
@(message: String)
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <soapenv:Header />
    <soapenv:Body>
        <helloResponse xmlns="http://ws/">@message</helloResponse>
    </soapenv:Body>
</soapenv:Envelope>

Et les routes :

1
2
GET             /services/TheWS         controllers.WSController.wsdl(wsdl)
POST            /services/TheWS         controllers.WSController.router

Création du client et test.

On peut maintenant générer le client :

1
wsimport -keep http://localhost:9000/services/TheWS?wsdl

Et vérifier le bon comportement du système :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package client;

import java.net.MalformedURLException;

import ws.TheWS;
import ws.TheWSService;

public class MainClient {

    public static void main(String[] args) throws MalformedURLException {
        TheWS service = new TheWSService().getTheWSPort();
        System.out.println(service.hello("test"));
    }
}

— Edit
J’ai ajouté cet exemple sur Github
https://github.com/rlemaire/play2-soap-ws-example
/– Edit