lundi 5 décembre 2011

Carte Google Map plein écran avec jQuery Mobile

jQuery mobile est arrivé en version finale il y a quelques jours. Voyons avec quelle simplicité jQuery mobile nous permet de créer des Mashups à base de Google Map API et portable sur n'importe quel terminal mobile supporté par jQuery mobile.

Récupération du template HTML5

jQuery mobile propose des templates de pages permettant de démarrer rapidement. Nous utiliserons le template suivant, légèrement modifié afin de pouvoir utiliser les icones personnalisées map icons de Nicolas Mollet disponible sous licence Creative Commons 3.0 BY-SA :
<!DOCTYPE html> 
<html> 
    <head> 
    
    <title>Fullscreen Google map with jQuery mobile</title>
    
    <meta name="viewport" content="width=device-width, initial-scale=1"> 
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.css" />
    <script src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.0/jquery.mobile-1.0.min.js"></script>
    
</head> 

<body> 

<div data-role="page" id="map" data-theme="a">

    <div data-role="header" id="header" data-position="fixed">
        <h1>Fullscreen Google map with jQuery mobile</h1>
    </div>

    <div data-role="content" id="content">    
        <!-- carte google map -->
    </div>
    
    <div data-role="footer" data-position="fixed">        
        <div data-role="navbar" data-iconpos="top">
            <ul>
                <li>
                    <a class="ui-btn ui-btn-icon-top ui-btn-up-a ui-btn-active" 
        data-icon="information" href="#" >
                        Infos
                    </a>
                </li>
                <li>
                    <a class="ui-btn ui-btn-icon-top ui-btn-up-a" data-icon="favorite" href="#">
                        Favoris
                    </a>
                </li>
                <li>
                    <a class="ui-btn ui-btn-icon-top ui-btn-up-a" data-icon="radar" href="#">
                        Radars
                    </a>
                </li>
            </ul>
        </div>
    </div>
    
</div>

</body>
</html>
Les icônes personnalisées déposées dans le répertoire static/images sont mises en place avec le CSS personnalisé ci dessous :
<style type="text/css">
    .ui-icon.ui-icon-favorite, .ui-icon.ui-icon-radar, 
    .ui-icon.ui-icon-information, .ui-icon.ui-icon-evenement {
        margin-top: -16px;
        margin-left: -19px;
        width: 37px;
        height: 37px;
        box-shadow: none;
    }
    .ui-icon.ui-icon-favorite {
        background: url(static/images/favorite.png) top center;
    }
    .ui-icon.ui-icon-radar {
        background: url(static/images/radar.png) top center;
    }
    .ui-icon.ui-icon-information {
        background: url(static/images/information.png) top center;
    }
    .ui-icon.ui-icon-evenement {
        background: url(static/images/evenement.png) top center;
    }
</style>
Ce qui nous donne le rendu de page suivant, avec sa barre de navigation style iPhone contenant nos icônes personnalisées :

Chargement de la carte Google en plain écran

L'étape suivante consiste à charger notre carte et à dimensionner son conteneur de façon à ce qu'il occupe la totalité de l'écran. Le padding du corps de la page qui contient le conteneur de la carte est positionné à 0 :
<div data-role="content" id="content" style="padding:0">
    <div id="map_canvas"></div>
</div>
L'événement pageshow de la page est utilisé pour de déterminer les dimensions maximales de la carte afin qu'elle occupe tout l'espace disponible. Cet évènement permet de s'assurer que tous les widgets jQuery mobiles ont été initialisés par le framework :
<script type="text/javascript">
    function initialize() {
        var latlng = new google.maps.LatLng(45.7452, 4.8418);
        var myOptions = {
            zoom: 14,
            center: latlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
        var map = new google.maps.Map(
                                document.getElementById("map_canvas"), 
                                myOptions);

    }
    
    $('#map').live('pageshow', function(event) {
        $("#map_canvas").width($(document).width());
        $("#map_canvas").height(
            $(window).height() 
            - $("div.ui-footer").outerHeight() 
            - $("div.ui-header").outerHeight()
        );
        initialize();
    });
</script>
Et voila, comme diraient nos amis anglophones !

jeudi 1 septembre 2011

Contrainte d'unicité et clé étrangère dans un XSD

Les XML Schema Definition ou XSD sont apparus en 2001. Il permettent de définir la structure et le type de contenu d'un fichier XML. En Java, on les rencontre régulièrement au coeur des web services dans l'élaboration de WSDL, à la base d'outils de génération automatique de code tels que Castor, lors de l'élaboration de modules spécifiques paramétrables par l'utilisateur (les développeurs s'orienteront plutôt vers les annotations depuis JAVA 5), ...

Les XSD offrent également la possibilité de définir des contraintes d'unicité sur la valeur de certains nœuds et de leurs attributs ainsi que des relations de type clés étrangères ou foreign key. C'est cet aspect que nous allons traité au travers d'un exemple très simple.

Prenons le fichier XML suivant, permettant de décrire une équipe de football :
<equipe 
    nom="Olympique Lyonnais"
    xmlns="http://avianey.blogspot.com" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://avianey.blogspot.com my.xsd">
        
    <effectif>
        <joueur nom="Lloris" prenom="Hugo" numeroMaillot="1">
            <poste>gardien</poste>
        </joueur>
        <joueur nom="Bastos" prenom="Michel" numeroMaillot="11">
            <poste>milieu</poste>
        </joueur>
        <joueur nom="Lopez" prenom="Lisandro" numeroMaillot="11">
            <poste>attaquant</poste>
        </joueur>
    </effectif>

    <capitaine numeroMaillot="9"/>

</equipe>
Ce fichier XML est valide au regard du schéma XSD suivant :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">
    
    <xs:element name="equipe">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="effectif" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="joueur" minOccurs="1" 
                                        maxOccurs="unbounded" type="joueur"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element name="capitaine" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:attribute name="numeroMaillot" use="required" type="xs:int"/>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
            <xs:attribute name="nom" use="required" type="xs:string"/>
        </xs:complexType>
    </xs:element>
    
    <xs:complexType name="joueur">
        <xs:sequence>
            <xs:element name="poste" minOccurs="1" maxOccurs="1">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                      <xs:enumeration value="gardien"/>
                      <xs:enumeration value="défenseur"/>
                      <xs:enumeration value="milieu"/>
                      <xs:enumeration value="attaquant"/>
                    </xs:restriction>
                </xs:simpleType>
            </xs:element>
        </xs:sequence>
        <xs:attribute name="nom" type="xs:string" use="required"/>
        <xs:attribute name="prenom" type="xs:string" use="required"/>
        <xs:attribute name="numeroMaillot" use="required">
            <xs:simpleType>
                <xs:restriction base="xs:int">
                    <xs:minInclusive value="0"/>
                </xs:restriction>
            </xs:simpleType>
        </xs:attribute>
    </xs:complexType>
    
</xs:schema>
Ce qui n'est pas totalement satisfaisant car notre XML comporte deux joueurs avec un même numéro de maillot et parce que le capitaine est définit par référence au numéro de maillot d'un joueur qui n'existe pas !

Ajout d'une contrainte d'unicité

Nous souhaitons maintenant pouvoir imposer l'unicité des numéros de maillots entre les joueurs d'une équipe. Pour cela il faut ajouter une contrainte d'unicité sur l'attribut numeroMaillot d'une balise <joueur>. La contrainte d'unicité s'ajoute dans le schéma XSD au niveau de la déclaration d'un élément parent de l'élément sur lequel la contrainte d'unicité doit s'appliquer. La balise à utiliser est la balise <unique> se positionne à l'intérieure de la balise <element> :
<xs:unique name="uniciteNumeroMaillot" >      
    <xs:selector xpath=".//tns:joueur" />      
    <xs:field xpath="@numeroMaillot" />    
</xs:unique>
La contrainte d'unicité doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises sur lesquelles la contrainte d'unicité doit s'appliquer.
<field>
Expression XPATH indiquant le champ ou attribut portant la contrainte. Identifiant de la valeur devant être unique.
Les expressions XPATH doivent être qualifiées afin que la contrainte soit correctement interprétée par la majorité des parseurs et validateurs. Il convient donc de déclarer un namespace préfixé identique au namespace cible (si cela n'est pas déjà le cas) :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:tns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">

Ajout d'une contrainte de type clé étrangère

Maintenant qu'un même numéro de maillot ne peut pas être attribué à deux joueurs différents, nous désirons nous assurer que le capitaine de l'équipe est bien désigné par un numéro de maillot attribué à un joueur de l'équipe. Une foreign key doit donc être mise en place entre l'attribut numeroMaillot de la balise <capitaine> et celui des balises <joueur>. L'attribut numeroMaillot des balises <joueur> est dans un premier temps déclaré en tant que clé dans le schéma XSD au niveau de la déclaration d'un élément parent des balises <joueur>. La balise à utiliser est la balise <key> se positionne à l'intérieure de la balise <element> :
<xs:key name="numeroMaillotCapitaine">
    <xs:selector xpath=".//tns:joueur" />
    <xs:field xpath="@numeroMaillot" />
</xs:key>
La clé doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises qui porteront les valeurs de référence.
<field>
Expression XPATH indiquant le champ ou l'attribut portant la valeur de référence.
Au même niveau que la balise <key>, la balise <keyref> permet de spécifier les éléments qui seront contraints par la clé étrangère :
<xs:keyref name="numeroMaillotCapitaineRef" refer="numeroMaillotCapitaine">
    <xs:selector xpath="./tns:capitaine" />
    <xs:field xpath="@numeroMaillot" />
</xs:keyref>
La référence doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises qui seront contraintes par la clé étrangère.
<field>
Expression XPATH indiquant le champ ou l'attribut dont la valeur devra être identique à une valeur de référence.
Il est important de faire attention au type des données utilisées pour la clé et la référence qui doivent être les mêmes. Un entier déclaré avec le type xs:int ne sera pas considéré comme étant égal à sa représentation sous la forme d'une chaine de caractère de type xs:string. Comme pour la contrainte d'unicité, les balises utilisées dans les chemins XPATH devront elles aussi être préfixées. Au final, notre schéma XSD sera le suivant :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:tns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">
    
    <xs:element name="equipe">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="effectif" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="joueur" minOccurs="1" 
                                        maxOccurs="unbounded" type="joueur"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element name="capitaine" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:attribute name="numeroMaillot" use="required" type="xs:int"/>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
            <xs:attribute name="nom" use="required" type="xs:string"/>
        </xs:complexType>
        <xs:unique name="uniciteNumeroMaillot" >      
            <xs:selector xpath=".//tns:joueur" />      
            <xs:field xpath="@numeroMaillot" />    
        </xs:unique>
        <xs:key name="numeroMaillotCapitaine">
            <xs:selector xpath=".//tns:joueur" />
            <xs:field xpath="@numeroMaillot" />
        </xs:key>
        <xs:keyref name="numeroMaillotCapitaineRef" refer="numeroMaillotCapitaine">
            <xs:selector xpath="./tns:capitaine" />
            <xs:field xpath="@numeroMaillot" />
        </xs:keyref>
    </xs:element>
    
    <xs:complexType name="joueur">
        <xs:sequence>
            <xs:element name="poste" minOccurs="1" maxOccurs="1">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                      <xs:enumeration value="gardien"/>
                      <xs:enumeration value="défenseur"/>
                      <xs:enumeration value="milieu"/>
                      <xs:enumeration value="attaquant"/>
                    </xs:restriction>
                </xs:simpleType>
            </xs:element>
        </xs:sequence>
        <xs:attribute name="nom" type="xs:string" use="required"/>
        <xs:attribute name="prenom" type="xs:string" use="required"/>
        <xs:attribute name="numeroMaillot" use="required">
            <xs:simpleType>
                <xs:restriction base="xs:int">
                    <xs:minInclusive value="0"/>
                </xs:restriction>
            </xs:simpleType>
        </xs:attribute>
    </xs:complexType>
    
</xs:schema>
Le fichier XML initial n'est plus valide. Il faut rectifier le numéro de maillot de Lisandro Lopez pour que chaque joueur dispose de son propre numéro de maillot et que celui du capitaine correspondent à celui d'un joueur :
<equipe 
    nom="Olympique Lyonnais"
    xmlns="http://avianey.blogspot.com" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://avianey.blogspot.com unique_key.xsd">
        
    <effectif>
        <joueur nom="Lloris" prenom="Hugo" numeroMaillot="1">
            <poste>gardien</poste>
        </joueur>
        <joueur nom="Bastos" prenom="Michel" numeroMaillot="11">
            <poste>milieu</poste>
        </joueur>
        <joueur nom="Lopez" prenom="Lisandro" numeroMaillot="9">
            <poste>attaquant</poste>
        </joueur>
    </effectif>

    <capitaine numeroMaillot="9"/>

</equipe>

vendredi 10 juin 2011

Déployer un Webservice avec la JSR-181 et CXF

La JSR-181 a facilité l'exposition de méthodes JAVA par webservice. Avec les outils et frameworks actuels, il n'a jamais été aussi simple de créer et d'exposer un service SOAP en JAVA. Voyons en pratique comment s'y prendre pour deployer un service SOAP en utilisant CXF, Maven, et n'importe quel serveur d'application (Jonas, JBoss, Websphere, Glassfish) ou conteneur de Servlet (Tomcat, Jetty).

La solution présentée est indépendante du serveur d'application utilisé et pourra être optimisée en fonction du serveur JEE cible.

La méthode que nous allons exposer permet de récupérer le résultat de la multiplication des nombres qui lui sont passés en paramètres :
package com.googlecode.avianey.cxf.v1;

public class Multiplier {

    public Long multiply(Long...in) {
        long result = 1l;
        for (long mult : in) {
            result *= mult;
        }
        return Long.valueOf(result);
    }

}

Quelques annotations suffisent...

Pour exposer la méthode en utilisant la JSR-181, il suffit de trois annotations :
WebService
Cette annotation permet de déclarer la classe comme webservice et et de configurer certains attributs du WSDL (portType, service et targetNamespace).
SOAPBinding
Cette annotation permet de spécifier le type d'encodage des messages SOAP encapsulés en HTTP.
WebMethod
Cette annotation permet de rendre visible une méthode de la classe. Une opération est associée à la méthode au niveau du WSDL.
package com.googlecode.avianey.cxf.v1;

import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(
    name = "multiplier",
    serviceName="multiplier",
    targetNamespace = "http://cxf.avianey.googlecode.com/V1")
@SOAPBinding(
    style = SOAPBinding.Style.DOCUMENT,
    use = SOAPBinding.Use.LITERAL,
    parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)
public class Multiplier {

    @WebMethod( operationName = "multiply" )
    public Long multiply(Long...in) {
        long result = 1l;
        for (long mult : in) {
            result *= mult;
        }
        return Long.valueOf(result);
    }

}

...ou presque

Il nous reste à configurer CXF pour que le service soit exposé à l'adresse souhaitée. CXF s'appuyant sur Spring, il faut déclarer le ContextLoaderListener Spring au niveau du web.xml de l'application, puis paramétrer la Servlet CXF sur une URL. Veillez à bien être en Servlet Specification 2.4 au minimum, sans quoi le contexte Spring ne sera pas chargé avant l'initialisation de la Servlet CXF.

    contextConfigLocation/WEB-INF/beans.xml

    
        org.springframework.web.context.ContextLoaderListener
    



    CXFServlet
    
        org.apache.cxf.transport.servlet.CXFServlet
    
    1



    CXFServlet
    /services/*
Le fichier de configuration Spring permet de déclarer la classe Multiplier en tant que bean et de l'exposer en tant que endpoint sur une adresse précise au moyen des extensions JAX-WS.


    
    
    

    

    
    

Configuration Maven

La configuration des dépendances Maven est minimaliste :

    
        org.apache.cxf
        cxf-rt-frontend-jaxws
        ${cxf.version}
    
    
        org.apache.cxf
        cxf-rt-transports-http
        ${cxf.version}
    
    
        javax
        javaee-api
        6.0
        compile
    
Toutefois, CXF propose un plugin Maven (cxf-java2ws-plugin) permettant de générer le WSDL du service au moment de la compilation. Dans l'exemple ci dessous, le WSDL du service sera créé dans le répertoire :
  • /src/main/webapp/WEB-INF/wsdl/

    org.apache.cxf
    cxf-java2ws-plugin
    ${cxf.version}
    
        
            org.apache.cxf
            cxf-rt-frontend-jaxws
            ${cxf.version}
        
        
            org.apache.cxf
            cxf-rt-frontend-simple
            ${cxf.version}
        
    
    
        
            process-classes
            process-classes
            
                ${basedir}/src/main/webapp/WEB-INF/wsdl/multiplier.wsdl
                multiplier
                com.googlecode.avianey.cxf.v1.Multiplier
                true
                true
            
            
                java2ws
            
        
    

Déploiement sous Tomcat

Il ne reste plus qu'à démarrer notre serveur d'application. Le service est accessible à l'adresse {context-root}/{url-pattern}/{adresse} où :
  • context-root - Le contexte de la webapp
  • url-pattern - Le mapping de la Servlet CXF tel que défini dans le web.xml
  • adresse - L'adresse du endpoint telle que définie dans le fichier de configuration Spring/CXF

Liste des services exposés par CXF

dimanche 15 mai 2011

Un tour de Norvège avec Hurtigruten

Retour d'expérience avec l'express côtier

De retour de voyage avec Hurtigruten en norvège, voici un retour d'expérience qui pourra en aider quelques un à finaliser les détails de leur croisière avec Hurtigruten.

Le voyage en question s'est déroulé pendant les vacances de pâques 2011, à cheval sur la fin avril et le début mai. La croisière était une croisière sud nord (Bergen - Kirkenes). La température permettait de tenir sur le pont du bateau par beau temps et au soleil jusqu'à Tromso. C'est à partir de Tromso d'ailleurs que la neige a commencé s'étendre jusqu'à la mer.

Notre voyage était un voyage à la carte et nous avons pris trois bateaux. L'un de Bergen à Alesund. Le suivant de Alesund à Svolsvaer aux Lofoten. Et le dernier, de Harstad aux Lofoten jusqu'à Kirkenes. Cela nous a permis de passer une nuit dans le Geiranger fjord, et trois nuits aux Lofoten.

Concernant l'escale à Geiranger, que nous avons préféré à la traversé du fjord à bord du bateau, elle permet d'avoir une vue du fjord par le haut. Avec plus de temps, nous aurions sans doute aimé le parcourir a flots. Nous avons passé la nuité en cabane au Geirangerfjorden Feriesenter, très agréable et au pied du fjord. Attention toutefois, à cette époque, tout les commerces ou presque étaient fermés à l'exception d'une supérette en semaine et d'un bar... dépaysement assuré. A cette époque, la route des trolls (Trollstigen) était fermée, dommage car nous avions choisi de faire une escale en partie pour cela.

Mettre pied à terre aux Lofoten quelques jours en milieu de croisière est tout simplement indispensable. Il faut compter deux journées complètes pour faire le tour des lieux les plus intéressants : village de Reine, village de Nusfjord, port de Svolsvaer, plages, mur des Lofoten, ... En été, quelques jours de plus devraient permettre de profiter de belles randonnées à l'intérieur des terres et sur les hauteurs. A partir de fin avril, il ne fait déjà plus nuit noir aux Lofoten.

En ce qui concerne les escales et excursions Hurtigruten, nous n'en avons fait aucune. Toutes les grandes villes se font sans problème à pied : Alesund, Trondheim, Tromso, Bodo, ... A chaque fois les points d'intérêt sont accessibles à pied et il y a de quoi trouver facilement à manger, fast food, à emporter ou restaurants. Les fast food et la vente à emporter permettent de se restaurer une fois retourné au bateau sans dépenser le prix (excessif) des repas à bord. A la vue de la carte et des menus des différents repas, seul le buffet du dîner du cap nord vaut le coup : crabe royal, langouste, poissons fumées, ... Pour les autres, cela ne vaut pas le coup de les réserver à l'avance pour les raisons suivantes :
  1. Leur prix est exorbitant
  2. Il est possible de se présenter sans réserver. Le premier soir, voyez s'il y a plusieurs services de prévus pour le dîner. Si c'est le cas, il y aura de la place pour vous le soir au dernier service. Le midi, en self service, il n'y a pas de place réservée, vous êtes donc quasiment sur de trouver un place à un moment ou un autre.
  3. Il existe un snack dans chaque bateau. C'est un peu le fast food du bateau. Beaucoup moins cher que le restaurant et proposant des pizza, des steak frites, des sandwich bien garnis... Ce n'est pas une alternative vantée par Hurtigruten (car moins bénéfique pour eux), mais c'est tellement plus souple et surtout beaucoup moins cher.
En quelques mots, nous regrettons peut-être juste de ne pas avoir vu le Geiranger fjord depuis la mer ni la Trollstigen depuis la route. Si c'était à refaire, nous partirions un peu plus tard afin que cette dernière soit ouverte. Nous resterions une journée supplémentaire afin de faire la traversée du fjord à bord du bateau. 

L'intérêt du nord du pays (Tromso et plus haut) par rapport au reste (Tromso et plus bas) en cette saison est discutable. Les dernières villes du trajet étaient tristes, grises et sans intérêts. L'excursion au cap nord ne justifie pas à elle seule de faire le dernier tronçon. Les alpes de Lyngen, immédiatement au nord de Tromso sont en revanche magnifiques.

Quelques photos...

Pour terminer, quelques photos de la norvège pendant les vacances de pâques.
De Bergen à Kirkenes avec Hurtigruten en passant par les Lofoten et le Geiranger Fjord.

IMG_0130.jpg
Sur les bateaux postes norvégiens

P1040147.jpg
Les maisons colorés, que l'on retrouve de Bergen à Kirkenes

IMG_0068.jpg
Alesund, posée sur la mer et porte d'entrée du Geirangerfjord

IMG_0050.jpg
Le village de Geiranger, au fond du Geirangerfjord

P1040232.jpg
L'express côtier, au fond du Geirangerfjord

IMG_0062.jpg
Le Geirangerfjord

IMG_0107.jpg
Trondheim, ses canaux...

IMG_0096-2.jpg
... et sa cathédrale

IMG_0094.jpg
bis

IMG_0137.jpg
Un phare, peu après avoir quitté Trondheim

P1040533.jpg
Aux Lofoten, les routes sont parfois en terre...

IMG_0342.jpg
...et débouchent sur des villages de pêcheurs

IMG_0269-2.jpg
Les mouettes et goélands sont chez eux sur terre, sur mer et dans les airs

IMG_0400.jpg
Tromso, dernière grande ville avant le nord

IMG_0402-Modifier.jpg
Les Alpes de Lyngen, au nord de Tromso

IMG_0425-Modifier.jpg
MS Nordstjernen, le plus vieux bateau de la flotte (1956)

IMG_0482.jpg
La pêche fait vivre une partie de la population, et les oiseaux

IMG_0423-2.jpg
MS Richard With en direction du sud

mercredi 23 mars 2011

L'Amazon Appstore démarre fort

Pour ceux qui serait passé à côté de l'info, Amazon a lancé hier sa place de marché d'applications Android sous l'appelation Amazon Appstore for Android. Aux côtés des Androidpit, Appbrain et autres pionniers qui tentent de s'adjuger une part du gâteau, Amazon à su partir à point en s'offrant l'exclusivité de la dernière version d'Angry birds : Angry birds RIO. L'Amazon Appstore pour Android à été l'application la plus téléchargée aux états unis et ce, sans même être présente sur l'Android market :


Applications les plus téléchargées aux états unis
le 22 mars 2010

Reste maintenant à savoir si cette place de marché saura satisfaire les développeur et attirer des utilisateurs et clients sur le long terme... Affaire à suivre.


Ces statistiques ont été agrégée au moyen de Google Analytics et de l'application Instant Uninstaller disponible sur l'Android market :

dimanche 20 mars 2011

Tests unitaires d'un application Jersey JAX-RS + JPA

Nous allons aborder dans ce billet la problématique des tests unitaires pour une application WEB JPA en générale, et une application WEB JAX-RS utilisant Jersey en particulier. L'application WEB à tester est une application Maven et les tests seront intégrés à la phase de tests du cycle de vie Maven.

L'objectif est de parvenir à automatiser le déploiement de l'application en s'appuyant sur une base HSQLDB in memory et un fichier de configuration persistence.xml spécifiques à la phase de tests. Il sera alors possible de construire une campagne de tests unitaires via l'utilisation de la librairie Jersey Client API.

Application à tester

Nous utiliserons Hibernate 3.6, MySQL et Jersey 1.4 :

    
        org.hibernate
        hibernate-entitymanager
        3.6.0.Final
    
    
        mysql
        mysql-connector-java
        5.1.6
    
    
        com.sun.jersey
        jersey-bundle
        1.4
    
    
        javax.servlet
        servlet-api
        2.5
        provided
    
L'application à tester consiste en un datastore REST permettant de stocker des couples (clé, valeur) en s'appuyant sur l'entitée JPA suivante :
package com.googlecode.avianey.jersey;

import javax.persistence.Entity;
import javax.persistence.Id;

/**
 * Simple bean for storing (key, value) pairs
 */
@Entity
public class JerseyJPABean {

    @Id
    private String key;
    private String value;
    
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }

}
La persistance des données est réalisée par une base de données relationnelle MySQL. Le fichier persistence.xml de paramétrage JPA de "production" est le suivant :
<persistence
    version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>com.googlecode.avianey.jersey.JerseyJPABean</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/prod" />
            <property name="hibernate.connection.username" value="mysql" />
            <property name="hibernate.connection.password" value="mysql" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="false" />
        </properties>
    </persistence-unit>
    
</persistence>
Les services REST sont exposés par Jersey à l'adresse "/" :
<?xml version="1.0" encoding="UTF-8"?>
<web-app 
    version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    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/web-app_2_5.xsd">

    <!-- SERVICES REST -->
    <servlet>
        <servlet-name>Jersey</servlet-name>
        <servlet-class>
            com.sun.jersey.spi.container.servlet.ServletContainer
        </servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>com.googlecode.avianey.jersey</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>Jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>

</web-app>
L'implémentation des services est un CRUD permettant de :
Stocker une valeur (PUT) :
Stocke une valeur à l'adresse correspondant à l'URL de la méthode HTTP PUT utilisée
Mettre à jour une valeur (POST) :
Met à jour la valeur stockée à l'adresse correspondant à l'URL de la méthode HTTP POST utilisée
Récupérer une valeur (GET) :
Récupère la valeur stockée à l'adresse correspondant à l'URL de la méthode HTTP GET utilisée
Supprimer une valeur (DELETE) :
Supprime la valeur à l'adresse correspondant à l'URL de la méthode HTTP DELETE utilisée
Les codes retours HTTP suivants sont utilisées afin de gérer les différentes erreurs et conflits rencontrés lors du traitement d'une requête :
200
Le traitement de la requête s'est correctement déroulé
400
La requête est refusée par le serveur : tentative de stockage d'une valeur indéfinie.
404
La donnée n'existe pas : pas de valeur stockée pour la clé demandée.
409
Conflit de données : création d'une valeur pour une clé déjà existante.
500
Erreur technique du serveur.
package com.googlecode.avianey.jersey;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import javax.persistence.Persistence;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;

@Path("/")
@Produces("application/json")
@Consumes("application/json")
public class JerseyJPAService {

    private static final EntityManagerFactory EMF =
            Persistence.createEntityManagerFactory("pu");

    /**
     * Add a new element
     * @param value
     */
    @PUT
    @Path("/{key: \\w+}")
    public void put(@PathParam("key") String key, String value) {
        if (value == null) {
            // HTTP 400 : Bad Request
            throw new WebApplicationException(400);
        }
        JerseyJPABean bean = new JerseyJPABean();
        bean.setKey(key);
        bean.setValue(value);
        EntityManager em = EMF.createEntityManager();
        try {
            em.getTransaction().begin();
            em.persist(bean);
            em.getTransaction().commit();
        } catch (PersistenceException e) {
            if (get(key) != null) {
                // HTTP 409 : Conflict
                throw new WebApplicationException(409);
            } else {
                // HTTP 500 : Internal Server Error
                throw new WebApplicationException(500);
            }
        } finally {
            if (em.getTransaction().isActive()) {
                try {
                    em.getTransaction().rollback();
                } catch (Exception e) {}
            }
            em.close();
        }
    }

    /**
     * Update an existing element
     * @param bean
     */
    @POST
    @Path("/{key: \\w+}")
    public void update(@PathParam("key") String key, String value) {
        if (value == null || value.trim().length() == 0) {
            // Delete existing stored value
            delete(key);
        } else {
            EntityManager em = EMF.createEntityManager();
            try {
                em.getTransaction().begin();
                JerseyJPABean bean = (JerseyJPABean) em.getReference(JerseyJPABean.class, key);
                bean.setValue(value);
                em.merge(bean);
                em.getTransaction().commit();
            } catch (EntityNotFoundException e) {
                // HTTP 404 : Not Found
                throw new WebApplicationException(404);
            } catch (PersistenceException e) {
                // HTTP 500 : Internal Server Error
                throw new WebApplicationException(500);
            } finally {
                if (em.getTransaction().isActive()) {
                    try {
                        em.getTransaction().rollback();
                    } catch (Exception e) {}
                }
                em.close();
            }
        }
    }
    
    /**
     * Retrieve a persisted element
     * @param key
     * @return
     */
    @GET
    @Path("/{key: \\w+}")
    public String get(@PathParam("key") String key) {
        EntityManager em = EMF.createEntityManager();
        try {
            JerseyJPABean bean = (JerseyJPABean) em.getReference(JerseyJPABean.class, key);
            return bean.getValue();
        } catch (EntityNotFoundException e) {
            // HTTP 404 : Not Found
            throw new WebApplicationException(404);
        } finally {
            em.close();
        }
    }
    
    /**
     * Delete a persisted element
     * @param key
     */
    @DELETE
    @Path("/{key: \\w+}")
    public void delete(@PathParam("key") String key) {
        EntityManager em = EMF.createEntityManager();
        try {
            em.getTransaction().begin();
            Query q = em.createQuery("DELETE JerseyJPABean WHERE key = :key");
            q.setParameter("key", key);
            if (q.executeUpdate() == 0) {
                // HTTP 404 : Not Found
                throw new WebApplicationException(404);
            }
            em.getTransaction().commit();
        } catch (Exception e) {
            // HTTP 500 : Internal Server Error
            throw new WebApplicationException(500);
        } finally {
            if (em.getTransaction().isActive()) {
                try {
                    em.getTransaction().rollback();
                } catch (Exception e) {}
            }
            em.close();
        }
    }

}

Configuration des tests

L'une des problématique principale concernant les tests unitaires d'une application WEB persistant ses données en base est de pouvoir disposer d'une base de données réservée aux tests qui permette d'accueillir des jeux de test divers et variés. Dans un contexte JPA, il convient de créer un fichier de configuration persistence.xml spécifique, stocké dans le répertoire src/test/resources du projet, et utilisant une base HSQLDB in memory :
<persistence
    version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <class>com.googlecode.avianey.jersey.JerseyJPABean</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:test" />
            <property name="hibernate.connection.username" value="sa" />
            <property name="hibernate.connection.password" value="" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
    
</persistence>
L'arborescence du projet Maven est la suivante :


Arborescence du projet Maven de test

Les librairies supplémentaires à ajouter au CLASSPATH de test contiennent notamment :
  • JUnit : pour monter les jeux de test et définir pour chaque test le résultat attendu
  • HSQLDB : pour disposer d'une base de données de test in memory
  • Jetty : pour déployer les sources à tester dans un serveur embarqué

    
        org.hibernate
        hibernate-entitymanager
        3.6.0.Final
    
    
        mysql
        mysql-connector-java
        5.1.6
    
    
        com.sun.jersey
        jersey-bundle
        1.4
    
    
        javax.servlet
        servlet-api
        2.5
        provided
    
    
        org.hsqldb
        hsqldb
        2.0.0
        test
    
    
        junit
        junit
        4.8.2
        jar
        test
    
    
        com.sun.jersey.jersey-test-framework
        jersey-test-framework-core
        1.4
        jar
        test
    
    
        org.mortbay.jetty
        jetty
        6.1.17
        jar
        test
        
            
                servlet-api
                org.mortbay.jetty
            
        
    
Lors de l'exécution de la phase de test par Maven, le CLASSPATH utilisé contient les librairies applicatives ainsi que les classes JAVA du répertoire src/main auquelles sont ajoutées les librairies de test (celles dont les scope est <test>) et les classes JAVA du répertoire src/test. Le CLASSPATH contient donc deux fichiers différents nommés tous deux META-INF/persistence.xml :
src/main/resources/META-INF/persistence.xml
Fichier de configuration JPA utilisé pour le packaging final
src/main/resources/META-INF/persistence.xml
Fichier de configuration JPA utilisé pour les tests unitaires
Pour s'assurer de l'utilisation du fichier de configuration de test lors de l'exécution de la phase de tests par Maven, il faut utiliser un ClassLoader personnalisé dont l'effet est de ne mettre en visibilité que le fichier META-INF/persistence.xml du répertoire src/test/resources :
public static class ClassLoaderProxy extends ClassLoader {

    public ClassLoaderProxy(final ClassLoader parent) {
        super(parent);
    }

    @Override
    public Enumeration<url> getResources(final String name) throws IOException {
        if (!"META-INF/persistence.xml".equals(name)) {
            return super.getResources(name);
        } else {
            System.out.println("Redirecting to specific persistence.xml");
            return new Enumeration<url>() {

                private URL persistenceUrl = (new File(
                        "src/test/resources/META-INF/persistence.xml"))
                        .toURI().toURL();

                @Override
                public boolean hasMoreElements() {
                    return persistenceUrl != null;
                }

                @Override
                public URL nextElement() {
                    final URL url = persistenceUrl;
                    System.out.println("Using custom persistence.xml : " + 
                            url.toString());
                    persistenceUrl = null;
                    return url;
                }

            };
        }
    }
}
Lors de la mise en place des tests dans la méthode setUp() du TestCase JUnit, la base HSQLDB in memory est démarrée à l'adresse indiquée dans le fichier persistence.xml de test. Une base vierge de toute donnée est ainsi disponible pour y déployer des jeux de tests et données spécifiques. Un serveur Jetty embedded est quant à lui lancé sur le port 8888 et configuré pour exposer notre application WEB sur le context "/" en utilisant le ClassLoader personnalisé définit précédemment :
@Override
protected void setUp() throws Exception {
    super.setUp();
    // Start HSQLDB
    try {
        Class.forName("org.hsqldb.jdbcDriver");
        con = DriverManager.getConnection(
                "jdbc:hsqldb:mem:test",
                "sa", "");
    } catch (Exception e) {
        e.printStackTrace();
        fail("Failed to start HSQLDB");
    }

    // Start Jetty
    try {
        System.setProperty("org.mortbay.log.class",
                "com.citizenmind.service.rest.TestRestService.CustomLogger");
        server = new Server(8888);
        WebAppContext context = new WebAppContext();
        context.setResourceBase("src/main/webapp");
        context.setContextPath("/");
        Thread.currentThread().setContextClassLoader(
                new ClassLoaderProxy(
                Thread.currentThread().getContextClassLoader()));
        context.setClassLoader(Thread.currentThread().getContextClassLoader());
        context.setParentLoaderPriority(true);
        server.setHandler(context);
        server.start();
    } catch (Exception e) {
        e.printStackTrace();
        server.stop();
        fail("Failed to start embedded Jetty");
    }
}
Lors de l'arrêt des tests dans la méthode tearDown() du TestCase JUnit, la base de données HSQLDB ainsi que le serveur Jetty embedded sont arrêtés :
@Override
protected void tearDown() throws Exception {
    super.tearDown();
    // Stop HSQLDB
    try {
        con.createStatement().execute("SHUTDOWN");
    } catch (Exception e) {
        e.printStackTrace();
    }
    // Stop jetty
    try {
        server.stop();
    } catch (Exception e) {
        e.printStackTrace();
        System.err.println("Jetty must be killed manually");
    }
}
La librairie Jersey Client API est utilisée pour dérouler les tests en interrogeant les services REST exposés par le serveur Jetty embedded :
/**
 * Test REST Services using Jersey client API
 */
public void testJerseyJPAService() {

    // Create a Jersey client
    Client c = Client.create();
    WebResource r;
    
    // Insert two values :
    // - (A, 1)
    // - (B, 2)
    try {
        r = c.resource(URL + "/A");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("1");
        r = c.resource(URL + "/B");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("2");
    } catch (UniformInterfaceException e) {
        e.printStackTrace();
        fail("Could not insert values");
    }

    // Store a value with an existing key
    try {
        r = c.resource(URL + "/B");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .put("2");
    } catch (UniformInterfaceException e) {
        // HTTP 409 : Conflict
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 409);
    }
    
    // Verify values
    r = c.resource(URL + "/A");
    String A = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("1".equals(A));
    r = c.resource(URL + "/B");
    String B = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("2".equals(B));
    
    // Update B
    r = c.resource(URL + "/B");
    r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .post("3");
    B = r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .get(String.class);
    assertTrue("3".equals(B));
    
    // Update C
    try {
        r = c.resource(URL + "/C");
        r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .post("3");
    } catch (UniformInterfaceException e) {
        // HTTP 404 : Resource C does not exists
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 404);
    }
    
    // Delete A
    r = c.resource(URL + "/A");
    r.type(MediaType.APPLICATION_JSON_TYPE)
        .accept(MediaType.APPLICATION_JSON_TYPE)
        .delete();
    try {
        Statement st = con.createStatement();
        ResultSet rs = st.executeQuery(
                "Select * From JerseyJPABean Where key = 'A'");
        assertTrue(!rs.next());
        rs.close();
        st.close();
    } catch (SQLException e) {
        e.printStackTrace();
        fail("Could not execute SQL Select");
    }
    
    // Get A
    try {
        A = r.type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON_TYPE)
            .get(String.class);
    } catch (UniformInterfaceException e) {
        // HTTP 404 : Resource A does not exists enymore
        assertTrue(e.getResponse()
                .getClientResponseStatus()
                .getStatusCode() == 404);
    }
}

Résultat final

Le projet complet Maven utilisé pour illustrer ce billet est disponible sous licence Apache License 2.0 à cette adresse :

Un petit test :

mvn test

Et l'on remarque le déploiement des services sous Jetty, la redirection du ClassLoader personnalisé ainsi que le détail des requêtes Hibernate :

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.googlecode.avianey.jersey.JerseyJPATest
2011-03-20 17:10:18.427::INFO: Logging to STDERR via org.mortbay.log.StdErrLog
2011-03-20 17:10:18.463::INFO: jetty-6.1.17
2011-03-20 17:10:18.531::INFO: NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
20 mars 2011 17:10:18 com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Scanning for root resource and provider classes in the packages:
com.googlecode.avianey.jersey
20 mars 2011 17:10:18 com.sun.jersey.api.core.ScanningResourceConfig logClasses
INFO: Root resource classes found:
class com.googlecode.avianey.jersey.JerseyJPAService
20 mars 2011 17:10:18 com.sun.jersey.api.core.ScanningResourceConfig init
INFO: No provider classes found.
20 mars 2011 17:10:18 com.sun.jersey.server.impl.application.WebApplicationImpl _initiate
INFO: Initiating Jersey application, version 'Jersey: 1.4 09/11/2010 10:41 PM'
2011-03-20 17:10:19.323::INFO: Started SocketConnector@0.0.0.0:8888
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Redirecting to specific persistence.xml
Using custom persistence.xml : file:/C:/projets/jersey-jpa-test/src/test/resources/META-INF/persistence.xml

Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: insert into JerseyJPABean (value, key) values (?, ?)
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: update JerseyJPABean set value=? where key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Hibernate: delete from JerseyJPABean where key=?
Hibernate: select jerseyjpab0_.key as key0_0_, jerseyjpab0_.value as value0_0_ from JerseyJPABean jerseyjpab0_ where jerseyjpab0_.key=?
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.343 sec

dimanche 27 février 2011

jRename : renommer des fichiers par lot

Cela fait quelques années que j'avais développé un outil bien pratique pour renommer des fichiers en série. Après avoir retrouvé les sources de cette application JAVA sur un disque de sauvegarde, je le propose aujourd'hui en licence open source GPL v3.

jRename (puisque c'est ainsi que je l'avais appelé à l'époque) est un utilitaire java gratuit et open source permettant de renommer des fichiers par lot. Il est multi-plateformes : Windows XP/Vista/7, Mac OS X et Linux et propose un grand nombre d'options : changement de case des caractères, insertion de dates, utilisation d'un compteur mais également filtrage et capture à base d'expressions régulières :




Parmi les fonctionnalités implémentées :
  • Utilisation d'un compteur
  • Changement de casse
  • Modification de caractères
  • Suppression de blancs superflus
  • Suppression de '.' superflus
  • Utilisation d'expressions régulières avec groupes capturants
  • Prévisualisation du résultat
  • Undo
  • Travail dans les répertoires et les sous répertoires

Ce qu'il reste à faire :
  • Utilisation de swing worker
  • Utilisation des données EXIF
  • Utilisation des données ID3 tag
  • Préciser l'ordre de traitement des fichiers
  • etc...

La notice d'utilisation en français se trouve à cette adresse : notice d'utilisation jrename.

D'un point de vue technique, jRename utilise le gestionnaire de Layout MiG Layout que je vous recommande pour sa simplicité et ses possibilités avancées de configuration. Il est disponible pour SWING, SWT et JavaFX.

Si vous avez des idées pour améliorer cette application, n'hésitez pas à m'en faire part.

Fork me on GitHub