ViaXoft

Aller au contenu | Aller au menu | Aller à la recherche

vendredi 20 février 2009

Comparaison de chaines grâce à un Collator

Lors de la manipulation de données, il est souvent nécessaire de comparer des chaines de caractère, pour réaliser des tris par exemple. Cependant, plusieurs problèmes peuvent se poser, comme la différence de casse et la présence de lettres diacritées (possédant un accent ou une cédille, par exemple).

Si pour les problèmes de casse, il est possible de s'en sortir avec les méthodes equalsIgnoreCase() et compareToIgnoreCase() de la classe String, il n'en va pas de même pour les problèmes de diacritiques. En effet, non seulement il est très difficile de prévoir tous les cas, mais en plus ce problème est dépendant de la langue utilisée (alors qu'il y a moins de 20 mots comportant des diacritiques en anglais, la quasi-totalité des mots vietnamiens en comportent).

Heureusement, Java propose un outil nommé Collator (du verbe anglais "to collate", ordonner, ranger, vérifier l'ordre) permettant de gérer ce problème de façon simple. En effet, le Collator permet de faire une comparaison intelligente en tenant compte de la locale, mais aussi des diacritiques et de la casse.

Voici un petit exemple pour illustrer son fonctionnement :

package com.viaxoft.test.collator;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

public class TestCollator {
  public static void main (String [] args) {
    final List pays = new ArrayList ();
    pays.add("zimbabwe");
    pays.add("angola");
    pays.add("Australie");
    pays.add("Zambie");
    pays.add("éthiopie");
    pays.add("États-Unis");
    pays.add("Égypte");
    pays.add("Sénégal");
    pays.add("Serbie");

    Collections.sort(pays);
    printTitle("Tri par défaut");
    printList(pays);

    Collections.sort(pays, new Comparator () {
      public int compare(String o1, String o2) {
        return o1.compareToIgnoreCase(o2);
      }
    });
    printTitle("Tri en ignorant la casse");
    printList(pays);

    final Collator frCollator = Collator.getInstance(Locale.FRENCH);
    Collections.sort(pays, new Comparator () {
      public int compare(String o1, String o2) {
        return frCollator.compare(o1, o2);
      }
    });
    printTitle("Tri avec un Collator");
    printList(pays);

    frCollator.setStrength(Collator.TERTIARY);
    printTitle("Comparaison avec un Collator \"TERTIARY\"");
    compareWithCollator ("egypte", "egypte", frCollator);
    compareWithCollator ("Egypte", "egypte", frCollator);
    compareWithCollator ("égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "Sénégal", frCollator);

    frCollator.setStrength(Collator.SECONDARY);
    printTitle("Comparaison avec un Collator \"SECONDARY\"");
    compareWithCollator ("egypte", "egypte", frCollator);
    compareWithCollator ("Egypte", "egypte", frCollator);
    compareWithCollator ("égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "Sénégal", frCollator);

    frCollator.setStrength(Collator.PRIMARY);
    printTitle("Comparaison avec un Collator \"PRIMARY\"");
    compareWithCollator ("egypte", "egypte", frCollator);
    compareWithCollator ("Egypte", "egypte", frCollator);
    compareWithCollator ("égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "egypte", frCollator);
    compareWithCollator ("Égypte", "Sénégal", frCollator);
  }

  private static void printTitle (final String title) {
    System.out.println("");
    System.out.println(title);
    System.out.println(title.replaceAll(".", "-"));
  }

  private static void printList (final List list) {
    int i=1;
    for (String str : list) {
      System.out.println(i+". "+str);
      i++;
    }
  }

  private static void compareWithCollator (final String str1, final String str2, final Collator collator) {
    if (collator.equals(str1, str2))
      System.out.println(str1+"=="+str2);
    else
      System.out.println(str1+"!="+str2);
  }
}


Et le résultat de l'exécution de ce programme :

Tri par défaut

  1. Australie
  2. Serbie
  3. Sénégal
  4. Zambie
  5. angola
  6. zimbabwe
  7. Égypte
  8. États-Unis
  9. éthiopie


Tri en ignorant la casse

  1. angola
  2. Australie
  3. Serbie
  4. Sénégal
  5. Zambie
  6. zimbabwe
  7. Égypte
  8. États-Unis
  9. éthiopie


Tri avec un Collator

  1. angola
  2. Australie
  3. Égypte
  4. États-Unis
  5. éthiopie
  6. Sénégal
  7. Serbie
  8. Zambie
  9. zimbabwe


Comparaison avec un Collator "TERTIARY"
egypte==egypte
Egypte!=egypte
égypte!=egypte
Égypte!=egypte
Égypte!=Sénégal

Comparaison avec un Collator "SECONDARY"
egypte==egypte
Egypte==egypte
égypte!=egypte
Égypte!=egypte
Égypte!=Sénégal

Comparaison avec un Collator "PRIMARY"
egypte==egypte
Egypte==egypte
égypte==egypte
Égypte==egypte
Égypte!=Sénégal

Comme on s'y attendait, la liste des pays n'est pas triée correctement avec les comparateurs de base de la classe String, mais l'est avec le Collator.
Cet exemple montre également l'influence de la propriété "strength" (force, puissance en anglais) sur le test d'égalité du collator : en "TERTIARY", on tient compte de la casse et des diacritiques, en "SECONDARY" seulement des diacritiques, et en "PRIMARY" ni de l'un ni de l'autre. Il faudra donc également prendre en compte ce paramètre lors de l'utilisation d'un Collator, et le choisir en fonction du besoin.

mercredi 21 janvier 2009

GWT et Drag and Drop

Le « glisser-déposer », ou Drag and Drop, n’est pas géré nativement par GWT. Afin de palier à ce manque, un certains nombres d'implémentations ont été élaborées.
Le choix de Viaxoft s’est porté sur dnd-gwt, et ceux pour deux raisons :

  • Cette API présente l’avantage d’être non invasive, de n’impliquer aucune génération de code annexe.
  • « dnd » permet la réutilisation directe des widgets de GWT et de nos composites.


Cette note survole les grands principes autours desquels s’articule cette librairie.

AbsolutePanel containingPanel = new AbsolutePanel();
containingPanel.setPixelSize(w, h);
containingPanel.getElement().getStyle().setProperty("position","relative");
PickupDragController dragController = new PickupDragController(containingPanel, true);

//Paramétrage du comportement DnD
//On interdit les selections mulitples et aux Widgets draggables d'être déposés hors des drop zones
dragController.setBehaviorBoundaryPanelDrop(false);
dragController.setBehaviorMultipleSelection(false);


Une fois un DragController défini, n’importe quel widget implémentant SourcesMouseEvents peut être transformé en élément « draggable ». On pourrait donc penser être limité aux Image, Label et FocusPanel. Cependant tout composite implémentant l’interface est lui-même éligible. Le DragController permet notamment de distinguer un composite dans son ensemble de sa partie réalisant le Drag & Drop.

class TextAndPicto extends Composite implements SourcesMouseEvents{
  TextField text;
  Image picto;
  public LabelAndPicto(TextField text, Image picto)
  {
   this.text = text;
   this.picto=picto;
   HorizontalPanel mainPanel = new HorizontalPanel();
   mainPanel.add(picto);
    mainPanel.add(text);
   initWidget(mainPanel);
  }

  public void addMouseListener(MouseListener listener) {
   picto.addMouseListener(listener);

  }

  public void removeMouseListener(MouseListener listener) {
   picto.removeMouseListener(listener);
...

   TextAndPicto w = new TextAndPicto(new TextBox(), new Image("pictoViaxeo/clients.gif"));
   dragController.makeDraggable(w,w.getImage());


Enfin le DropController permet de définir le comportement des widgets sur lesquels pourront être déposés les éléments « draggables ». Le constructeur prend en paramètre le widget qui servira de « drop zone », et les 4 méthodes suivantes sont définies par l’interface afin de gérer son comportement : onEnter, onLeave, onDrop et onPreviewDrop. Les deux premières sont appelées lorsqu’un item est « dragué » au dessus de la drop zone. La troisième détermine les actions à prendre consécutivement à la dépose du dit item. Et enfin, onPreviewDrop permet d’effectuer des contrôles préalables avant l’exécution d’onDrop.

 public class DropZoneListener extends SimpleDropController {
  private VerticalPanel panel;

  public DropZoneListener(Widget dropTarget) {
   super(dropTarget);
   panel = (VerticalPanel)dropTarget;
  }

  public void onDrop(DragContext context) {
   super.onDrop(context);
   for(SmartWidget smartWidget : (List)context.selectedWidgets)
    panel.add(smartWidget);
   }

  public void onEnter(DragContext context) {
   super.onEnter(context);
   panel.addStyleName("viaxeo-DnD-Liste-Header-seleted");
   }

  public void onLeave(DragContext context) {
   super.onLeave(context);
   panel.removeStyleName("viaxeo-DnD-Liste-Header-seleted");
   }

  public void onPreviewDrop(DragContext context) throws VetoDragException {
   super.onPreviewDrop(context);
   }

}


Dnd-gwt propose un large panel de DropController allant du GridConstrainedDropController (gestion du drop par grilles) au FlexTableRowDropController (ajout/suppression dynamique de lignes dans une FlexTable).

vendredi 9 janvier 2009

GWT et éditeur de texte riche : impossible ?

Au cours du développement du produit, nous avons été confronté à un problème imprévu : l'intégration d'un éditeur de texte riche au sein d'une application écrite en GWT.
En effet, nous désirions laisser la possibilité à l'utilisateur de créer ou modifier une page web en mode WYSIWYG directement depuis l'application, mais l'éditeur proposé par GWT ne nous paraissait pas suffisant.

Après une rapide analyse des solutions existantes, nous avons tout d'abord décidé d'utiliser FCKeditor, celui-ci semblant offrir le plus grand nombre de fonctionnalités sur le marché à l'heure actuelle.
Nous sommes pour cela repartis de gwt-html-editor que nous avons adaptée à nos besoins. Cependant, si cette solution fonctionnait parfaitement lors de nos tests en hosted mode, dès le premier test de l'application déployée, nous avons rencontré des problèmes :

  • comportements différents selon les navigateurs
  • problèmes lors du premier chargement du composant
  • problèmes lors du rechargement du composant
  • ...


Ces problèmes étant impossibles à résoudre facilement, nous avons décidé de tester les solutions concurrentes : Xinha, TinyMCE et Yahoo! editor.
Mais celles-ci se sont avérées souffrir des même problèmes que FCKeditor. En effet, ces éditeurs ont été conçus pour fonctionner au sein d'une application web "classique", et ne sont pas adaptés pour une application entièrement en AJAX, comme celles réalisées en GWT.

Suite à ce constat, nous avons choisi de privilégier une solution hybride : la visualisation du texte riche se fait dans une iframe au sein de l'application, mais son édition se fait dans une page web classique présentée en popup.

Le composant s'articule autour de trois fichiers :

  • FCKRichEditorImpl.java, le composant en lui-même
  • iframeFckEditor.html, la page web affichant le rendu au sein d'une iframe (l'iframe permet d'avoir un style propre à la page affichée, ce que ne permettait pas l'utilisation d'un setInnerHTML (), par exemple)
  • pageFckEditor.html, la page en popup permettant d'éditer le texte grâce à FCKeditor


Cette solution fonctionne parfaitement et ne souffre pas des problèmes cités précédemment, mais elle en apporte cependant deux nouveaux :

  • moins bonne intégration de l'édition de texte avec le reste de l'application
  • possibilité de problème avec les bloqueurs de popup


C'est pourquoi nous restons à l'écoute du marché, et surveillons avec attention les nouvelles solutions disponibles.

Si vous désirez le code de notre solution n'hésitez pas à nous contacter

jeudi 4 décembre 2008

Utilisation de Dozer pour le mapping Bean à Bean

Nous utilisons Dozer au sein de notre application lorsque nous avons besoin de faire du mapping Bean à Bean et notamment lors de l'appel de services web publiés par nos partenaires.

Dozer offre des fonctionnalités assez poussées comme la possibilité de surcharger les setter/getter ou les méthodes de création de Bean.

L’une des fonctionnalités indispensables, mais pourtant assez mal documentée, lors du mapping avec des classes générées par XMLBean ou JAXB2, est la possibilité de mapper les inner class :

<mapping>
  <class-a>com.viaxoft.Availability$Response</class-a>
  ...

Le $ remplace le point dans le chemin vers l’inner class.

mercredi 23 juillet 2008

Modification de template Velocity au runtime

Velocity est un moteur de template Open Source Java de la fondation Apache. Nous utilisons Velocity depuis pas mal de temps pour tout ce qui est rapport aux fusions (Mail, publipostage,...).
Jusqu'à présent nous l'utilisions sous sa forme la plus "traditionnelle", basée sur des fichier template (.vm), que nous chargions au runtime pour être fusionnés à une map de paramètres :

  // Initialisation du moteur velocity
  VelocityEngine engine = new VelocityEngine();
  engine.init();
  // Chargement du template
  Template template = engine.getTemplate( "test.vm" );
  // Création du contexte
   VelocityContext context = new VelocityContext();
  context.put("nom", "Durand");
  // Exécution du template dans une String
  StringWriter writer = new StringWriter();
  template.merge( context, writer );

  System.out.println( writer.toString() );

Dernièrement nous avons eu le besoin de composer une variable sur la base de plusieurs autres. Il s'agit de faire un peu plus que de la concaténation ; nous avons donc laissé nos utilisateurs créer leur propres variables de la sorte :

  politesse = &civilite &nom &prenom

Mais nous devions à ce moment là modifier nos template au runtime. Velocity offre pour cela une méthode evaluate qui permet d'évaluer un template (sous forme de String) :

  VelocityContext velocityContext = new VelocityContext(params);
  StringWriter result = new StringWriter();
  try {
    velocityEngine.evaluate(velocityContext, result, "template", template);
  } catch (Exception e)...

Ceci nous a permis de stocker les modèles en base de données sans avoir ensuite à recréer des InputStream ou autre pour les fusionner.

Nous modifions les modèles au runtime en leur ajoutant les variables créées par les utilisateurs.

vendredi 20 juin 2008

GWT, Tomcat, Eclipse et Cypal Studio

GWT offre un mode de de développement "embarqué" (le hosted mode) qui permet de faire tourner l'application au sein de la machine virtuelle Java et ainsi garder tous les avantages d'un IDE tel qu'Eclipse et notamment le debugger.

Mais pour l'intégration de module plus avancés tels que la sécurité avec Acegi, il est primordial de dé ployer l'application sur un tomcat externe afin de configurer au mieux ce dernier. Voici un lien vers un très bon post, détaillant au sein d'Eclipse la création d'un projet GWT en s'appuyant sur le plugin Cypal. Ceux qui ont déjà galéré sur ce type de déploiement apprécieront :
Creating your first GWT app with Cypal Studio for GWT.
Merci à Prakash pour ce billet.

L'intégration d'Acegi au trio GWT, Spring, Hibernate fera l'objet d'un prochain post.

vendredi 6 juin 2008

Gestion des logs

La gestion des logs est un sujet qui est souvent sous estimé de la part des équipes de développement. Avec l'utilisation des outils de développement moderne et leur débugger intégré, les logs applicatives ont bien moins d'intérêts au moment du développement. Par contre, lorsque l'application est testée ou déployée, les logs reprennent tout leur sens.

L'apparition de framework Java tels que Log4J ou celui intégré au JDK facilite grandement la vie du développeur, mais l'écriture de logs reste toujours à la discrétion de ce dernier.

L'AOP (Programmation Orientée Aspect) apporte une réponse à cette problématique. Nous pouvons grâce à celle-ci générer des logs à chaque entrée/sortie de méthode et à chaque levée d'exception. Voici un exemple d'implémentation de gestion des logs utilisant Spring et Spring AOP :

Nous définissons tout d'abord un greffon (advice) qui implémente MethodBeforeAdvice, AfterReturningAdvice et ThrowsAdvice. Ceci nous permet de redéfinir les trois méthodes before, afterReturning et afterThrowing afin de générer les logs à chaque entrée/sortie de méthode et à chaque levée d'exception.

package com.viaxoft.ext.logging;

import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;

public class LoggingAdvice implements MethodBeforeAdvice, AfterReturningAdvice,
ThrowsAdvice {
private static Log logger = null;

public LoggingAdvice() {
}

public void before(Method arg0, Object[] arg1, Object arg2)
throws Throwable {
logger = LogFactory.getLog(arg2.getClass());
logger.info("Entrée dans la méthode : " + arg0.getName());
}

public void afterReturning(Object arg0, Method arg1, Object[] arg2,
Object arg3) throws Throwable {
logger = LogFactory.getLog(arg3.getClass());
logger.info("Sortie de la méthode : " + arg1.getName());
}

public void afterThrowing(Method m, Object[] args, Object target,
Throwable ex) {
logger = LogFactory.getLog(target.getClass());
logger.error("Exceptiondans la méthode : " + m.getName() + " L'exception : "
+ ex.getMessage());
}
}

Nous devons ensuite définir notre aspect sous forme de bean au sein du fichier de config Spring. Nous utilisons l'implémentation RegexpMethodPointcutAdvisor qui nous permet de matcher les méthodes sur lesquelles notre aspects doit s'appliquer grâce à une expression régulière :

<bean id="loggingAdvisor"   class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
  <property name="advice" ref="loggingAdvice" />
  <property name="patterns" value=".*" />
</bean>

A noter ici que nous générons la log pour toutes les méthodes. On pourrait imaginer générer plusieurs niveau de log (DEBUG, INFO, ...), en fonction de la méthode à laquelle on s'adresse ; par exemple les méthodes du layer DAO seraient en DEBUG, alors que celles de la couche service en INFO.

L'implémentation RegexpMethodPointcutAdvisor permet de définir à la fois l'aspect et la coupe au sens AOP.

Nous définissons également le greffon :

<bean id="loggingAdvice"
  class="com.viaxoft.ext.logging.LoggingAdvice">
</bean>

Il ne nous reste plus ensuite qu'à tisser les aspects. Nous utilisons DefaultAdvisorAutoProxyCreator pour un tissage automatique :

<bean   class="org.springframework.aop.framework.autoproxy.
 DefaultAdvisorAutoProxyCreator" />

Le résultat dans le fichier de log :

juin 06 08:54:25 INFO iaxoft.dao.impl.AddressDaoImpl before Entrée dans la méthode: saveAddress
juin 06 08:54:25 INFO ernate.impl.SessionFactoryImpl before Entrée dans la méthode: openSession
juin 06 08:54:25 INFO ernate.impl.SessionFactoryImpl afterReturning Sortie de la méthode : openSession
juin 06 08:54:25 INFO ernate.impl.SessionFactoryImpl before Entrée dans la méthode: getTransactionManager
juin 06 08:54:25 INFO ernate.impl.SessionFactoryImpl afterReturning Sortie de la méthode : getTransactionManager
juin 06 08:54:25 INFO iaxoft.dao.impl.AddressDaoImpl afterReturning Sortie de la méthode : saveAddress

page 2 de 2 -