Developpez.com - 4D
X

Choisissez d'abord la catégorieensuite la rubrique :


Clusters : une utilisation performante des ensembles enregistrés - Partie 2

Date de publication : Janvier 2006

Par Kent D. Wilbur (Manager of Information System, 4D Inc)
 

Voici la seconde note sur l'utilisation des clusters (ensembles enregistrés) pour améliorer radicalement les performances d'une base. La première traitait de la création et de la maintenance des clusters. Celle-ci traite l'utilisation des clusters pour la recherche.

Introduction
Les clusters sont-ils vraiment plus rapides ?
Que se passe-t-il dans le code ?
Une interface Web
La page DisplayRecords.shtml
Affichage détaillé via HTML
Résumé


Introduction

Brièvement résumé, l'objectif est de permettre une recherche rapide de mots-clés ou même d'expressions situées n'importe où dans le titre ou le champ texte de la table [BlocsTextes] dans la base exemple. Chaque mot isolé qui ne figure pas dans la liste des exclusions est analysé et enregistré dans la table [Mots]. Les clusters sont des tableaux booléens qui indiquent quels enregistrements comportent un mot particulier. Ils sont sauvegardés dans des BLOBs de la table [Mots].

info La première note technique est ici :
Clusters : une utilisation performante des ensembles enregistrés - Partie 1


Les clusters sont-ils vraiment plus rapides ?

La réponse est simple, oui ! Mais pour ceux d'entre vous qui en doutent encore, la base exemple propose plusieurs tests. Ouvrez la base et, dans le menu Fichier, choisissez Blocs de texte. Ceci affichera tous les enregistrements de la table [BlocsTexte] et activera la ligne Cluster démo du menu Démo.

Le premier enregistrement de la base a pour titre "Upgrading to 4D Server 6.5.1" (oui, je sais, les données sont obsolètes). Dans cet enregistrement se trouve l'expression "upgrading and converting". Cette occurrence n'existe que pour ce seul enregistrement.

Faisons quelques tests. Si vous avez ouvert l'enregistrement pour vérifier le texte, quittez et rouvrez la base pour vous assurer qu'il n'y a rien dans le cache pour le premier test.



Voici le tableau des résultats que j'ai obtenus en recherchant l'expression "upgrading and converting".


La recherche par cluster a donné le même temps avec ou sans cache. C'est une base relativement réduite, de moins de 6000 enregistrements. Dans une base plus importante, la recherche par "contient" prendrait encore plus de temps, alors que le temps de recherche par cluster resterait virtuellement identique. Pour la recherche par "est égal à", ceci représente un gain de 99,67%.

Donc, ma réponse aujourd'hui est : les clusters peuvent vraiment accélérer significativement ma base avec un peu plus de codage et un plus gros disque dur.

info Note :
Les valeurs ci-dessus sont des moyennes sur plusieurs tests. Elles varient d'une machine à l'autre et selon les critères de recherche. Mais elles sont bien représentatives des résultats attendus.


Que se passe-t-il dans le code ?

La première requête est une simple recherche par contenu directement dans les champs. C'est quelque chose qui se pratique souvent dans les bases sans clusters.

Pour les autres requêtes, les mots cherchés ont été analysés dans un tableau et comparés à la liste des mots exclus (voir partie 1 pour une explication sur les mots exclus). Après le tri, on construit des tableaux de mots, chaque élément de tableau, un mot unique, est trouvé dans la table [Mots]. Le tableau booléen stocké dans la table [Mots] est chargé en mémoire et un ensemble est créé à partir du tableau booléen. Puis, selon la recherche par OU, ET ou Contient, un nouvel ensemble est créé par la commande REUNION ou INTERSECTION. Dans le cas de la recherche par Contient, les enregistrements obtenus avec l'INTERSECTION sont ensuite explorés par une recherche traditionnelle sur les champs eux-mêmes. La différence est que cette recherche ne concerne que les 7 enregistrements qui contiennent à la fois "upgrading" et "converting" au lieu de toute la table. Le résultat est un gain de temps de plus de 99%.

La méthode M_ClusterQuery est attachée au menu Demo de la barre de menu Demo. Elle affiche le dialogue et passe les bons paramètres au code correspondant au choix dans le dialogue.

code 4D - méthode M_ClusterQuery
Si (Faux)
   ` Méthode: M_ClusterQuery
   ` Created by: Kent Wilbur
   ` Objet: Démonstration de la recherche par cluster 
Fin de si 

   ` Déclaration des variables locales
C_ENTIER LONG($LEndTime)
C_ENTIER LONG($LPid)
C_ENTIER LONG($LStartTime)
C_POINTEUR($pTable)

$LWindowID:=Creer fenetre formulaire([zDialogs];"QueryDemo";Dialogue modal déplaçable ; 
                              Centrée horizontalement ;Centrée verticalement ;*)
DIALOGUE([zDialogs];"QueryDemo")
FERMER FENETRE($LWindowID)

Si (OK=1)
   $LStartTime:=Nombre de millisecondes

   Au cas ou 
      : (Longueur(tQueryText)=0)
         ALERTE("Vous devez entrer une valeur à rechercher.")
      : (rb1=1)  ` Ancienne manière de faire une recherche par contenu
         CHERCHER([BlocsTextes];[BlocsTextes]Titre="@"+tQueryText+"@";*)
         CHERCHER([BlocsTextes]; | ;[BlocsTextes]ZoneTexte="@"+tQueryText+"@")
      Sinon 
            ` C'est la seule table pour cette base, mais on pourrait en gérer d'autres ici
         $pTable:=->[BlocsTextes]  
         Au cas ou 
            : (rb3=1)  ` Nouvelle manière de faire une recherche par contenu
               CLUSTER_DoQuery ($pTable;tQueryText;"Contains")
            : (sb1=1)  ` Recherche ET
               CLUSTER_DoQuery ($pTable;tQueryText;"And")
            Sinon 
                  CLUSTER_DoQuery ($pTable;tQueryText;"OR")
         Fin de cas 
         Au cas ou 
            : (Taille tableau(atQueryValues)=0)
               ALERTE("La valeur Entrée ne contient que des mots non indexés\r
			                  Merci d'entrer une autre expression.")
            : (Enregistrements trouves($pTable->)=0)
               ALERTE("Aucun enregistrement trouvé.")
         Fin de cas 
   Fin de cas 

   $LEndTime:=Nombre de millisecondes
   ALERTE("Temps de recherche : "+Chaine($LEndTime-$LStartTime)+" milliseconde(s) pour "
                 +Chaine(Enregistrements trouves([BlocsTextes]))+" enregistrement(s) trouvé(s.")
   WIN_OutputWindowTitle
Fin de si 

  ` Fin méthode

La méthode Cluster_DoQuery a été écrite dès l'origine pour permettre la gestion de plus d'une table. Il suffirait de rajouter des Au cas ou pour chaque table qui utilise la table des clusters. Il faut, bien sûr, un BLOB différent dans la table [Mots] pour chaque table qui utilise la technique des clusters. C'est nécessaire parce qu'on ne peut pas avoir un cluster valide pour un ensemble sur deux tables à la fois. La base centrale 4D Partner a actuellement trois BLOBs stockés dans la table [Mots] pour trois tables principales différentes. Notez que la méthode Cluster_ProcessWordFinds utilise un booléen pour déterminer la condition ET/OU. Ce booléen est passé en paramètre à Cluster_ProcessWordFinds sous forme d'une équation ($tQueryType="AND").

code 4D - méthode CLUSTER_DoQuery
Si (Faux)
   ` Méthode : CLUSTER_DoQuery(ptr;text;text)
   ` Created by: Kent Wilbur
   ` Objet: Effectuer une recherche par cluster
Fin de si 

   ` Déclaration des paramètres
C_POINTEUR($1;$pTable)
C_TEXTE($2;$tQueryText)
C_TEXTE($3;$tQueryType)

   ` Réaffectation pour plus de lisibilité
$pTable:=$1
$tQueryText:=$2
$tQueryType:=$3

CLUSTER_Text2Array ($tQueryText;->atQueryValues)

Au cas ou 
   : (Taille tableau(atQueryValues)=0)  ` Rien à faire, rien à chercher
   : ($pTable=(->[BlocsTextes]))  ` Faire une recherche ET
      Au cas ou 
         : ($tQueryType#"Contains")  ` Faire une recherche ET/OU
            CLUSTER_ProcessWordFinds ($pTable;->[Mots]EnsembleBlocTexte;
                                                   ->atQueryValues;($tQueryType="And"))
         Sinon   `Méthode pour chercher par contenu en utilisant les clusters
            CLUSTER_ProcessWordFinds ($pTable;->[Mots]EnsembleBlocTexte;->atQueryValues;Vrai) 
               ` Faire une recherche ET
            CHERCHER DANS SELECTION($pTable->;[BlocsTextes]Titre="@"+$tQueryText+"@";*)
               ` Chercher dans ce qui reste
            CHERCHER DANS SELECTION($pTable->; | ;[BlocsTextes]ZoneTexte="@"+$tQueryText+"@")
      Fin de cas 
Fin de cas 

  ` Fin méthode

La méthode Cluster_ProcessWordFinds gère le passage en sélection courante des enregistrements trouvés dans les clusters. Dans le cas où aucun mot n'est valide, elle réduit cette sélection à la sélection vide. Si un seul mot-clé est à traiter, au lieu de créer et manipuler des ensembles, elle modifie directement la sélection courante.

code 4D - méthode CLUSTER_ProcessWordFinds
Si (Faux)
   ` Method: CLUSTER_ProcessWordFinds(ptr;ptr;ptr;bool)
   ` Created by: Kent Wilbur
   ` Objet : Traite le tableau de mots à chercher
	
   ` $1 = pointeur de table
   ` $2 = pointeur sur le champ blob qui contient les booléens
   ` $3 = pointeur sur le tableau des mots à rechercher
   ` $4 = indicateur ET / OU      Vrai = ET
Fin de si 

   ` Déclaration des paramètres
C_POINTEUR($1;$pTable)
C_POINTEUR($2;$pBLOBField)
C_POINTEUR($3;$pArray)
C_BOOLEEN($4;$fAndQuery)

   ` Déclaration des variables locales
C_ENTIER LONG($LSizeOfArray)

   ` Réaffectation pour plus de lisibilité
$pTable:=$1
$pBLOBField:=$2
$pArray:=$3
$fAndQuery:=$4

$LSizeOfArray:=Taille tableau($pArray->)

Au cas ou 
   : ($LSizeOfArray=0)
      REDUIRE SELECTION($pTable->;0)
		
   : ($LSizeOfArray=1)
      CHERCHER([Mots];[Mots]Mot=$pArray->{1})
      CLUSTER_LoadFromBLOB ($pTable;$pBLOBField)
   Sinon 
      CHERCHER([Mots];[Mots]Mot=$pArray->{1})
      CLUSTER_LoadFromBLOB ($pTable;$pBLOBField;"TempSet")
      ENREGISTREMENT SUIVANT([Mots])
      Boucle ($i;2;$LSizeOfArray)
         CHERCHER([Mots];[Mots]Mot=$pArray->{$i})
         CLUSTER_LoadFromBLOB ($pTable;$pBLOBField;"TempSet2")
         Si ($fAndQuery)
            INTERSECTION("TempSet";"TempSet2";"TempSet")
         Sinon 
            REUNION("TempSet";"TempSet2";"TempSet")
         Fin de si 
         ENREGISTREMENT SUIVANT([Mots])
      Fin de boucle 
      UTILISER ENSEMBLE("TempSet")
      EFFACER ENSEMBLE("TempSet")
      EFFACER ENSEMBLE("TempSet2")
Fin de cas 

   ` Fin méthode

La méthode Cluster_LoadFromBLOB charge le tableau booléen contenu dans le BLOB et soit le convertit en un ensemble, soit passe simplement en sélection courante le contenu du BLOB sans créer un ensemble, ce qui rend le code plus efficace lorsqu'une seule valeur est recherchée.

code 4D - méthode CLUSTER_LoadFromBLOB
Si (Faux)
   ` Method: CLUSTER_LoadFromBLOB(ptr;ptr{;str})
   ` Created by: Kent Wilbur
   ` Objet: Charge le blob dans un ensemble
	
   `$1 =  pointeur de table
   `$2 = pointeur sur le champ blob qui contient les booléens
   `$3 = nom de l'ensemble, si longueur = 0, prendre la sélection courante
Fin de si 

   ` Déclaration des paramètres
C_POINTEUR($1;$pTable)
C_POINTEUR($2;$pBLOBField)
C_ALPHA(31;$3;$tSetName)

   ` Déclaration des variables locales
TABLEAU BOOLEEN($afBoolean;0)
$pTable:=$1
$pBLOBField:=$2

   ` Réaffectation pour plus de lisibilité
$tSetName:=""
Si (Nombre de parametres>2)
   $tSetName:=$3
Fin de si 

` Lire le tableau booléen dans l'enregistrement
BLOB VERS VARIABLE($pBLOBField->;$afBoolean)  

Si (Longueur($tSetName)=0)  ` Nous ne créons pas un ensemble, mais une sélection courante
   TABLEAU BOOLEEN($afBoolean;Taille tableau($afBoolean)+1)
   CREER SELECTION SUR TABLEAU($pTable->;$afBoolean;"")
Sinon 
   CREER ENSEMBLE SUR TABLEAU($pTable->;$afBoolean;$tSetName)
Fin de si 
   ` Fin méthode

Une interface Web

Il faut très peu de travail supplémentaire pour offrir les mêmes fonctionnalités de recherche via le web. En fait, il suffit de deux méthodes et quelques pages HTML. La page par défaut du site, index.html, est un simple formulaire, presque le même que le formulaire de recherche ci-dessus.


Pour charger cette page html, entrez http://127.0.0.1 dans la fenêtre de votre navigateur.

Dans la méthode Cluster_DoQuery ci-dessus, le type de recherche est déterminé par les mots ET, OU ou Contient. Donc, les boutons radio du formulaire envoient le mot pour lequel chaque bouton radio est sélectionné.

<input type="radio" name="tQueryType" value="And" checked>  <strong> Recherche ET</strong><br>
<input type="radio" name="tQueryType" value="Or">  <strong> Recherche OU</strong><br>
<input type="radio" name="tQueryType" value="Contains">  <strong> Recherche par contenu</strong><br>

Le bouton d'envoi utilise 4DACTION et appelle la méthode WEB_ClusterQuery. Du fait du mécanisme d'affectation des variables dans le 4D Web Server intégré, la seule chose à faire est de déclarer toutes les variables passées à 4D via le Web dans une méthode COMPILER_WEB.
code 4D - Compiler Web
C_TEXTE(tQueryText)
C_TEXTE(tQueryType)
C_ENTIER LONG(LRecordNumber)
C_TEXTE(tCGI)

tQueryText:=""
tQueryType:=""
LRecordNumber:=0
Une fois les variables définies dans la méthode COMPILER_WEB, 4D affecte automatiquement les variables d'un formulaire HTML aux variables correspondantes de 4D. Les déclarations de COMPILER_WEB sont absolument nécessaires pour assurer le fonctionnement des formulaires envoyés à 4D via le web.

La méthode WEB_ClusterQuery réplique les fonctionnalités de la méthode M_ClusterQuery utilisée dans l'interface 4D.
code 4D - méthode WEB_ClusterQuery
Si (Faux)
   ` Method: WEB_ClusterQuery
   ` Created by: Kent Wilbur
   ` Objet: Montrer la recherche par cluster via le web
Fin de si 

CLUSTER_DoQuery (->[BlocsTextes];tQueryText;tQueryType)
TABLEAU ENTIER LONG(aLRecordNumber;0)
TABLEAU TEXTE(atTitle;0)
SELECTION VERS TABLEAU([BlocsTextes];aLRecordNumber;[BlocsTextes]Titre;atTitle)
ENVOYER FICHIER HTML("DisplayRecords.shtml")
   ` Fin méthode

Il y a deux manières d'afficher des enregistrements dans une page web avec une boucle.
     1) En utilisant les enregistrements eux-mêmes ou
     2) en utilisant des tableaux.

Nous voulons que l'utilisateur puisse voir l'information du formulaire détaillé s'ils le souhaitent. Pour cela, avec les enregistrements eux-mêmes, nous aurions besoin d'une clé unique pour chaque enregistrement [BlocsTextes]. Mais, pour garder cette base aussi simple que possible, je n'ai pas créé de champ clé. C'est pourquoi nous allons utiliser le numéro d'enregistrement et, de ce fait, il sera plus simple de boucler sur des tableaux plutôt que sur les enregistrements eux-mêmes. Deux tableaux sont créés, l'un pour les numéros d'enregistrement et l'autre pour les titres.

info Note :
Vous ne pouvez pas utiliser des tableaux locaux pour l'affichage sur le web.

Enfin, le fichier HTML est envoyé. Notez l'extension .shtml. Ceci force 4D à analyser la page pour rechercher les balises 4dvar qui pourraient se trouver dans la page. Sans l'extension .shtml, la page est considérée comme statique et pourrait ne pas être analysée.

Comme cette méthode est appelée directement par 4DACTION, il est nécessaire d'activer la propriété de la méthode qui l'autorise à être appelée par 4DACTION. Pour protéger votre base des accès indésirables, toute méthode non appelée par 4DACTION devrait avoir cette option de sécurité décochée.
(C'est l'option par défaut pour les nouvelles méthodes, mais il faut vérifier cette option pour les bases converties depuis des versions antérieures à 4D 2003 qui ont probablement cette option activée. C'était une nécessité pour la compatibilité descendante.)


La page DisplayRecords.shtml

C'est le seul élément complexe créé pour afficher correctement les données. C'est complexe parce que cela doit non seulement afficher les données obtenues avec des liens pour montrer les enregistrements trouvés, mais aussi gérer toutes les erreurs d'entrée des utilisateurs.

info Note :
Toutes les pages html fournies utilisent des feuilles de style css. Je ne vais pas exposer ici comment on les utilise ni comment elles fonctionnent. C'est un sujet sur lequel il existe des livres entiers. Je dirai seulement que les feuilles de style css sont utilisées dans les pages html pour donner à la page une apparence générale en matière de couleurs, bordures et polices. Cette note technique ne traite que la part active du corps des pages .shtml.

page html
<body>
   <div id="wrapper">
   <!--#4dif (Size of array(atTitle)=0)-->

      <p>
      <!--#4dif (Length(tQueryText)=0)-->
         You must enter someting to search if you expect to find anything!
      <!--#4delse-->
         <!--#4dif (Size of array(atQueryValues)=0)-->
            Sorry the value entered contains only non-indexed words. Please try another phrase.
         <!--#4delse-->
            Sorry, no records were found matching your request. Please try different words.
         <!--#4dendif-->
      <!--#4dendif--><br><br><br><br>
      </p>

   <!--#4delse-->

      <!--#4DLoop atTitle-->
         <h1>Title:<a href="<!--#4dvar tCGI-->/4daction/Web_ShowRecord?LRecordNumber=
         <!--#4dvar aLRecordNumber{atTitle}-->"><!--#4DVAR atTitle{atTitle }--></a></h1>
      <!--#4DEndLoop-->
      
   <!--#4dendif-->
   </div>

   <a href="http://www.4d.com/" id="logo">
   <img src="/images/4dlogo.gif" width="39" height="53" alt="4D Logo" />
   </a>
</body>

Il y a trois conditions d'erreur.
   · Premièrement, aucun critère de recherche n'a été entré ;
   · deuxièmement, tous les mots entrés figurent dans la liste des exclusions ;
   · troisièmement, aucun enregistrement n'a été trouvé.

Quoique ces conditions soient mutuellement exclusives, il n'y a pas de balise "Au cas où" pour l'affichage de données dans une page 4d.shtml. De ce fait, le code utilise des Si en cascade, tous basés sur le fait qu'aucun enregistrement n'a été trouvé. Comme dans d'autres technologies .shtml, les balises 4d sont incluses dans des commentaires html : <!--quelque chose->. Quoiqu'une balise puisse techniquement fonctionner sans le # devant la fonction 4d, il est vivement recommandé de l'inclure systématiquement, pour qu'il soit correctement interprété par les différents éditeurs en cas de modification des documents html.

Si des enregistrements sont trouvés, une balise #4dloop est exécutée. Cette balise <!--#4DLoop atTitle--> doit utiliser un tableau avec au moins un élément ou le nom d'une table dont la sélection contient au moins un enregistrement et elle se comporte comme une boucle.
code 4D
Boucle (atTitle ;1 ;Taille tableau (atTitle )).

Dans cette boucle, le titre est affiché et un lien <a href> est créé à la volée pour chaque enregistrement du tableau.
< !--4dvar tCGI/4daction/Web_ShowRecord ?LRecordNumber=< !--#4dvar aLRecordNumber{atTitle}-->

Dans cette base, tCGI n'a pas de valeur, rendant le lien relatif à l'URL dans le navigateur.
Après analyse, cela pourrait produire quelque chose comme :
     /4daction/Web_ShowRecord ?LRecordNumber=5553


Affichage détaillé via HTML

Lorsque l'utilisateur clique sur un lien affiché avec la page DisplayRecords.shtml, la méthode WEB_ShowRecord est appelée. Une fois de plus, assurez-vous que l'option de sécurité 4DACTION est activée dans toutes les bases que vous créez.

code 4D - méthode WEB_ShowRecord
Si (Faux)
   ` Method: WEB_ShowRecord
   ` Created by: Kent Wilbur
   ` Objet: Affiche l'enregistrement cherché
Fin de si 

ALLER A ENREGISTREMENT([BlocsTextes];LRecordNumber)
ENVOYER FICHIER HTML("ShowRecord.shtml")

Ceci trouve l'enregistrement et envoie la page ShowRecord.shtml, laquelle affiche simplement les informations sur l'enregistrement courant en utilisant directement les champs de la table.


Résumé

Dans cette série de notes techniques, j'ai tenté de montrer à quel point l'implémentation de clusters peut améliorer les requêtes dans votre base.

Nos tests sur la base exemple ont renvoyé des résultats en un temps de l'ordre de la milliseconde. Avec requête à un seul mot-clé via une page web, les résultats étaient renvoyés avant que j'aie le temps de bouger ma souris après le clic sur le bouton de recherche. C'est rapide !

En y réfléchissant, vous pouvez adapter les clusters pratiquement à n'importe quoi. Ce ne sont pas obligatoirement des champs textes. N'importe quelle recherche multicritères fréquente est candidate à l'utilisation de clusters. Si vous avez des recherches fréquentes qui prennent plusieurs minutes, pensez aux clusters. Imaginez le passage des minutes aux millisecondes.

Lorsqu'on parle d'optimisation pour une base, on répond compilation. C'est généralement une bonne réponse, à moins que la base puisse utiliser les clusters. Rien n'améliore les performances comme le bon usage des clusters. (Mais, vous pouvez bien sûr améliorer la création et la maintenance des clusters grâce à la compilation.)

Maintenant que vous les connaissez, si vous en implémentiez dans votre propre base vous pourriez savourer les félicitations de vos utilisateurs finaux pour une si notable accélération.

__________________________________________________
Copyright © 1985-2009 4D SA - Tous droits réservés
Tous les efforts ont été faits pour que le contenu de cette note technique présente le maximum de fiabilité possible.
Néanmoins, les différents éléments composant cette note technique, et le cas échéant, le code, sont fournis sans garantie d'aucune sorte. L'auteur et 4D S.A. déclinent donc toute responsabilité quant à l'utilisation qui pourrait être faite de ces éléments, tant à l'égard de leurs utilisateurs que des tiers.
Les informations contenues dans ce document peuvent faire l'objet de modifications sans préavis et ne sauraient en aucune manière engager 4D SA. La fourniture du logiciel décrit dans ce document est régie par un octroi de licence dont les termes sont précisés par ailleurs dans la licence électronique figurant sur le support du Logiciel et de la Documentation afférente. Le logiciel et sa documentation ne peuvent être utilisés, copiés ou reproduits sur quelque support que ce soit et de quelque manière que ce soit, que conformément aux termes de cette licence.
Aucune partie de ce document ne peut être reproduite ou recopiée de quelque manière que ce soit, électronique ou mécanique, y compris par photocopie, enregistrement, archivage ou tout autre procédé de stockage, de traitement et de récupération d'informations, pour d'autres buts que l'usage personnel de l'acheteur, et ce exclusivement aux conditions contractuelles, sans la permission explicite de 4D SA.
4D, 4D Calc, 4D Draw, 4D Write, 4D Insider, 4ème Dimension ®, 4D Server, 4D Compiler ainsi que les logos 4e Dimension, sont des marques enregistrées de 4D SA.
Windows,Windows NT,Win 32s et Microsoft sont des marques enregistrées de Microsoft Corporation.
Apple, Macintosh, Power Macintosh, LaserWriter, ImageWriter, QuickTime sont des marques enregistrées ou des noms commerciaux de Apple Computer,Inc.
Mac2Win Software Copyright © 1990-2002 est un produit de Altura Software,Inc.
4D Write contient des éléments de "MacLink Plus file translation", un produit de DataViz, Inc,55 Corporate drive,Trumbull,CT,USA.
XTND Copyright 1992-2002 © 4D SA. Tous droits réservés.
XTND Technology Copyright 1989-2002 © Claris Corporation.. Tous droits réservés ACROBAT © Copyright 1987-2002, Secret Commercial Adobe Systems Inc.Tous droits réservés. ACROBAT est une marque enregistrée d'Adobe Systems Inc.
Tous les autres noms de produits ou appellations sont des marques déposées ou des noms commerciaux appartenant à leurs propriétaires respectifs.
__________________________________________________
 



Valid XHTML 1.1!Valid CSS!

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.
Contacter le responsable de la rubrique 4D