Académique Documents
Professionnel Documents
Culture Documents
Novembre 2009
INTRODUCTION
La dernière décennie a vu l‟explosion du Web et plus récemment l‟apparition du Web 2.0.
Les interactions automatiques entre sites Web, sites et applications clientes ou entre sites
Web et bases de données métier sont en revanche plus récentes et exigent généralement
des interconnexions globales parfois difficiles à comprendre.
La vitesse de déplacement et de rafraîchissement des données sur le Web est devenue bien
supérieure à la capacité de consultation de chacun ; d‟où une forte demande pour les
programmes permettant de localiser, suivre et superviser des informations multisources
(donnes commerciales, informations financières, communautés en ligne, campagnes
marketing, etc.).
POURQUOI DES SERVICES WEB ?
L‟émergence des services Web permet d‟envisager de nouveaux modes d‟utilisation
d‟Internet par les entreprises... Naviguer sur différentes pages pour passer une commande
est suffisant pour les environnements B2C (business-to-consumer) mais certainement pas
pour les contextes B2B (business-to-business) plus sophistiqués. Par exemple, il est
généralement très ergonomique pour un utilisateur de rechercher et acheter des livres sur un
site marchand ; en revanche, si vous dirigez une librairie en ligne passant des centaines de
commandes par jour, la même approche serait totalement inefficace – surtout si d‟autres
applications déterminent les réapprovisionnements en fonction des ventes… Ressaisir ces
données dans une autre application serait en effet complètement ridicule...
Les services Web ont (ou du moins avaient à l‟origine) précisément pour vocation de
résoudre ce problème. Par exemple, lorsque le programme crée automatiquement une
demande de réapprovisionnement de livres ; il l‟adresse ensuite à un service Web qui
retourne immédiatement les informations sur la commande. L‟étape suivante consistant par
exemple à demander un numéro de suivi de livraison. À ce stade, le programme peut utiliser
un autre service Web pour suivre la livraison jusqu‟à sa destination – et informer le client sur
le temps d‟attente. À la livraison, le programme peut également adresser une information
(par SMS/Pager/Twitter) aux personnes ayant des commandes en attente, déclencher un
paiement avec un service Web bancaire, etc. Cet exemple donne une bonne idée de la
cinématique générale : les services Web sont à l‟interopérabilité des systèmes ce que le
Web et l‟e-mail sont aux interactions entre individus...
Le périmètre des services Web est extrêmement étendu et implique de multiples
technologies et standards. Je me concentrerai donc sur l‟implémentation Delphi sous-jacente
et sur les aspects techniques des services Web – sans aborder leurs implications métier au
sens large. Delphi for Win32 propose des solutions très sophistiquées pour prendre en
charge les services Web – dérivées initialement de SOAP – et qui peuvent aujourd‟hui être
facilement étendues à travers des composants HTTP et REST.
TECHNOLOGIES CLÉS DES SERVICES WEB : SOAP ET REST
Le concept de service Web est en soi plutôt abstrait. Technologiquement, deux solutions
attirent principalement les développeurs : le standard SOAP (Simple Object Access Protocol,
voir http://www.w3.org/TR/soap/) et REST (Representational State Transfer) avec sa
variante XML-RPC (XML-Remote Procedure Call).
Ces deux solutions utilisent généralement HTTP comme protocole de transmission (bien qu‟il
existe des alternatives) et XML (ou JSON) pour les déplacements bidirectionnels de
données. Le protocole HTTP standard permet au serveur Web de gérer les requêtes et de
faire transiter les paquets de données à travers les pare-feux.
Ce livre blanc n‟aborde pas les détails de SOAP et porte exclusivement sur REST. Je
commencerai par définir les fondations théoriques, je présenterai ensuite un exemple simple
de serveur et de client, je reviendrai en détail sur le développement de clients REST pour
des services Web REST particulièrement populaires et je conclurai sur le support serveur
REST offert par Delphi 2010 – en tant qu‟extension de l‟architecture DataSnap.
Les données extraites doivent ressembler à ce qui suit (légèrement simplifié pour plus de
lisibilité) lorsqu‟elles sont affichées dans un éditeur XML :
TRAITEMENT DES DONNÉES RSS AVEC XPATH
Pour extraire les informations requises dans ce document XML, le programme RssClient
utilise des expressions XPath. Par exemple, pour lire le titre de la première contribution du
blog (item) de la liste :
/rss/channel/item[1]/title
XMLDocument1.LoadFromXML(strXml);
XMLDocument1.Active := True;
IDomSel := (XMLDocument1.DocumentElement.DOMNode
as IDOMNodeSelect);
for I := 1 to 15 do
begin
Node := IDomSel.selectNode(
'/rss/channel/item[' + IntToStr (i) + ']/title');
title := getChildNodes (Node);
Node := IDomSel.selectNode(
'/rss/channel/item[' + IntToStr (i) + ']/author');
author := getChildNodes (Node);
Node := IDomSel.selectNode(
'/rss/channel/item[' + IntToStr (i) + ']/pubDate');
pubDate := getChildNodes (Node);
ListBox1.Items.Add(author + ': ' + title + ' [' +
pubDate + ']');
end;
end;
L‟écran ci-dessous présente les effets de l‟exécution de ce programme :
DES CARTES ET EMPLACEMENTS
L‟accès à des éléments de cartographie et de positionnement est très souvent nécessaire –
pour toutes les applications incluant une ou plusieurs adresses ; de multiples informations
cartographiques sont accessibles sur différents sites majeurs (Google, Yahoo, Microsoft,
etc.).
SERVICE DE CODAGE GÉOGRAPHIQUE DE
GOOGLE
Nous commencerons par le service de géocodification de Google permettant de soumettre
une adresse et de récupérer ses coordonnées de latitude/longitude à partir d‟une requête
telle que :
http://maps.google.com/maps/geo?q=[address]&output=[format]
&key=[key]
Il est également possible d'entrer une URL similaire dans son navigateur à des fins de test
(voir ci-après les coordonnées de New York dans un navigateur en format XML) :
for I := 0 to sListCity.Count - 1 do
begin
ListBox1.ItemIndex := I;
if Length (sListCity.Names[I]) > 2 then
begin
strResponse := IdHTTP1.Get( TIDUri.UrlEncode(
'http://maps.google.com/maps/geo?q=' +
(sListCity.Names[I]) + '&output=csv&key=' +
googleMapKey));
sList.LineBreak := ',';
sList.Text := strResponse;
str1 := sList[2];
str2 := sList[3];
cdsTown.AppendRecord([sListCity.Names[I],
StrToFloat (str1), StrToFloat (str2),
Length (sListCity.ValueFromIndex[I])]);
Sleep (150);
Application.ProcessMessages;
end;
end;
sList.Free;
end;
Le résultat doit ressembler à la copie d‟écran qui suit :
YAHOO MAPS
L‟étape suivante peut consister à accéder à la véritable carte correspondant à une adresse.
Google Maps propose d‟innombrables fonctionnalités conçues pour être hébergées sur les
sites Web plutôt que dans les applications clientes. (Je propose néanmoins un exemple
d‟hébergement Google Map dans un programme client ; l‟architecture et le code en sont
néanmoins complexes et ne sont pas présentés dans cette étude).
Ce nouvel exemple dénommé YahooMaps utilise l‟API Yahoo Map pour obtenir une carte et
l‟afficher dans un contrôle Image. Les informations sur cette API REST et le lien pour obtenir
un identifiant gratuit pour Yahoo Application sont accessibles sur :
http://developer.yahoo.com/maps/
Une fois encore, cet identifiant est requis pour exécuter les programmes et enregistrer un
fichier INI spécifique dans le répertoire « Documents utilisateur » dénommé YahooMaps.ini.
La carte est récupérée en deux étapes : le premier appel HTTP passe l‟adresse et reçoit
l‟URL de l‟image de la carte ; le deuxième appel HTTP la récupère. Une fois encore, ces
deux étapes peuvent être simulées dans un navigateur Web – très utile pour le débogage.
Le programme utilise la base de données et l‟élément StringList de l‟exemple précédent. Il
intègre également un bouton permettant d‟afficher la carte ou une ville définie (par exemple,
San José, Californie) avec la méthode suivante :
const
BaseUrl = 'http://api.local.yahoo.com/MapsService/V1/';
procedure TFormMap.Button1Click(Sender: TObject);
var
strResult: string;
memStr: tFileStream;
begin
strResult := IdHTTP1.Get(BaseUrl +
'mapImage?' +
'appid=' + yahooAppid +
'&city=SanJose,California');
XMLDocument1.Active := False;
XMLDocument1.XML.Text := strResult;
XMLDocument1.Active := True;
strResult := XMLDocument1.DocumentElement.NodeValue;
XMLDocument1.Active := False;
// Chargement de l‘image
Image1.Picture.LoadFromFile('temp.png');
end;
La première requête GET HTTP fournit la requête réelle et retourne un document XML
avec l‟URL de l‟image de la carte comme suit (identifiant long omis) :
<Result>
http://gws.maps.yahoo.com/mapimage?MAPDATA=[...]&mvt=m
&cltype=onnetwork&.intl=us&appid=[...]
&oper=&_proxy=ydn,xml
</Result>
C‟est la raison pour laquelle le programme peut extraire la valeur du seul nœud avec le
code :
XMLDocument1.DocumentElement.NodeValue
Finalement, l‟image est enregistrée dans un fichier temporaire et chargé dans un contrôle
Image. Au-delà de la carte de cette ville spécifique, le programme peut également retrouver
celles de la base de données Customer.cds de l‟exemple précédent. Un bouton permet
d‟obtenir la carte de l‟emplacement spécifiquement indiqué (San José, exemple de la
démonstration Delphi Live).
En voici un exemple :
Dans ce cas, il est inutile d‟obtenir une clé de signature (ni de fichier INI) ; il suffit de fournir
un site référent (bien que tout semble fonctionner même sans cette information). Il est
possible de demander une traduction dans son navigateur Web en entrant une URL du type :
http://ajax.googleapis.com/ajax/services/language/translate?
v=1.0&q=What%20a%20nice%20day&langpair=en|de
Le résultat de cet appel est présenté ci-dessous (avec résultat JSON pour plus de lisibilité) :
{
"responseData":
{
"translatedText":"Was für ein schöner Tag"
},
"responseDetails": null,
"responseStatus": 200
}
Cet exemple accomplit une étape de plus par rapport aux précédentes démonstrations. Au
lieu d‟une requête HTTP, il utilise un composant VCL spécifique, appelé par une méthode de
classe (pour éviter de placer le composant dans un formulaire -- même si cela est possible).
Ce composant de support rend l‟API extrêmement simple d‟emploi et encapsule
intégralement l‟appel HTTP.
COMPOSANT DE TRADUCTION
Ci-après la déclaration de classe du composant :
type
TBabelGoogleRest = class (TComponent)
protected
Http1: TIdHttp;
FFromLang: string;
FToLang: string;
FActiveInForm: Boolean;
procedure SetFromLang(const Value: string);
procedure SetToLang(const Value: string);
public
function DoTranslate (strIn: string): string; virtual;
constructor Create(AOwner: TComponent); override;
strUrl := Format (
'http://ajax.googleapis.com/ajax/services/language/tran
slate?' +
'v=1.0&q=%s&langpair=%s',
[TIdUri.ParamsEncode (strIn),
FFromLang + '%7C' + FToLang]); // Format n’aime pas du
tout %7 !!!
strResult := Http1.Get(strUrl);
La seule condition est de réaliser une requête GET HTTP passant l‟en-tête HTTP standard –
avec le nom utilisateur et le mot de passe – une opération extrêmement simple à ajouter
dans Delphi aux propriétés correspondantes d‟un composant IdHttp. Une opération plus
compliquée, si l‟on peut dire, consiste à poster une mise à jour d‟état sur votre compte. Cette
opération est réalisée avec une requête POST HTTP passant un paramètre d‟état comme
dans le snippet qui suit, permettant d‟adresser à Twitter le texte de l‟un des champs de
l‟enregistrement actuel d‟un composant ClientDataSet (ClientDataSet1TEXT) et de le
marquer comme « posté » :
procedure TForm34.btnPostCurrentClick(Sender: TObject);
var
strResult: string;
listParams: TStringList;
begin
listParams := TStringList.Create;
listParams.Add('status=' + ClientDataSet1TEXT.AsString);
try
strResult := idhttp1.Post(
'http://twitter.com/statuses/update.xml',
listParams);
ShowMessage (strResult);
ClientDataSet1.Edit;
ClientDataSet1POSTED.AsString := 'T';
ClientDataSet1.Post;
finally
listParams.Free;
end;
end;
Ce code est extrait de mon application « Delphi Tweet of the Day » qui utilise un composant
ClientDataSet (local ou raccroché à un serveur distant) pour saisir les Tweets. Après avoir
posté la valeur du champ TEXT de la table, en réalité, le programme indique « Vrai » dans le
champ POSTED.
Il y a d‟autre code dans le programme mais rien qui soit réellement lié à Twitter ni à son API
REST. L‟élément suivant pertinent est la configuration du composant IdHttp :
object IdHTTP1: TIdHTTP
Request.ContentLength = -1
Request.Accept = 'text/html, */*'
Request.BasicAuthentication = True
Request.Password = '***' // Omis mais requis
Request.UserAgent = 'tweetoftheday'
Request.Username = 'delphitweetday'
HTTPOptions = [hoForceEncodeParams]
end
En d‟autres termes, le codage Twitter est extrêmement simple et permet de bénéficier d‟une
mise en ligne automatique à intervalles fixes ou en cas de survenance d‟un événement
pertinent pour vos activités (et dans votre base de données).
sList := TStringList.Create;
try
sList.Text := res;
FAuthString := sList.Values['Auth'];
FReqTime := Now;
finally
sList.Free;
end;
end;
J‟ai créé un service global de type « singleton » pour cette classe ; chaque fois qu‟une
requête est nécessaire, j‟applique une méthode « helper » (fonction globale), qui ajoute le
jeton supplémentaire. Voici le code de cette (assez longue) fonction :
function DoRequest (
const methodAttr, fromAttr, strData: string): string;
var
res: String;
postStream: TStream;
IdHttp: TIdHttp;
resStream: TStringStream;
begin
IdHttp := TIdHttp.Create (nil);
try
// Ajoût autorisation de la clé enregistrée
IdHttp.Request.CustomHeaders.Values ['Authorization'] :=
'GoogleLogin auth=' + googleAuth.AuthString;
IdHttp.Request.CustomHeaders.Values ['Content-type'] :=
'application/atom+xml';
IdHttp.Request.CustomHeaders.Values ['GData-Version'] :=
'2';
// Utilisation SSL
IdHttp.IOHandler :=
TIdSSLIOHandlerSocketOpenSSL.Create(IdHttp);
try
if (methodAttr = 'post') or (methodAttr = 'put') then
begin
postStream := TStringStream.Create (strData);
try
postStream.Position := 0;
if (methodAttr = 'post') then
Bien qu‟un peu lourd, ce code structurel est précieux pour simplifier les appels SSL habilités.
Ainsi, pour requérir une liste des feuilles de calcul privées, on utilisera le code suivant :
DoAppRequest (
'get',
'http://spreadsheets.google.com/feeds/spreadsheets/private
/full',
'');
Ce code fait partie de la démonstration, qui utilise les informations disponibles dans le fichier
dbtosheet.ini – structuré comme suit et chargé d‟exécuter le programme de démo :
[google]
email=
password=
accounttype=GOOGLE
Le fichier est chargé au démarrage du programme et les trois valeurs de l‟arborescence sont
utilisées pour renseigner l'objet TGoogleAuth, désigné googleAuth. Le quatrième paramètre
– le service – est défini par le programme :
googleAuth.GoogleEmail :=
inifile.ReadString('google', 'email', '');
googleAuth.GooglePwd :=
inifile.ReadString('google', 'password', '');
googleAuth.AccountType :=
inifile.ReadString('google', 'accounttype', 'GOOGLE');
googleAuth.ServiceName := 'wise';
Dans cette configuration, le programme dispose d‟un premier bouton permettant de requérir
la liste de feuilles de calcul disponibles et d'ajouter leurs identifiants (ou URL) dans une boîte
liste. L‟objet XML en résultant est analysé avec XPath – après élimination de l‟espace de
nommage, qui ne doit pas être pris en compte par les requêtes XPath, au risque de devenir
excessivement complexes :
procedure TForm34.btnListSheetsClick(Sender: TObject);
var
strXml: string;
IDomSel: IDOMNodeSelect;
Node: IDOMNode;
I, nCount: Integer;
title, id: string;
begin
strXml := DoAppRequest ('get',
'http://spreadsheets.google.com/feeds/spreadsheets/priv
ate/full',
'');
strXml := StringReplace (strXml,
'<feed xmlns=''http://www.w3.org/2005/Atom''', '<feed
', []);
XMLDocument1.LoadFromXML(strXml);
XMLDocument1.Active := True;
IDomSel := (XMLDocument1.DocumentElement.DOMNode as
IDOMNodeSelect);
nCount := IDomSel.selectNodes ('/feed/entry').length;
for I := 1 to nCount do
begin
Node := IDomSel.selectNode(
'/feed/entry[' + IntToStr (i) + ']/title');
title := getChildNodes (Node);
Node := IDomSel.selectNode(
'/feed/entry[' + IntToStr (i) + ']/content/@src');
id := getChildNodes (Node);
ListBox1.Items.Add(title + '=' + id);
end;
end;
La boucle s‟exécute sur chaque nœud (en utilisant la fonction de décompte XPath pour
déterminer le nombre de nœuds) et ajoute le titre de la feuille de calcul et son ID/URL à la
liste, comme le montre l‟illustration suivante :
Grâce à ces données d‟identification ID/URL, il est possible d‟adresser une seconde requête
pour demander les feuilles d‟onglet disponibles dans le document spécifique. Même s‟il n‟en
existe qu‟un seul, il est nécessaire de se référer à un onglet spécifique dans chaque
opération affectant le contenu du document. Ceci permet au programme de récupérer
l‟identifiant dans la liste et d‟établir la seconde requête REST :
strSheetId := ListBox1.Items.ValueFromIndex
[ListBox1.ItemIndex];
strXml := DoAppRequest ('get', strSheetId, '');
À ce stade, une autre boucle similaire permet d‟extraire les noms de feuilles et de les
déplacer dans une seconde boîte liste :
IDomSel := (XMLDocument1.DocumentElement.DOMNode as
IDOMNodeSelect);
nCount := IDomSel.selectNodes ('/feed/entry').length;
for I := 1 to nCount do
begin
Node := IDomSel.selectNode(
'/feed/entry[' + IntToStr (i) + ']/title');
title := getChildNodes (Node);
Node := IDomSel.selectNode(
'/feed/entry[' + IntToStr (i) + ']/content/@src');
id := getChildNodes (Node);
ListBox2.Items.Add(title + '=' + id);
end;
Mais la véritable force de ce programme est sa capacité à ajouter des données au document
lui-même. Le moyen le plus simple d‟y parvenir est de créer un document se comportant
comme une table de base de données (c‟est-à-dire disposant d‟une première ligne avec les
noms de champs). Ce type de document, apparaissant dans le navigateur, est illustré ci-
dessous :
Les cellules de la première ligne sont de simples entrées texte. L‟intérêt est qu'il permet de
se référer aux colonnes correspondantes à l'aide d'un espace de nommage dynamique
comme :
Ainsi, il est possible d‟ajouter de nouvelles lignes au document en récupérant les champs
d‟une table avec les noms correspondants. Ceci nécessite une requête POST HTTP
reposant sur l‟identifiant ID :
const
gsxNameSpace =
'xmlns:gsx="http://schemas.google.com/' +
'spreadsheets/2006/extended"';
begin
strSheetId := ListBox2.Items.ValueFromIndex
[ListBox2.ItemIndex];
Memo1.Lines.Add(
DoAppRequest ('post',
strSheetId,
'<entry xmlns="http://www.w3.org/2005/Atom" ' +
gsxNameSpace + '>' +
recordtoxml +
'</entry>'));
La fonction recordtoxml récupère les valeurs des champs qui nous intéressent dans
l‟enregistrement en cours du jeu de données client (ClientDataSet) utilisé par le programme
et produit les données d‟entrée appropriées :
function recordtoxml: string;
begin
Result :=
FieldToXml ('custno') +
FieldToXml ('company') +
FieldToXml ('city') +
FieldToXml ('state') +
FieldToXml ('country');
end;
Comme précédemment mentionné, l‟espace de nommage gsx: pseudo se réfère aux noms
des colonnes de la feuille de calcul, qui sont déterminés par les chaînes de la première ligne.
Ce code permet d‟ajouter à la feuille de calcul de nouvelles lignes correspondant à des
informations enregistrées dans la base de données.
Cependant, rien ne vous empêche (ni toute autre personne disposant de droits d‟édition sur
le document) d‟éditer les données après leur publication ; le document recalculera alors
automatiquement les totaux en fonction des données adressées par l‟application cliente –
permettant à n'importe quel utilisateur correctement habilité de consulter le document
résultant.
Nous avons donc créé un mécanisme sophistiqué de consultation et d'édition de données
émanant de tables de base de données, avec des mises à jour automatiques et continues et
une solide infrastructure d‟autorisation… et tout cela à partir d‟un compte Gmail gratuit !
Pour un hébergement effectif sur serveur Web, l'option ISAPI est recommandée – bien que
l‟option CGI (généralement plus lente) soit plus simple à paramétrer et à déboguer.
Toutefois, pour de réelles fonctionnalités de débogage et de test, il est fortement conseillé
d'utiliser l'infrastructure Débogueur d‟application Web fournie par Delphi, qui permet d‟ajouter
directement un point de rupture au code et de contrôler les flux de données HTTP (y compris
les en-têtes). Si vous choisissez cette option, il vous faudra insérer un nom de classe
interne, exclusivement utilisé comme référence programme (dans l‟URL Débogueur
d‟application Web). Notez également que même si cela n‟est pas explicitement mentionné
dans la boîte de dialogue de l‟assistant, Delphi continue à supporter les modules
d‟intégration de serveur Apache - étant précisé que le paramétrage projet sera manuel.
La boîte de dialogue de l‟assistant DataSnap WebBroker permet de demander une classe de
méthode serveur prête à l‟emploi avec des échantillons ; cependant, vous pouvez, une fois
encore, très simplement déployer la vôtre dans le code. Avec les paramétrages décrits ci-
dessus, Delphi générera un projet avec différentes unités organisées en arborescence :
Un formulaire principal sans usage spécifique mais pouvant servir pour les
informations de connexion. Ce formulaire sera supprimé lorsque vous modifierez la
structure projet pour un module serveur Web ou CGI – ne vous en préoccupez donc
pas trop.
Un module de données Web (héritier de TWebModule) hébergeant les composants
serveur DataSnap
Un module de données agissant comme une classe serveur, permettant d'ajouter le
code à exécuter dans le serveur REST.
MODULE DE DONNÉES WEB GÉNÉRÉ PAR L‟ASSISTANT
DATASNAP WEBBROKER
Nous examinerons tout d‟abord en détail ces deux modules de données, avant d'ajouter de
nouvelles lignes de code programme et de les tester. Le module Web est un composant clé
de l‟architecture WebBroker. Il est capable de définir de multiples actions et dispose
d'événements pré- et post-traitement pour n'importe quelle requête HTTP. Sur ce module
Web, il est possible d‟ajouter des composants qui interceptent des actions URL données,
comme DSHTTPWebDispatcher dans l‟exemple fourni ci-dessous :
Ce composant intercepte n‟importe quelle requête avec une URL commençant par
« datasnap », transmise au support HTTP de DataSnap. Pour les requêtes commençant par
« datasnap » et indiquant un chemin d‟accès « rest », le traitement sera redirigé sur le
moteur REST intégré. En d‟autres termes, les requêtes ayant un chemin d'accès
« datasnap/rest » sont considérées comme des requêtes REST.
S‟agissant de deux chaînes, il est possible de les modifier pour prendre en compte
différentes URL, comme il est expliqué plus loin dans ce document. Mais pour commencer,
nous examinerons les paramétrages standards.
Les deux autres composants du module de données Web constituent la base globale de
DataSnap et indiquent quelle classe répondra aux requêtes (ou quelles classes, en cas
d‟ajout de composants DSServerClass multiples). Les paramètres par défaut sont les
suivants :
object DSServer1: TDSServer
AutoStart = True
HideDSAdmin = False
end
object DSServerClass1: TDSServerClass
OnGetClass = DSServerClass1GetClass
Server = DSServer1
LifeCycle = 'Session'
end
Le composant DSServer ne demande qu‟à être démarré (manuellement ou
automatiquement) ; en revanche, la configuration DSServerClass a essentiellement lieu dans
le gestionnaire d‟événement retournant la classe cible. Le code par défaut (généré par
l‟assistant), qui retourne la classe du module de données secondaire (TServerMethods1)
hébergé par l‟unité correspondante (Fsrs_ServerClass) est le suivant :
procedure TWebModule2.DSServerClass1GetClass(
DSServerClass: TDSServerClass;
var PersistentClass: TPersistentClass);
begin
PersistentClass := Fsrs_ServerClass.TServerMethods1;
end;
Finalement, le module Web fournit une réponse HTTP par défaut à toutes les autres actions
en retournant un simple code HTML :
procedure TWebModule2.WebModule2DefaultHandlerAction(Sender:
TObject;
Request: TWebRequest; Response: TWebResponse; var Handled:
Boolean);
begin
Response.Content := '<html><heading/><body>' +
'DataSnap Server</body></html>';
end;
Actions = <
item
Default = True
Name = 'DefaultHandler'
PathInfo = '/'
OnAction = WebModule2DefaultHandlerAction
end>
CLASSE SERVEUR EXEMPLE GÉNÉRÉE PAR L‟ASSISTANT
DATASNAP WEBBROKER
La troisième unité générée par l‟assistant DataSnap WebBroker est la classe serveur
exemple – celle qui présente les méthodes invoquées à distance via REST. Il s‟agit de la
classe Delphi connectée au composant DSServerClass via le gestionnaire d‟événements
DSServerClass1GetClass mentionné précédemment, qui récupère l‟essentiel du code réel.
La classe « squelette » ainsi générée est très simple et déterminée par le fait que des
exemples de méthodes sont demandés dans l‟assistant. En voici le code :
type
TServerMethods1 = class(TDSServerModule)
private
{ Private declarations }
public
http://localhost:8081/FirstSimpleRestServer.FirstSimpleRestSe
rver
Pour rappel, il s‟agit là du HTML retourné par le programme pour l‟action standard – ce qui,
bien sûr, n‟est pas d‟un grand intérêt...
L‟étape suivante consiste à utiliser l‟URL spécifique pour la seule requête exécutable par le
serveur REST – l‟invocation de la méthode EchoString du TServerMethods1 du support
« rest » du serveur « datasnap ». L‟URL est automatiquement combinée avec ajout du
préfixe du serveur REST (/datasnap/rest, par défaut), du nom de classe, du nom de méthode
et des paramètres de méthode :
/datasnap/rest/TServerMethods1/EchoString/hello%20world
Dans l‟URL, « %20 » remplace simplement un espace (il est également possible de saisir un
espace dans le navigateur). La réponse JSON du serveur REST apparaîtra alors sous la
forme suivante :
Pendant l'exécution de ces tests, il est possible d'utiliser le Débogueur d‟application Web
pour déterminer quelles requêtes et réponses HTTP doivent effectivement être transférées.
La page illustrée ci-dessus est générée par une requête de navigateur :
GET /FirstSimpleRestServer.FirstSimpleRestServer/
datasnap/rest/TServerMethods1/EchoString/hello%20world
HTTP/1.1
Host: localhost:8081
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US)
AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27
Safari/532.0
Accept:
application/xml,application/xhtml+xml,text/html;q=0.9,
text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Encoding: gzip,deflate,sdch
Cookie:
LastProgID=FirstSimpleRestServer.FirstSimpleRestServer
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Comme mentionné précédemment, la simplicité d‟accès à ces informations peut s‟avérer très
utile pour déboguer les applications HTTP.
Une autre option, présentant plus d'intérêt, consiste à extraire les informations réelles de la
structure de données JSON retournée par le serveur. Une approche manuelle pourrait être
adoptée (comme il est montré précédemment), mais dans ce cas, il peut être préférable de
profiter du support JSON disponible dans Delphi 2010.
Ce nouveau support JSON de Delphi est proposé via une série de classes définies dans
DBXJSON, qui – malgré son nom – peut aussi être utilisé dans les applications non liées au
framework dbExpress. L‟unité DBXJSON définit des classes utilisables pour manipuler
différents types de données JSON (valeurs individuelles de différents types, réseaux, objets,
etc.). Ceci est très utile pour personnaliser le résultat des applications serveur (comme nous
le verrons dans le projet suivant) et pour lire les données reçues par un client, comme dans
le cas qui nous intéresse.
Les données JSON retournées par le serveur sont présentées sous forme de chaîne, mais le
support serveur REST crée un objet associé à une valeur nommée (ou « paire ») et place la
valeur réelle dans une gamme. C‟est la raison pour laquelle, après analyse du résultat HTTP
dans une structure de données JSON, il est nécessaire de naviguer de l‟objet à la paire qu'il
contient - et de la paire jusqu‟à l‟élément unique qu‟elle contient :
procedure TFormFirstRestClient.btnToJSONClick(Sender:
TObject);
var
strParam, strHttpResult, strResult: string;
jValue: TJSONValue;
jObj: TJSONObject;
jPair: TJSONPair;
jArray: TJSOnArray;
begin
strParam := edInput.Text;
strHttpResult := IdHTTP1.Get(strServerUrl +
strMethodUrl + strParam);
jValue := TJSONObject.ParseJSONValue(
TEncoding.ASCII.GetBytes(strHttpResult), 0);
if not Assigned (jValue) then
begin
ShowMessage ('Error in parsing ' + strHttpResult);
Exit;
end;
try
jObj := jValue as TJSONObject;
jPair := jObj.Get(0); // Obtention de la seule et unique
paire JSON
jArray := jPair.JsonValue as TJsonArray; // La valeur de
paire est un array
strResult := jArray.Get(0).Value; // Premier et seul
élément d’Array
ShowMessage ('The response is: ' + strResult);
finally
jObj.Free;
end;
end;
Là encore, la complexité est due à la structure de données retournée par le serveur, puisque
dans d‟autres circonstances, il serait bien plus simple d‟analyser les données JSON
résultantes et d‟y accéder.
APPEL DU SERVEUR REST A PARTIR D‟UNE APPLICATION
WEB AJAX
Pour un simple transfert d‟objet d‟une application Delphi serveur à une autre, il existe de
nombreuses alternatives à l‟utilisation de JSON – approche particulièrement adaptée pour
appeler le serveur Delphi compilé à partir d‟une application JavaScript exécutée dans le
navigateur. Ce cas d‟utilisation est très pertinent dans la mesure où la technologie AJAX
(appels JavaScript asynchrones effectués dans le navigateur Web) a été – et demeure – l‟un
des éléments moteur de l‟adoption REST. L‟appel d‟un serveur SOAP correspondant à l‟aide
d‟un programme basé sur navigateur est largement plus complexe.
Comment donc créer une application reproduisant le client précédemment développé tout en
étant exécutée dans le navigateur Web ? De multiples méthodologies et bibliothèques
pourraient être utilisées, mais l‟option privilégiée à ce stade est celle de la bibliothèque open-
source JavaScript jQuery, disponible sur :
http://jquery.com
Une présentation approfondie de jQuery et de ses modes d‟utilisation serait trop longue pour
ce livre blanc, mais nous expliquerons néanmoins le code jQuery sous-jacent de cet
exemple précis. Tout d‟abord, la page HTML inclut jQuery et son support JSON :
<head>
<title>jQuery and Delphi 2010 REST</title>
<script
src="http://jqueryjs.googlecode.com/files/jquery-
1.3.2.min.js"
type="text/javascript"></script>
<script
src="http://jquery-json.googlecode.com/files/jquery.json-
2.2.min.js"
type="text/javascript"></script>
</head>
Par ailleurs, la page a une interface utilisateur très simple, avec du texte, un champ d'entrée
et un bouton (sans fichiers CSS sophistiqués et graphiques ajoutés – l‟objectif étant de se
concentrer sur le point qui nous intéresse ici) :
<body>
<h1>jQuery and Delphi 2010 REST</h1>
<br/>
<input type="text" id="inputText" size="50"
value="This is a message from jQuery">
<br/>
<input type="button" value="Echo" id="buttonEcho">
$("#inputText")
Cette opération retourne un objet jQuery encapsulant l‟élément DOM de texte d‟entrée. Pour
définir un gestionnaire d‟événements, il est possible de transmettre un paramètre de
méthode anonyme à la fonction click() du bouton. Deux nouvelles invocations sont
effectuées : l‟invocation REST à proprement parler (à partir des données getJSON globales)
et l‟invocation htmlI() pour ajouter le résultat à la page HTML de l‟élément de sortie.
Voici la ligne complète du code sous-jacent de cette démonstration – un snippet de code
JavaScript très compact mais peu lisible :
$(document).ready(function() {
$("#buttonEcho").click(function(e) {
$.getJSON("http://localhost:8081/"
"FirstSimpleRestServer.FirstSimpleRestServer/"
"datasnap/rest/TServerMethods1/EchoString/" +
$("#inputText").val(),
function(data) {
$("#result").html(data.result.join(''));
} );
});
});
En ouvrant simplement un fichier HTML avec le code donné, il est possible d‟appeler le
serveur personnalisé – mais uniquement si les paramètres d'autorisation navigateur
permettent un appel AJAX à partir d‟un fichier local et à destination d‟un serveur REST local.
La plupart des navigateurs n‟autorisent les invocations de serveurs REST que sur le même
site que celui dont émane la page HTML.
Dans tous les cas, Internet Explorer semble bien fonctionner sur ce fichier local, après
activation des scripts locaux et demande de sécurité limitée (option disponible puisque le
fichier se trouve sur la machine locale ; voir les icônes de la barre d'état) :
Sur les autres navigateurs, le serveur Web doit nécessairement retourner la page HTML et
les données REST – ce qui ne présente pas de difficulté particulière, puisque le serveur
REST est, en fait, un serveur Web. Ainsi, pour une « solution serveur » (sous l‟angle du
navigateur Web) il suffit d‟ajouter une action au module de serveur Web (qu‟on accrochera à
l‟URL « /file ») et de retourner un fichier HTML à partir de celui-ci :
procedure TWebModule2.WebModule2DefaultHandlerAction(Sender:
TObject;
Request: TWebRequest; Response: TWebResponse; var Handled:
Boolean);
var
strRead: TStreamReader;
begin
strRead := TStreamReader.Create('jRestClient.html');
try
Response.Content := strRead.ReadToEnd;
finally
strRead.Free;
end;
end;
Il est maintenant possible de se référer à une page serveur donnée avec l‟URL /file, d‟obtenir
le fichier avec le code JavaScript et de lui faire appeler le serveur REST :
La différence avec l'image précédente ne tient pas uniquement à l'utilisation d'un navigateur
différent – l‟URL désignée est également différente. Ce second cas ne repose pas sur le
chargement d‟un fichier, mais utilise l‟application REST serveur comme serveur Web
complet, en retournant le code HTML utilisé pour invoquer ce même serveur via AJAX.
type
TMyData = class (TPersistent)
public
Name: String;
Value: Integer;
public
constructor Create (const aName: string);
end;
Les objets sont conservés dans un dictionnaire et implémentés à l‟aide de la classe de
conteneur générique TObjectDictionary<TKey,TValue> définie dans l‟unité
Generics.Collections depuis Delphi 2009. Cet objet global est initialisé lors du démarrage du
programme avec l‟ajout d‟une paire d'objets prédéfinis. Nous remarquerons que l‟ajout des
objets repose sur une procédure AddToDictionary spécifique pour garantir que le nom
d‟objet est conforme à la clé de dictionnaire et reçoit une valeur aléatoire, si aucune n‟est
fournie :
var
DataDict: TObjectDictionary <string,TMyData>;
md.Value := nVal;
DataDict.Add(aName, md);
end;
initialization
DataDict := TObjectDictionary <string,TMyData>.Create;
AddToDictionary('Sample');
Une fois cette structure de données implémentée, il est temps d‟aborder les deux premières
méthodes exemple utilisées pour retourner les valeurs JSON. La première retourne la valeur
de l‟objet donné (en choisissant une valeur par défaut si aucun paramètre n‟est transmis à la
fonction) :
Si nous utilisons une URL avec ou sans le paramètre (comme dans les deux lignes
suivantes) :
/datasnap/rest/TObjectsRest/PlainData/Test
/datasnap/rest/TObjectsRest/PlainData
nous obtiendrons tout de même une réponse JSON, pour l‟objet spécifique ou pour un objet
par défaut :
{"result":[8978]}
Comment retourner un objet complet et non une valeur spécifique ? Le serveur REST ne
peut pas retourner une valeur TObject car le système n‟est pas capable de le convertir
automatiquement ; néanmoins, il peut utiliser le nouveau support de conversion JSON pour
convertir un objet existant en format JSON :
Cette approche est surtout utile pour recréer l‟objet dans l‟application cliente Delphi – et ne
présente pas grand intérêt dans les cas où le client est développé dans un autre langage. Le
code JSON correspondant est assez lourd :
{"result":[{
"type":"ObjectsRestServer_Classes.TMyData",
"id":1,
"fields": {
"Name":"Test",
"Value":8068}
}]}
Quelle est donc la meilleure option pour retourner un objet JSON ? Nous recommanderons
probablement d'en créer un au niveau serveur à l‟aide de classes de support – comme nous
l‟avons fait dans la fonction MyData :
function TObjectsRest.MyData(name: string): TJSONObject;
var
md: TMyData;
begin
md := DataDict[name];
Result := TJSONObject.Create;
Result.AddPair(
TJSONPair.Create ('Name', md.Name));
Result.AddPair(
TJSONPair.Create ('Value',
TJSONNumber.Create(md.Value)));
end;
Nous avons donc créé un code TJSONObject et ajouté deux paires de propriétés pour le
nom et la valeur. Nous aurions pu utiliser un nom dynamique (c‟est-à-dire la désignation du
nom faisant partie de la paire), mais cela aurait complexifié la récupération des données sur
le client. Le résultat de ce code retournera un code JSON plus net, semblable à l‟exemple
suivant :
{"result":[{
"Name":"Test",
"Value":8068
}]}
<script>
var baseUrl = "/ObjectsRestServer.RestObjects/dataSnap" +
"/rest/TObjectsRest/";
$(document).ready(function() {
$.getJSON(baseUrl + "MyData/Sample",
function(data) {
strResult = data.result[0].Value;
$("#sample").html(strResult);
} );
L‟invocation AJAX de MyData transmet le nom d‟objet comme nouveau paramètre URL et
extrait du groupe de résultat la valeur nommée propriété/paire en l‟affichant dans un élément
HTML avec espace vide. Un processus similaire (bien qu'un peu plus complexe) a lieu pour
la liste. Il s‟agit là encore d‟un appel AJAX, mais cette fois, le code HTML résultant doit être
développé. L'opération est exécutée dans une fonction refreshList séparée, appelée
automatiquement au démarrage et manuellement par l‟utilisateur :
function refreshList()
{
$.getJSON(baseUrl + "list",
function(data) {
thearray = data.result[0];
var ratingMarkup = ["<br>"];
for (var i=0; i < thearray.length; i++) {
ratingMarkup = ratingMarkup +
"<a href='#'>" + thearray[i] +
"</a><br>";
}
$("#list").html(ratingMarkup);
} );
};
Le code utilise une boucle « pour » pour vérifier le groupe généré. Nous aurions pu utiliser le
mécanisme d‟énumération $.each de jQuery, mais cela aurait complexifié la lecture du code.
La boucle crée le code HTML, qui s‟affiche ensuite dans l‟emplacement espace avec l‟ID
donnée. Il s‟agit d‟une sortie individuelle incluant la valeur de l‟objet « Sample » (le code
décrit précédemment) et la liste des valeurs retournées dans le groupe JSON :
$(document).ready(function() {
refreshList();
$("#refresh").click(function(e) {
refreshList();
} );
Quelques opérations restent encore pour générer le code… Dès que le code HTML est prêt
pour la liste (il s‟agit d‟une liste de liens), il doit être « accroché » à ces liens pour que
chaque entrée de la liste dans l‟application cliente, lorsqu‟elle est sélectionnée, entraîne le
chargement de l‟objet correspondant au niveau serveur. L‟interface utilisateur des données
objet est constituée de deux boîtes d‟entrée, qui seront ultérieurement utilisées pour
manipuler les données objet. Le comportement est ajouté à chaque point d‟ancrage dans le
conteneur de liste.
$("#list").find("a").click(function(e) {
var wasclicked = $(this);
$.getJSON(baseUrl + "MyData/" + $(this).html(),
function(data) {
strResult = data.result[0].Value;
$("#inputName").val(wasclicked.html());
$("#inputValue").val(strResult);
} );
});
Notons l‟utilisation de l‟appel $(this), qui constitue plus ou moins le paramètre Émetteur pour
un événement Delphi. Le contenu html de l‟élément sélectionné est son texte, qui est le nom
de l‟élément à transmettre au serveur dans l‟URL avec l‟expression :
Après avoir développé ce code, on pourra observer l‟action produite lorsque l‟on clique sur
l‟un des éléments de la liste : une nouvelle invocation AJAX est effectuée à destination du
serveur pour demander une valeur donnée et la valeur retournée s‟affiche dans deux boîtes
d‟entrée texte :
Comme le montre cette illustration, le programme permet de récupérer une valeur, mais
comporte également trois boutons pour exécuter les opérations les plus communes
(Création/Lecture/Mise à jour/Suppression). Ces actions sont supportées en HTML à l‟aide
des 4 méthodes HTML de code (respectivement, PUT, GET, POST et DELETE). Dans le
chapitre suivant, nous verrons comment ces méthodes sont supportées par un serveur
REST Delphi 2010.
POST, PUT ET DELETE
Jusqu‟à présent, nous avons vu comment obtenir des données du serveur REST. Mais qu‟en
est-il de leur mise à jour ? L‟idée généralement admise pour l‟environnement REST est qu‟il
faut éviter d‟utiliser des URL spécifiques pour identifier les opérations et qu‟il est préférable
de n‟en utiliser qu‟une seule pour identifier les objets serveur (comme MyData/Sample dans
le cas développé ici) et d‟utiliser les méthodes HTTP pour indiquer les actions à exécuter.
Il serait regrettable que le support REST de Delphi se contente d'associer les URL aux
méthodes ; mais au contraire, il relie non seulement les URL, mais également la méthode
HTTP aux méthodes selon un schéma relativement simple : le nom de l‟opération est
initialement associé au nom de méthode à l‟aide du mappage suivant :
GET obtention (par défaut, peut être omis)
POST mise à jour
PUT acceptation
DELETE annulation
Ces mappages peuvent être personnalisés en manipulant les quatre gestionnaires
d‟événements correspondants du composant DSHTTPWebDispatcher. En cas de choix des
règles de nommage standards, le support des différentes opérations exigera de définir la
classe serveur comme suit :
type
TObjectsRest = class(TPersistent)
public
function List: TJSONArray;
function MyData (name: string): TJSONObject;
procedure updateMyData (name, value: string);
procedure cancelMyData (name: string);
procedure acceptMyData (name, value: string);
end;
Pour obtenir ou supprimer un élément, seul le nom est nécessaire ; en revanche, la création
ou la mise à jour d‟un élément exigera un second paramètre, en plus des données.
L‟implémentation des trois nouvelles méthodes est assez simple et directe, notamment parce
qu‟elles n‟ont pas à retourner une valeur (il va sans dire qu‟on aura vérifié au préalable que
les paramètres ne sont pas vides et que l'objet serveur existe réellement) :
procedure TObjectsRest.updateMyData (name, value: string);
begin
DataDict[name].Value := StrToIntDef (Value, 0);
end;
$("#buttonUpdate").click(function(e) {
$.ajax({
type: "POST",
url: baseUrl + "MyData/" +
$("#inputName").val() + "/" +
$("#inputValue").val(),
success: function(msg){
$("#log").html(msg);
}
});
});
La suppression est tout aussi simple, exigeant uniquement de créer l‟URL avec la référence
à l‟objet à supprimer :
$("#buttonDelete").click(function(e) {
$.ajax({
type: "DELETE",
url: baseUrl + "MyData/" +
$("#inputName").val(),
success: function(msg){
$("#log").html(msg);
}
});
});
Cette méthode est invoquée par l‟application cliente après le chargement de la page, à
travers la construction dynamique d'une table HTML avec le code jQuery suivant (semblable
au code amplement développé dans les chapitres précédents) :
$(document).ready(function() {
$.getJSON(
"/DataRestServer.RestDataServer/datasnap/rest/TServerDa
ta/Data",
function(data) {
thearray = data.result[0];
var ratingMarkup = "<table border='1'>";
for (var i=0; i < thearray.length; i++) {
for I := 0 to ClientDataSet1.FieldDefs.Count - 1 do
Result.Add(ClientDataSet1.FieldDefs[I].Name);
end;
Le JavaScript au niveau client a été étendu par une seconde invocation du serveur REST
pour obtenir les métadonnées :
$.getJSON(
"/DataRestServer.RestDataServer/datasnap/rest/TServerDa
ta/Meta",
function(data) {
theMetaArray = data.result[0];
Ces informations sont utilisées pour créer l‟entête de table et accéder dynamiquement aux
propriétés d‟objet, avec l'objet de notation [“propertyname”]. Dans le cas qui nous intéresse,
le code existant utilisé pour accéder à un objet avec le symbole de propriété :
thearray[i].Company
devient le code ci-dessous, qui lit la propriété par nom, en utilisant le nom du champ
enregistré dans les métadonnées :
thearray[i][theMetaArray[j]].
// content
for (var i=0; i < thearray.length; i++) {
ratingMarkup = ratingMarkup + "<tr>";
for (var j=0; j < theMetaArray.length; j++) {
ratingMarkup = ratingMarkup + "<td>" +
thearray[i][theMetaArray[j]] + "</td>";
};
ratingMarkup = ratingMarkup + "</tr>";
}
ratingMarkup = ratingMarkup + "</table>";
Là encore, cet exemple n‟offre qu‟une base de départ et n'utilise aucune des extensions
jQuery, qui ajouteraient des fonctionnalités significatives aux tables HTML en les
transformant en puissantes grilles d‟interface utilisateur avec des outils de tri, de filtrage et
d‟édition.
CONCLUSION
Ce livre blanc ne fait qu‟effleurer l‟architecture REST et le développement d‟applications
REST client et serveur avec Delphi 2010. Il fournit un aperçu très rapide des technologies
associées (XML, JSON, XPath, JavaScript, jQuery) qui peuvent être nécessaires pour
devenir expert des architectures REST.
Étant donné le nombre croissant de serveurs REST publics, l‟émergence du cloud-
computing et l‟intérêt grandissant pour l‟hébergement d‟applications Web, Delphi pourrait
jouer un rôle significatif dans le développement de riches clients d‟interface appelant des
serveurs distants et dans le développement de serveurs réels de manipulation de structures
de données dans une application cliente (développée dans n‟importe quel langage) ou
directement dans le navigateur.
Comme l‟ont montré les démonstrations finales, la combinaison de JavaScript et d‟un
serveur REST Delphi permet d‟utiliser l‟EDI Embarcadero pour développer des applications
Web professionnelles, modernes et de qualité optimale.
A PROPOS DE L‟AUTEUR
Marco est l‟auteur du best-seller « Mastering Delphi » et a publié, au cours des dernières
années, des ouvrages sur les dernières versions de Delphi, notamment « Delphi 2007
Handbook », « Delphi 2009 Handbook » et le « Delphi 2010 Handbook » (actuellement en
cours de rédaction).
Parallèlement à ses prestations de formation et de consulting sur Delphi, Marco propose des
services de consulting sur les architectures Web et l‟intégration Web des projets Delphi –
sous l‟angle de l‟invocation d‟autres serveurs et de leur mise à disposition.
Vous pouvez consulter le blog de Marco sur http://blog.marcocantu.com, échanger avec lui
sur Twitter sur http://twitter.com/marcocantu ou le contacter par email à marco.cantu
@gmail.com.
Marco souhaite remercier Daniele Teti pour sa contribution à l‟élaboration de ce livre blanc,
tout particulièrement sur le support JSON de Delphi 2010.
Embarcadero Technologies, Inc. est un éditeur leader de solutions primées pour les
développeurs d‟applications et les professionnels des bases de données pour les aider à
concevoir et exécuter plus efficacement et rapidement leurs solutions – quels que soient les
langages de programmation ou les plates-formes. 90 des 100 premières sociétés du
classement Fortune et une communauté active de plus de 3 millions d‟utilisateurs dans le
monde font confiance aux outils Embarcadero pour maximiser leur productivité, réduire les
coûts, simplifier la gestion du changement et de la conformité et accélérer l‟innovation. Ses
produits les plus emblématiques sont notamment : Embarcadero® Change Manager™,
Embarcadero™ RAD Studio, DBArtisan®, Delphi®, ER/Studio®, JBuilder® et Rapid SQL®.
Créé en 1993, le siège d‟Embarcadero se situe à San Francisco ; l‟entreprise est présente
dans le monde entier et son site Web peut être consulté à l‟adresse www.embarcadero.com