Accueil / Articles PiApplications. / La plate-forme Java / Librairies PiApplications / Librairies Jmp.Web

Construction d'un arbre HTML.

La construction d'un arbre HTML est une opération fastidieuse. Pour la simplifier, la librairieJmp.Web dispose de la classe Jmp.Controls.Tree. Cet article donne une procédure à partir d'un exemple permettant la création d'un tel arbre. Nous allons l'illustre ici par un exemple. Ce dernier consiste à créer l'arbre HTML à partir d'un arbre n-aire "ordinaire" (classe Jmp.Data.SerializableTree).

Pour rappel, la classe SerializableTree représente le nœud d'un arbre. Ce nœud est affecté d'un identifiant unique (entier long) auquel on peut "accrocher" une objet qui implémente l'interface Serializable. Ce nœud possède une collection d'un nombre quelconque "d'enfants" qui a leur tour peuvent avoir un nombre quelconques d'enfants.

La construction d'un arbre reposant sur la classe SerializableTree n'est pas l'objet de cet article mais elle ne présente pas de grosse difficulté. Nous supposerons que nous détenons un arbre de ce type à représenter dans une page HTML.

Transformation d'un arbre en arbre HTML.

Voici alors la procédure à suivre :

  1. Créer un contrôle HTML sur la base de la classe Tree en utilisant un de ses constructeurs (par exemple Tree tre = new Tree();).
  2. Ajouter à l'arbre un lien hypertexte de commande de pliage du nœud. Il suffit pour cela de créer un objet de classe ImageLink et de la transmettre à l'arbre via la méthode setCollapseCommand. La classe Tree ajoutera systématiquement le paramètre HTTP id={identifiant du nœud} à l'URL du lien ce qui permet au serveur de savoir quel est le nœud dont l'utilisateur souhaite le pilage ou le déploiement.
    // Ajout de l'image-lien de pliage d'un noeud
    Image imgExpanded = new Image();
    imgExpanded.getCommonAttribute().setTitle(_lcl.getString("ToCollapse"));
    imgExpanded.setImageLink(String.format("%s/images/triangle-down.png", _acn.getSiteURL()));;
    imgExpanded.setDimensions(new Point(16,16));
    String sTag = "";
    if (cmd != null)
      sTag = String.format("cmd=%s&scm=eoc", cmd.name());
    ImageLink ilnCollapse = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgExpanded);
    tre.setCollaspeCommand(ilnCollapse);
    Notez que l'invocation de la méthode  n'est pas obligatoire. Si elle n'est pas invoquée, aucune icône de pliage ou de déploiement ne sera ajoutée à l'arbre.
  3. Ajouter à l'arbre un lien hypertexte de commande de déploiement du nœud. Le principe est rigoureusement identique à celui de l'étape précédente :
    // Ajout de l'image-lien de dépliage d'un noeud
    Image imgCollapsed = new Image();
    imgCollapsed.getCommonAttribute().setTitle(_lcl.getString("ToExpand"));
    imgCollapsed.setImageLink(String.format("%s/images/triangle-right.png", _acn.getSiteURL()));
    imgCollapsed.setDimensions(new Point(16,16));
    sTag = "";
    if (cmd != null)
      sTag = String.format("cmd=%s&scm=eoc", cmd.name());
    ImageLink ilnExpand = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgCollapsed);
    tre.setExpandCommand(ilnExpand);
    Là encre, la présence de ce lien-image n'est pas obligatoire.
  4. Construire les branches de l'arbre de manière récursive :
    // Construction des branches à partir des noeuds racine
    lst.stream().forEach(srl ->
    {
      Node nde = new Node(tre, srl.getID());
      nde.setData(srl);
      nde.setLabel(tlb.buildLabel(nde));
      if (srl.isExpanded())
        nde.expand();
      else
        nde.collapse();
      nde.setLeafIcon(aimgNode[0]);
      nde.setExpandedIcon(aimgNode[1]);
      nde.setCollapsedIcon(aimgNode[2]);
      tre.addChild(nde);
      buildBranch(srl, nde, tlb, aimgNode);
    });
    Bien que ce code paraisse simple, il masque l'ensemble des difficultés. Nous allons le détailler. L'idée générale est de transformer l'arbre initiale en un arbre HTML en substituant aux nœuds de classe SerializableTree des nœuds de classe Node "accrochés" aux même endroits que ceux de l'arbre initial. Notez que pour ne pas perdre d'information sur le nœud initial, nous l'accrochons en tant que donnée utilisateur au nœud HTML : nde.setData(srl); (le paramètre est le nœud initial).

    Une des méthodes les plus importante de la classe Jmp.Web.Controls.Node est la méthode setLabel. Cette méthode attend un objet qui implémente l'interface IWebComponent. Ici la variable tlb construit un lien hypertexte à partir des données du nœud lui-même (plus précisément celle de l'objet de classe SerializableTree accroché précédemment à ce nœud).

    Notez également que le nœud prend l'état de pliage ou de déploiement du nœud initial auquel il se substitue. Au moment de sa sérialisation sous forme de code HTML, l'arbre tient compte de cet état est n'affichera pas les enfants du nœud si ce dernier est plié.

    Le nœud reçoit ensuite 3 icônes représentatives de son état au moment de sa présentation (nœud sans filiation (ou nœud "feuille"), nœud déplié, nœud plié. Cette icône s'affichera devant el libellé du nœud. Sa présence n'est bien évidemment pas obligatoire.

    Ceci fait on ajout le nœud HTML (classe Node) à l'arbre (classe Tree) puis on évoque la méthode buildBranch pour construire l'ensemble de la filiation de ce nœud. Voici le détail de cette méthode récursive :
    /**
     * Construit la branche "concrète" (affichable en HTML) d'une branche issue d'un arbre virtuel.
     * @param srlParent Noeud virtuel parent.
     * @param ndeParent Noeud concret parent.
     * @param tlb Fabrique des libellés de l'arbre.
     * @param aimgNode Images adapté à l'état du noeud. 0 : feuille, 1 : déplié,  2 ou (> 1) : replié.
     */
    private void buildBranch(SerializableTree srlParent, Node ndeParent, ITreeLabelBuilder tlb, Image[] aimgNode)
    {
      srlParent.getChildren().stream().forEach(srl ->
      {
        Node nde = new Node(ndeParent.getTree(), srl.getID());
        nde.setData(srl);
        nde.setLabel(tlb.buildLabel(nde));
        nde.setLeafIcon(aimgNode[0]);
        nde.setExpandedIcon(aimgNode[1]);
        nde.setCollapsedIcon(aimgNode[2]);
        if (srl.isExpanded())
          nde.expand();
        else
          nde.collapse();
        ndeParent.addChild(nde);
        // Récursion
        buildBranch(srl, nde, tlb, aimgNode);
      });
    }
    La récursivité s'interrompt lorsque le nœud n'a pas d'enfant.

Toutes ces étapes accomplies, nous possédons un arbre HTML de classe Tree qui est le reflet de l'arbre initial de classe SerializableTree. Il suffit d'invoquer sa méthode toHtml5 pour le sérialiser sous code HTML5.

Ces explications montent également qu'il est assez aisé de  créer un arbre HTML from scratch (on crée des objets de classe Node que l'on accroche aux différents autres nœuds de l'arbre. Le lecteur attentif, peut s'interroger sur la nécessité de passer par une classe Tree alors que l'on aurait pu se contenter d'un objet de classe Node comme "racine". Cela tient au fait qu'en HTML, on a assez souvent besoin de présenter des branches n'ayant pas une racine commune (un arbre multi-racines en quelques sortes). De plus, la classe Tree permet de factoriser les comportements spécifiques à l'arbre et non au nœud (comme les icônes de pliage ou de déploiement).

Bien entendu, il est assez facile de transposer ce code à n'importe quel arbre non réalisé sur des nœuds de classe SerializableTree.

Une méthode statique de transposition automatique.

Nous allons regrouper ces fragments de code en une méthode générique qui permet de transposer assez simplement un arbre de classe SerializableTree en un arbre de classe Tree. Cet extrait est tiré d'une application réelle. Sa généricité peut donc encore être nettement améliorée de manière à ne faire un méthode statique de portée très générale.

  /**
   * Construit l'arbre affichable via HTML qui correspond à l'arbre passé en paramètre.
   * @param lst Liste des noeuds racine de l'arbre virtuel.
   * @param tlb Fabrique des libellés de l'arbre.
   * @param sTreeID Identifiant de l'arbre.
   * @param aimgNode Images adapté à l'état du noeud. 0 : feuille, 1 : déplié,  2 ou (> 1) : replié.
   * @param cmd Commande à ajouter aux liens des boutons de pliage / dépliage des noeuds (pas d'ajout si objet nul).
   * @return Arbre capable de s'afficher en HTML.
   */
  public Tree buildTree(
      List<SerializableTree> lst, ITreeLabelBuilder tlb, String sTreeID, Image[] aimgNode, Command cmd)
  {
    if (tlb == null)
      throw new IllegalArgumentException("ITreeLabelBuilder tlb = null");
    if (lst == null)
      return null;
    Tree tre = new Tree(sTreeID);
    // Ajout de l'image-lien de pliage d'un noeud
    Image imgExpanded = new Image();
    imgExpanded.getCommonAttribute().setTitle(_lcl.getString("ToCollapse"));
    imgExpanded.setImageLink(String.format("%s/images/triangle-down.png", _acn.getSiteURL()));;
    imgExpanded.setDimensions(new Point(16,16));
    String sTag = "";
    if (cmd != null)
      sTag = String.format("cmd=%s&scm=eoc", cmd.name());
    sTag = AbstractPageBuilder.cipherHttpParameters(_acn, sTag);
    ImageLink ilnCollapse = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgExpanded);
    tre.setCollaspeCommand(ilnCollapse);
    // Ajout de l'image-lien de dépliage d'un noeud
    Image imgCollapsed = new Image();
    imgCollapsed.getCommonAttribute().setTitle(_lcl.getString("ToExpand"));
    imgCollapsed.setImageLink(String.format("%s/images/triangle-right.png", _acn.getSiteURL()));
    imgCollapsed.setDimensions(new Point(16,16));
    sTag = "";
    if (cmd != null)
      sTag = String.format("cmd=%s&scm=eoc", cmd.name());
    sTag = AbstractPageBuilder.cipherHttpParameters(_acn, sTag);
    ImageLink ilnExpand = new ImageLink(String.format("%s?tag=%s", _acn.getApplicationURL(), sTag), imgCollapsed);
    tre.setExpandCommand(ilnExpand);
    // Construction des branches à partir des noeuds racine
    lst.stream().forEach(srl -> 
    {
      Node nde = new Node(tre, srl.getID());
      nde.setData(srl);
      nde.setLabel(tlb.buildLabel(nde));
      if (srl.isExpanded())
        nde.expand();
      else
        nde.collapse();
      nde.setLeafIcon(aimgNode[0]);
      nde.setExpandedIcon(aimgNode[1]);
      nde.setCollapsedIcon(aimgNode[2]);
      tre.addChild(nde);
      buildBranch(srl, nde, tlb, aimgNode);
    });
    return tre;
  }

En vue d'améliorer la généricité de cette méthode, on note que ce code contient "en dur" un certain nombre de choses qui pourraient être converties en paramètres ou éléments de contexte (comme l'URL des icônes de pliage/déploiement ou les paramètres de commande du pliage/déploiement du nœud). L'attribut _lcl est un objet de traduction des chaînes de caractères. On aura intérêt à en faire un paramètre ou un objet global ce qui permettrait de rendre cette méthode statique.

Ce code s'appuie sur les librairies PiApplications Jmp.jar et Jmp.Web.jar.

Pour être complet, nous livrons le code de l'interface ITreeLabelBuilder.

import Jmp.Web.Controls.IWebComponent;
import Jmp.Web.Controls.Node;

/**
 * Interface implémentée par toute classe de construction d'un libellé du noeud d'un arbre affichable.
 * @author (c) PiApplications 2014.
 */
public interface ITreeLabelBuilder
{
  /**
   * Construit le libellé d'un noeud d'arbre à afficher.
   * @param nde Noeud dont le libellé doit être affiché.
   * @return Libellé d'un noeud d'arbre à afficher.
   */
  IWebComponent buildLabel(Node nde);
}

Vous noterez qu'il s'agit d'une interface fonctionnelle ce qui autorise à remplacer le paramètre ITreeLabelBuilder tlb dans la méthode buildTree par une expression lambda.

Il ne reste plus qu'à implémenter cette interface pour obtenir le contrôle HTML qui représentera le libellé d'un nœud dans un contexte donné.

Liaison entre l'arbre et ses nœuds.

La classe Tree est la tête d'un arbre limité à une collection de nœuds "racine" (objets de classeJmp.Web.Controls.Node). Chaque nœud dispose ensuite d'une filiation d'autres objets de classes Node et ainsi de suite. Cette classe Node implémente l'interface IWebComponent.

On ajoute un nœud racine à l'arbre en invoquant la méthode addChild de la classe Tree. Cette méthode a deux prototypes dont l'un est trompeur. Il donne l'impression d'accepter un paramètre qui se limite à implémenter l'interface IWebComponent (ce que fait la classe Node) mais en fait, quelque soit les prototypes, ils attendent l'ajout d'un objet de classe ou dérivant de la classe Node. Le prototype admettant un paramètre de type IWebComponent est simplement imposé par l'héritage de la classe Tree (AbstractWebComponent).

La représentation HTML du nœud (son libellé) se fait au moyen de la méthode Node.setLabel(IWebComponent). Le contrôle qui implémente l'interface IWebComponent peut ici être quelconque tant qu'il est compatible de l'ajout à une cellule d'un tableau.

(c) PiApplications 2016