ViaXoft

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

samedi 8 mai 2010

Sélection de colonnes dans Hibernate

Les outils d'ORM (Hibernate, Ibatis, ...) sont aujourd'hui à maturité et ne souffrent plus des problèmes de performance des premières versions. Le code SQL généré, ainsi que la transformation en objets, ont été très largement optimisés.

Cependant selon la taille de la grappe d'objet remontée, il peut subsister des problèmes de performance, liés au nombre et à la complexité des objets à créer. Le cas d'utilisation typique sur une application de gestion est l'affichage d'une liste d'objet complexe sur un écran de recherche.

Prenons le cas par exemple d'une liste de commande (que nous simplifions volontairement) : 

public Class Commande {

  private Destination destination;

  private Set<LigneCommande> lignes;

  private Client client;

  ...

}

Une requête Criteria qui permet de remonter la liste des commandes ressemble à ceci :

DetachedCriteria criteria = DetachedCriteria.forClass(Commande.class,"commande")

.createAlias("commande.Destination", "destination", CriteriaSpecification.LEFT_JOIN)

.createAlias("commande.Lignes", "lignes", CriteriaSpecification.LEFT_JOIN)

.createAlias("commande.Client", "client", CriteriaSpecification.INNER_JOIN);

List<Commande> listCommande = getHibernateTemplate().findByCriteria(criteria);

Cette requête remonte donc tous les objets Commande (toutes les colonnes de la table correspondante), ainsi que les objets liés (et donc toutes les colonnes associées). Dans le cas où la grappe d'objets est importante et composée de nombreux objets, le nombre de lignes remontées devient très important et la constitution des objets très couteuse. Rappelons que nous souhaitons simplement afficher une liste avec seulement quelques colonnes... au moins 95% des données remontées ne nous servent donc à rien.

Depuis la version 3 d'Hibernate a été introduite la notion de Projection. Celle-ci va nous permettre non seulement de remonter seulement les colonnes qui nous intéressent mais également une liste d'objets non gérés par Hibernate ; par exemple nous allons pouvoir remonter directement une liste de DTO (data transfert object) en se basant sur le même objet criteria (forClass(Commande.class,"commande")) :

On définit le DTO :

public class DealListDto implements Serializable  {
    private static final long serialVersionUID = 1L;

    private java.lang.String id;
    private String ref;
    private Date date;
    private String nom;
    private String prenom;
    private String destNom;
    ...
}

puis les propriétés que l'on souhaite remonter, ainsi que le bean avec lequel on veut les mapper :

criteria.setProjection(Projections.projectionList()

.add(Property.forName("commande.Id"), "id")

.add(Property.forName("commande.Ref"), "ref")

.add(Property.forName("commande.Date"), "date")

.add(Property.forName("client.Nom"), "nom")

.add(Property.forName("client.Prenom"), "prenom")

.add(Property.forName("destination.nom"), "destNom")

.setResultTransformer( Transformers.aliasToBean(DealListDto.class));

List<DealListDto> listCommande = getHibernateTemplate().findByCriteria(criteria);

Nous obtenons donc une liste de DealListDto, beaucoup plus légers, que nous pouvons  faire remonter directement sur la couche client.

dimanche 27 septembre 2009

Mise en place de datasources en hosted mode avec GWT 1.6


La migration de GWT de la version 1.5 à la version 1.6 s'est accompagnée de changements en profondeur au niveau du hosted mode. En effet, celui-ci utilisait précédemment une version embarquée du serveur tomcat, qui a été remplacée dans la dernière version par jetty.

Ce changement a donc entraîné une restructuration au niveau de la configuration des projets et notamment de la déclaration des datasources. Cet article va donc présenter la configuration des datasources en hosted mode avec GWT 1.6.

La première étape est de rajouter les jar jetty-naming-6.1.11.jar et jetty-plus-6.1.11.jar à notre projet. Ceux-ci permettront d'utiliser notre propre fichier de configuration de jetty. Si comme nous vous utilisez Maven pour gérer les dépendances de votre projet, rajoutez les deux entrées suivants à votre pom.xml :


<dependency>
   <groupid>org.mortbay.jetty</groupid>
   <artifactid>jetty-naming</artifactid>
   <version>6.1.11</version>
</dependency>
<dependency>
   <groupid>org.mortbay.jetty</groupid>
   <artifactid>jetty-plus</artifactid>
   <version>6.1.11</version>
</dependency>

Sinon, vous trouverez ces jars sur le site de jetty.
Il faut maintenant crée un fichier nommé jetty-web.xml qui contiendra la déclaration de nos datasources sous la forme suivante :


<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.mortbay.org/configure.dtd">
<Configure class="org.mortbay.jetty.webapp.WebAppContext">
  <New id="dataset1" class="org.mortbay.jetty.plus.naming.Resource">
    <Arg>java:comp/env/jdbc/dataset1</Arg> <Arg>
      <New class="com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource">
        <Set name="Url">jdbc:mysql://localhost:3306/database1?autoReconnect=true</Set>
        <Set name="User">username1</Set>
        <Set name="Password">password1</Set>
      </New>
    </Arg>
  </New>
  <New id="dataset2" class="org.mortbay.jetty.plus.naming.Resource">
    <Arg>java:comp/env/jdbc/dataset2</Arg>
    <Arg>
      <New class="com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource">
        <Set name="Url">jdbc:mysql://localhost:3306/database2?autoReconnect=true</Set>
        <Set name="User">username2</Set>
        <Set name="Password">password2</Set>
      </New>
    </Arg>
  </New>
</Configure>

C'est cette configuration qui va indiquer à quelle base de donnée correspond chacun des datasets, et les paramètres de connexion. Dans cette exemple, nous déclarons deux datasets qui utilisent les bases de donnée MySql database1 et database2 du serveur local.
Il faut ensuite référencer les datasources dans le web.xml pour que notre application soit en mesure de les utiliser :


<resource-ref>
  <res-ref-name>java:comp/env/jdbc/dataset1</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>
<resource-ref>
  <res-ref-name>java:comp/env/jdbc/dataset2</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

Une fois cette étape réalisée, vous pouvez utiliser vos datasets au sein de votre application comme vous le feriez normalement :


Context ctx = new InitialContext();
DataSource source = (DataSource)ctx.lookup("java:comp/env/jdbc/dataset2");
Connection connection = source.getConnection();
//...

jeudi 16 juillet 2009

Cache applicatif avec Spring et ehcache

La recherche de performances applicatives passe souvent par la mise en place de caches applicatifs.
Ces caches permettent de maintenir en mémoire les données souvent requêtées par les utilisateurs mais peu mises à jour (paramétrages, villes, ...).
Spring offre la mise en place d'un tel cache assez facilement par l'intégration d'un framework tel que ehcache.

Dans le fichier configuration de Spring on ajoute les lignes suivantes :

<ehcache:config failQuietly="false" />
<ehcache:annotations>
              <ehcache:caching id="test" cacheName="testCache" />
              <ehcache:flushing id="test" cacheNames="testCache" when="after" />
</ehcache:annotations>

Le cacheName et ce que l’on va retrouver dans le fichier de configuration de ehcache,
L’id et ce que l’on va retrouver dans l’annotation,
caching l’id est utilisé pour la mise en cache,
flushing l’id est utilisé pour la libération du cache.

Dans le fichier configuration de ehcache.xml, on retrouve la définition du cache : nombre d’objet en cache, écriture sur disque, politique d’éviction,…

<cache name="testCache" maxElementsInMemory="1000" overflowToDisk="true" eternal="true" memoryStoreEvictionPolicy="LFU"/>



On ajoute les annotations suivantes dans le source Java afin de les mettre en cache après le premier appel :

@CacheFlush(modelId = "test")

    public String saveObject(Object objet) {…}

 

@Cacheable(modelId = "test")

    public User findObject(String idObject) {…}

mardi 24 mars 2009

Multi instances Tomcat

Nous utilisons Tomcat comme moteur de Servlet. L'installation d'un Tomcat standard est assez simple, mais on en atteind très vite les limites.
En effet, selon l'environnement, on va avoir besoin de plusieurs instance de Tomcat ; par exemple, pour toute la phase en amont de la pré-prod / prod, on souhaite gérer des environnements d'intégration, de recettte, de montée en charge, ...

La première solution est de multiplier les installations de Tomcat. C'est très simple à mettre en place , mais lorsque l'on change de version du serveur, on doti modifier toutes les installations.

Une autre solution assez simple apportée par Tomcat, est la possibilité de définir plusieurs instances de Tomcat pour une même installation. Nous allons décrire dans ce billet comment le mettre en place :

  • La première chose à faire est de définir un scipt de démarrage/arrêt par instance de Tomcat.

    • On crée un fichier $TOMCAT_HOME/bin/startupMonInstance.sh qui sera identique au startup.sh auquel on ajoute :

      export CATALINA_BASE=….. /monInstance/

      (le chemin peut être complètement indépendant de l’arborescence Tomcat)
      On peut également modifier les paramètres de la JVM de chaque instance en ajoutant dans ce script la variable CATALINA_OPTS.

    • Et un fichier $TOMCAT_HOME/bin/shutdownMonInstance.sh qui sera identique au shutdown.sh auquel on ajoute :

      export CATALINA_BASE=/……./monInstance/

  • On copie les fichiers de configuration $TOMCAT_HOME/conf/*.* dans le répertoire monInstance/conf

  • On modifie les ports du fichier monInstance/conf /server.xml pour pouvoir lancer les différents tomcats en même temps.

Au démarrage de chaque instance, Tomcat se charge de créer les répertoires manquants, notamment le webapps.

dimanche 8 mars 2009

Lire un fichier dont on ne connaît pas l'encodage

Lorsque l'on développe une application, on est souvent amené à lire des fichiers provenant de diverses sources (utilisateurs, autres programmes,...), et dont on ne connaît pas forcément le format, notamment l'encodage.

Java ne fournit pas en standard de moyen de détecter l'encodage d'un fichier, mais grâce aux nombreuses librairies externes disponibles, il est possible de résoudre ce problème.

Nous avons, pour notre part choisi d'utiliser la librairie ICU, développée par IBM, et ce pour plusieurs raisons :

  • c'est une librairie très répandue, et utilisée par de nombreux projets majeurs (gage de sa fiabilité)
  • elle est distribuée sous une licence open source non restrictive, et soutenue par IBM (gage de sa pérennité)
  • nous utilisons BIRT à l'intérieur de notre application, qui dépend lui-même d'ICU, et nous avions donc déjà cette librairie au sein de notre projet.

Voici comment nous l'utilisons dans l'application :

String fileName = "...";
final CharsetDetector detector = new CharsetDetector();
detector.setText(new BufferedInputStream (new FileInputStream(fileName)));
Reader fileReader = detector.detect().getReader();

En faisant cela, nous obtenons donc un reader classique java, qui nous permettra de lire le fichier sans se préoccuper de l'encodage, et de ne pas avoir de problème avec le traitement des caractères non ASCII.
Dans cet exemple, l'utilisation d'un BufferredInputStream est nécessaire, en effet, ICU va lire une partie du fichier pour déterminer l'encodage, puis revenir au début du fichier grâce à la méthode reset() lorsqu'on lui demande un reader, méthode que l'on ne peut pas utiliser sur un FileInputStream.

vendredi 6 mars 2009

Compilation GWT et gestion de la mémoire

Nous utilisons le plugin maven googlewebtoolkit2, pour compiler notre application GWT, sans paramétrage spécifique ou optimisation.
La taille de l'application devenant de plus en plus importante, nous sommes tombés sur un classique

java.lang.OutOfMemoryError: PermGen space.

Premier problème trouver comment modifier les paramètres de la JVM.

Il suffit de rajouter dans la configuration du plugin la balise extraJvmArgs

<extraJvmArgs>-Xmx512M</extraJvmArgs>

Malgré une augmentation de l’espace mémoire alloué, nous avions toujours la même erreur (des 32Mo par défaut à 1024Mo). Nous avons donc essayé de jouer sur les autres paramètres impactant le PermGen space. Et en rajoutant le –Xmn50M nous somme parvenu à compiler à nouveau l’application.

<extraJvmArgs>-Xmx512M -Xms512M –Xmn50M </extraJvmArgs>

Mais nos espoirs ont été de courte durée…Nous nous sommes trouvés avec une compilation aléatoire qui passait de temps en temps…

Nous avons donc choisit de tester la compilation avec une JVM concurrente de celle de Sun : JRockit (initialement BEA, Oracle depuis 2008).
JRockit se veut plus performante que la JVM SUN. Il y a peu d'études intépendantes qui montrent les apports réels en termes performance. Nous avons donc décidé de réaliser les tests nous même.
Suite à quelques essais, force est de constater qu'avec cette JVM nous avons une compilation qui fonctionne sans aucun problème toutes les nuits avec un temps de traitement légèrement inférieur.

mardi 24 février 2009

Hibernate : mapping d'une colonne en Set

L’utilisation d’une relation many-to many avec hibernate permet de rendre transparent la table d’association.

Dans l'exemple suivant, la table EDITION_LIVRE sera mappée de manière transparente :
Livre Many Many
La mapping de la table LIVRE sera le suivant :


‹set name="editions" table="EDITION_LIVRE"›
  ‹key column="LIVRE_ID" /›
  ‹many-to-many class="Edition" column="EDITION_ID" /›
‹/set›

Et Livre contiendra un Set<Edition>, mais l'objet EditionLivre n’existera pas…

A la vue de ce type de mapping, on peut être intéressé de faire la même chose pour un mapping many to one.
En effet, plutôt que de me retrouver avec un Set d'objet dont nous n'avons besoin que pour de l'affichage, nous préférerions avoir un Set de String. Par exemple, si on prend une relation de ce type :

Livre Set
Et qu’on ne veut pas avoir à mapper l’objet LivreMotcle, il suffit de définir dans le fichier de mapping Livre le Set suivant :


‹set name=”mots" table="LIVRE_MOTCLE"›
  ‹key column="LIVRE_ID" /›
  ‹element column="MOT" type="string"/›
‹/set›

On obtient alors un Set<String> mots dans l'objet Livre.

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

- page 1 de 2