We apologize for untranslated text, you can use the Google Translation button to get an automatic translation of the web page in the language of your choice.

Services Web RESTful avec JCMS Open API

1. L'interopérabilité

1.1 Problématique

Les systèmes d'informations d'entreprises reposent sur des parcs aux technologies hétérogènes (JavaEE, .NET, PHP, Ruby on rails). Cela rend le besoin d'interopérabilité de plus en plus indispensable. Jusqu'à présent, il a été possible de répondre à ces besoins avec JPlatform en effectuant des développements spécifiques côté JPlatform (JSP ou servlets), appelés de manière distante (en HTTP) ou l'inverse.

JPlatform propose, à partir de la version 6, une API de Services Web native, de manière à :

  • offrir une interface indépendante du langage avec lequel l'API est invoquée ;

  • industrialiser les développements : faire reposer les développements sur un socle solide et maintenu ;

  • pouvoir réaliser des composants réutilisables : par exemple des modules d'intégrations entre plusieurs applications JPlatform.

1.2 Les solutions technologiques

Plusieurs normes d'interopérabilité pourraient être utilisées, parmi ces normes les plus fréquemment citées, on trouve :

  • JSR-170 (et sa version plus récente, JSR-283) : il s'agit de deux normes permettant d'accéder à un Content repository, c'est-à-dire à un système de persistance de gestion de contenu. L'interface définie par cette norme est en Java, et donc il n'y a pas indépendance par rapport au langage des applications tierces. Par ailleurs, les services que nous souhaitons proposer vont au delà de la seule gestion des contenus ;

  • JSR-168 (et sa version plus récente, JSR-286) : ces deux normes permettent qu'un portail conteneur de portlets, au sens de ces normes, présentent des portlets développées par ailleurs. Il s'agit, là encore, d'interopérabilité limitée au seul langage Java. L'étendue de l'interface est limitée aux portlets et ne permet pas d'interaction applicative ;

  • WSRP : cette norme a essentiellement le même objectif que celui des normes JSR-168 et 286, mais l'interface n'est pas en Java, mais en services web SOAP. Sa vocation est limitée à l'interopérabilité de portlets ;

  • Services Web SOAP : SOAP est une norme permettant de faire des appels de procédures à distance, de manière indépendante du protocole de transport sous-jacent. Lorsque cette norme est utilisée en corrélation avec HTTP, on parle de services web SOAP. Utiliser et développer des services web SOAP nécessite de la part des développeurs de disposer d'ateliers de développement (AGL ou outils RAD) supportant cette norme. Dans la pratique, les services Web SOAP sont principalement utilisés dans des réseaux locaux, car ils sont très verbeux (ils utilisent beaucoup de bande passante) et ils peuvent être utilisés, par exemple, en conjonction avec des EAI fonctionnant en services web SOAP (on parle alors d'ESB) ;

  • Services Web REST : il s'agit de services web basés sur HTTP, reposant sur l'utilisation pertinente des possibilités et du sens de chaque élément de la norme et des champs des requêtes et réponses HTTP.

Nous avons choisi de développer une architecture de services Web REST, pour des raisons pragmatiques :

  • c'est le format d'interopérabilité qui est massivement adopté par les services web sur Internet, et qui est utilisé pour réaliser des mashups (Services Web Google Map, Amazon S3, Flickr, …) ;

  • il est moins lourd au niveau développement de développer de tels services web que des services web SOAP ;

  • utiliser des services web REST plutôt qu'un format entièrement spécifique nous a paru préférable pour plusieurs raisons techniques que nous développeront dans le chapitre suivant, mais également parce qu'ils sont aujourd'hui massivement utilisés sur Internet, et donc il est facile pour des développeurs web de s'y adapter. Par ailleurs, le fait d'utiliser des services web REST n'induit pas de restrictions fonctionnelles.

Une nouvelle norme d'interopérabilité, CMIS, est disponible. Il s'agit d'une norme d'interopérabilité pour les outils de gestion de contenu, élaboré par différents éditeurs de logiciels de gestion de contenu.

Sa prise en compte est le sujet du module CMIS Server.

2. Services Web RESTful

REST n'est pas strictement un protocole, mais plutôt un ensemble de principes à respecter pour fournir une API de service web HTTP. On dit d'une API de services web respectant ces principes qu'elle est RESTful ou orientée REST. On parle aussi d'Architecture Orientée Ressources (ROA pour Resources Oriented Architecture).

L'architecture orientée REST comprend quatre concepts et quatre principes.

Les quatre concepts :

  1. Les ressources : ce sont les objets que l'on gère dans une API REST ;

  2. Les URI (nom) des ressources : une URI est le moyen universel d'identifier une ressource ;

  3. Les représentations : une ressource peut avoir plusieurs représentations (comme : HTML, XML, ATOM, CSV, …), qui est déterminée, en HTTP, par la ou les valeurs dans le champ Accept de la requête ;

  4. Les liens entre les ressources, par l'intermédiaire des URI présentes dans les représentations.

Les quatre principes :

  1. l'adressabilité des ressources : une ressource doit être adressable par une URI qui lui est propre ;

  2. les ressources sont sans-état : c'est-à-dire que si on fait deux fois la même requête sur une ressource qui n'a pas évoluée, la réponse est la même. Le client qui utilise ces ressources peut par contre maintenir un état ;

  3. connexité des ressources il y a un moyen d'aller d'une ressource à l'autre : les représentations des ressources contiennent des URI vers les autres ressources ;

  4. l'interface uniforme : l'action d'une requête est donnée principalement par la méthode HTTP (GET, POST, PUT, …).

Dans la pratique :

  • On conçoit un modèle accès sur des ressources. Les ressources qui correspondent à des objets métiers sont les plus faciles à modéliser, puis au besoin, on conçoit d'autres ressources plus conceptuelles, comme celles qui représentent des transactions, voire toute l'application. Dans le cas de JPlatform, les objets de l'application (publications, membres, catégories, ...) peuvent être vus comme des ressources;

  • Il y a également des ressources qui ne correspondent pas à ces données, comme par exemple une requête de recherche ;

  • Ces ressources doivent être sans état, c'est-à-dire qu'une requête doit comprendre toutes les informations nécessaires à son traitement. Il n'y a pas d'information d'état en session côté serveur. Il peut, par contre, y avoir des états et une notion de session côté client, mais cela ne passe pas dans les requêtes ;

  • On réalise une conception de template URI, c'est-à-dire d'URI paramétriques pour déterminer l'ensemble des URI de ressources de même nature. Par exemple, dans JPlatform Open API, l'URI représentant un contenu d'identifiant c_4000 est :

    • [/<context-path>]/rest/data/c_4000

  • Le template URI est alors :

    • [/<context-path>]/rest/data/{id}

  • L'interface uniforme implique que la méthode HTTP d'une requête indique la nature de l'opération sur la ressource :

    • GET : récupération d'une représentation d'une ressource ;

    • PUT : création ou modification de la ressource que l'on indique dans l'URI ;

    • DELETE : suppression de la ressource indiquée dans l'URI ;

    • POST : autres type d'actions. On utilise cette méthode, en particulier, pour créer une ressource pour laquelle (et c'est le cas en général) c'est le serveur qui détermine l'URI. C'est le cas, par exemple, pour les créations d'objets JPlatform, puisque c'est le serveur qui détermine l'identifiant, alors que pour la création d'un Workflow, par exemple, c'est le client qui détermine le nouveau nom. Dans le cas où une création de ressource est faite suite à un POST, l'URL de la nouvelle ressource est indiquée dans l'en-tête de réponse Location.

Dans JPlatform, dans le cas d'une création de donnée, la méthode utilisée est toujours POST. En effet, c'est le serveur qui va déterminer l'identifiant d'une nouvelle donnée, donc le client ne peut connaître a priori l'URI de la ressource correspondant à la donnée après sa création, et donc on n'est pas dans les conditions de l'usage de la méthode PUT.

Les principes de base de l'architecture REST ont été présentés pour la première fois dans la thèse de Roy Fielding (en anglais), qui est l'un des concepteurs du protocole HTTP.

L'ouvrage RESTful Web Services (en anglais) présente de façon plus détaillée ces concepts et principes.

Parmi les avantages de respecter ces principes REST (plutôt que d'utiliser des formats spécifiques) on trouve :

  1. des structures d'URI et des formats de retour compréhensibles pour un utilisateur humain ;

  2. des technologies utilisées ne nécessitant pas un environnement technique complexe pour le développement ;

  3. une bande passante moins sollicitée (pas de corps de requête pour une consultation, pas d'enveloppe SOAP dans les résultats, et représentation indiquée dans la requête) ;

  4. un meilleur support des caches des requêtes (puisque les ressources sont sans-état) ;

  5. ce type de service web est de plus très répandu, en particulier chez les grands acteurs du Web 2.0 (Amazone, Google, Yahoo !, ...) qui proposent des services pour réaliser des mashups. Il est donc connu des développeurs Web.

 

3. JPlatform OpenAPI

3.1 Principes

JPlatform Open API est une API de services Web qui repose sur les protocoles HTTP et HTTPS. Cette API respecte les principes d'une architecture orientée ressources. Elle est extensible par ajout de ressources dans des modules JPlatform.

En complément, Jalios fournit JPlatform Open API Client qui est une bibliothèque Java facilitant le développement de clients pour JPlatform Open API.

Une description des ressources disponibles est présente dans la documentation interne de JPlatform : Espace d'administration > Documentation > Open API.

 

Fig. 1. Lien vers la documentation de l'Open API

La description précise de l'API et les exemples dans cet article sont valables à partir de JPlatform 6.0 SP1.

3.2 Architecture générale

JPlatform Open API est basée sur le framework Restlet.

Les requêtes de JPlatform Open API sont servies par la RestletServlet, qui écoute sur le préfixe /rest. Elle prend donc en compte les requêtes de la forme :

  • <url de base du site>/rest/<suffixe propre à la ressource>

Lorsqu'une requête est adressée à JPlatform, la chaîne de traitement suivante est déclenchée :

  • la requête passe par la servlet filter InitFilter, de la même manière que pour toute requête attaquant l'application JPlatform. C'est durant ce traitement que l'authentification est déterminée ;

  • la RestletServlet prend en compte cette requête ;

  • elle vérifie quelle est la politique d'ouverture de l'OpenAPI (cf. Administration des services OpenAPI) et si la requête est conforme à cette politique ;

  • elle regarde si une ressource est associée au suffixe propre à la ressource :

    • parmi les ressources disponibles nativement dans JPlatform ;

    • parmi les ressources ajoutées dans les modules ;

  • elle instancie la classe associée au suffixe de la ressource ;

  • en fonction de ce qui est déterminé dans le constructeur et de la méthode HTTP de la requête, telle ou telle méthode de la classe est appelée.

3.3 Administration des services OpenAPI

Par défaut, l'accès aux ressources de l'OpenAPI est interdit. L'administration de son activation et sa configuration se trouvent dans : Espace d'administration > Propriétés > Onglet Services Web > Open API.

 

Fig. 2. Administration de l'accès à JPlatform Open API

Il est possible de :

  • activer/désactiver l'accès à Open API de manière générale ;

  • activer/désactiver l'accès à Open API, en ce qui concerne la lecture (c'est-à-dire les requêtes HTTP avec la méthode GET) ;

  • mettre en place un filtre sur les adresses IP pour l'accès en lecture ;

  • activer/désactiver l'accès à Open API, en ce qui concerne les opérations (c'est-à-dire les requêtes HTTP avec autre chose que la méthode GET), notamment les écritures et les opérations d'administration ;

  • mettre en place un filtre sur les adresses IP pour les autres requêtes.

Par défaut, Open API est désactivée. Lorsque la lecture est autorisée, elle l'est uniquement pour les adresses IP locales et les opérations sont désactivées.

Cette logique d'administration est prévue pour limiter au maximum par défaut, puis rendre disponible uniquement ce qui est légitime, dans une optique de liste blanche.

3.4 L'API

3.4.1 Gestion des données

Le tableau ci-dessous liste les ressources disponibles pour la gestion de données.

URI templates

Méthodes HTTP

Descriptions

/rest/data/{param}

GET
POST
PUT
DELETE

Cette ressource peut représenter soit une donnée, soit l'ensemble des données d'un type particulier, selon que la valeur de param soit un identifiant (comme j_3455) ou un type de donnée (comme Article ou Member)
Elle offre la possibilité d'utiliser et de gérer le système de gestion de contenus à distance.

Exemples :

/rest/data/j_300

/rest/data/Article

Un fichier peut être déposé pour créer un FileDocument. Il faut alors envoyer la requête en multipart.

/rest/data/children/{id} GET Cette ressource donne accès à l'ensemble des enfants de la donnée d'identifiant "id", enfants au sens TreeNode ou enfants d'un groupe.

/rest/data/Member/{login}

GET

POST

Ressource représentant le membre dont le login est indiqué dans l'URI.
Cette resource est accessible (en GET) pour les administrateurs, et pour un utilisateur connecté, si elle est invoquée avec son propre login.

Utilisée avec la méthode POST, peut effectuer les actions suivantes en fonction du paramètre "action" (de la queryString) :

  • disable : désactive le membre;
  • enable : active le membre nécessite également la présence d'un paramètre password.

/rest/workflows

GET

Représente la liste des Workflows disponibles dans l'application.

/rest/workflows/{wfid}

GET

Représente un Workflow.

Exemple :

/rest/workflows/basic

/rest/types

GET

Représente la liste des types de Publication disponibles dans l'application.

/rest/types/{typeshortclassname}

GET

Représente un type de Publication.

Exemple :

/rest/types/Article

A titre d'exemple, la requête ci-dessous déclenche la création d'un article.

POST /jcms6/rest/data/Article HTTP/1.1
Host: localhost:8080
User-Agent: JaliosJCMS-JavaRestClient/dev (http://www.jalios.com/)
Accept: */*
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cache-Control: no-cache
Pragma: no-cache

author=j_2&title=Title+in+english&title=Titre+en+fran%C3%A7ais&summary=textfieldvalue+1&summary=textfieldvalue+2&content=textareafieldvalue+1&content=textareafieldvalue+2

Dans cette requête :

  • l'URI est la concaténation de :

    • /jcms6 : context path de l'application ;

    • /rest : commun à toute l'Open API ;

    • /data/Article, indique que l'on va travailler sur le type Article.

  • On utilise la méthode POST, puisque l'on travaille sur une ressource dont l'URI sera définie par le serveur (non connue par le client) ;

  • L'en-tête Accept indique le format (type mime) de la représentation souhaitée. En l'occurrence, la requête accepte tout type de représentation, mais uniquement text/xml et application/json (depuis JPlatform 10) sont  disponibles. Si le format de représentation demandé est inexistant, le code réponse suivant est envoyé : HTTP : 406 – Not Acceptable ;

  • L'authentification est réalisée en Basic HTTP ;

  • Les paramètres sont ici dans le corps de la requête, tels qu'envoyés par un formulaire HTML.

Voici la réponse HTTP correspondant à cette requête :

HTTP/1.1 201 Crée
Location: http://localhost:8080/jcms6/rest/data/c_5180

Server: Apache-Coyote/1.1

Content-Type: text/html;charset=UTF-8
Content-Length: 31
Date: Fri, 10 Oct 2008 15:46:26 GMT

Dans cette réponse :

  • Le code 201 indique que la création à bien eu lieu ;
  • L'en-tête Location indique l'URL de l'article créé.

3.4.2 Recherches

La ressource suivante permet d'effectuer des recherches dans JPlatform :

URI templates Méthodes HTTP Descriptions
/rest/search GET Ressource permettant d'effectuer une recherche dans les contenus de l'application en utilisant le format des queryHandler de JPlatform.

Voici le détail HTTP d'une requête OpenAPI, pour rechercher les contenus de type Article :

GET /jcms6/rest/search?types=Article&pageSize=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Cookie: JSESSIONID=ABC02F5C0FC360F510D3DF7A629FAED4

Remarques :

  • Il s'agit d'une requête utilisant la méthode HTTP GET, elle a été obtenue à partir d'un navigateur, en utilisant l'URL :

    • http://localhost:8080/jcms6/rest/search?types=Article&pageSize=10

  • Il n'y a plus de champs correspondant à l'authentification : elle est en session. Laquelle est véhiculée via le cookie JSESSIONID ;

  • Il y a dans la queryString, deux paramètres, appartenant à deux familles de paramètres distinctes :

    • ceux correspondants à la recherche ;

    • ceux qui précisent comment paginer les résultats de la recherche.

Les paramètres de la recherche sont ceux de la recherche classique de JPlatform. Ils correspondent aux attributs du javabean com.jalios.jcms.handler.QueryHandler.

Les paramètres de pagination sont les suivants :

 

Paramètre Effet
pageSize Nombre d'éléments maximum à présenter
start Indice du premier résultat à afficher (en partant de zéro)
sort Critère de tri à appliquer
reverse Indication qu'il faut inverser le tri
pagerAll Indication qu'il ne faut afficher toutes les données

Si le nombre de résultat est supérieur a la valeur définie dans la propriété pager.pager-all-limit (par défaut à 500), alors l'option pagerAllne peut pas être utilisée et il faut paginer. 

La propriété rest.pager.pager-all-limit permet de surcharger la valeur spécifiée dans pager.pager-all-limit pour les requetes REST OpenAPI.

Voici la réponse correspondante (le contenu a été réduit) :

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Date: Wed, 07 Jan 2009 15:39:31 GMT
Server: JCMSOpenAPI/6.0.1
Vary: Accept-Charset, Accept-Encoding, Accept-Language, Accept
Content-Type: text/xml;charset=UTF-8
Content-Length: 96786

<?xml version='1.0' encoding='UTF-8' ?>
<dataset>
<pager [...]>
<data class='generated.Article' id='c_5622' url='http://127.0.0.1:8080/jcms/c_5622/article-52'>
   <field name='title'>Article 52</field>
   <field name='titleML'>
     <entry>
       <key>en</key>
       <value>Article en 52</value>
     </entry>
   </field>
   <field name='categories'>
     <item id='c_5235' class='com.jalios.jcms.Category'>Classification/Keyword/Cat 1/Cat 2/Cat 4</item>
     <item id='c_5123' class='com.jalios.jcms.Category'>Navigation/WS 3 Cat</item>
   </field>
   <field name='pdate'>2007-05-23T18:40:20+02:00</field>
   <field name='udate'>2007-10-09T19:12:57+02:00</field>
   <field name='version' major='1' minor='0'>1.0</field>
   <field name='mainLanguage'>en</field>
   <field name='pstatus'>0</field>
   <field name='workspace' id='c_5122' class='com.jalios.jcms.workspace.Workspace'>Workspace 3</field>
   <field name='templates'>
     <item>default</item>
     <item>query.default</item>
   </field>
   <field name='author' id='c_5165' class='com.jalios.jcms.Member' login='ws3writer1'>WS3 Writer 1</field>
   <field name='opAuthor' id='c_5165' class='com.jalios.jcms.Member' login='ws3writer1'>WS3 Writer 1</field>
   <field name='cdate'>2007-10-09T19:12:57+02:00</field>
   <field name='mdate'>2007-10-09T19:12:57+02:00</field>
   <field name='summary' mlField='summaryML' abstract='true'>Lorem ipsum dolor sit amet</field>
   <field name='summaryML'>
     <entry>
       <key>en</key>
       <value>Lorem ipsum dolor sit amet</value>
     </entry>
   </field>
   <field name='content' mlField='contentML'>Lorem ipsum dolor sit amet</field>
</data>
[...]
<scores max="0.0" >
<data id="c_5622" score="0.0" />
[...]
</scores>
</dataset>

Le contenu correspondant à une balise <data> est l'export XML de cette donnée.

La balise <pager> fournit des informations sur la pagination des résultats.

<pager start="0" pagesize="10" total="137" pagerall="false">
  <authpsize>10</authpsize>
  <authpsize>20</authpsize>
  <authpsize>50</authpsize>
  <authpsize>100</authpsize>
  <authpsize>200</authpsize>
  <authpsize>500</authpsize>
</pager>

Elles indiquent :

  • L'indice de la première donnée (start) : 0 ;

  • Le nombre de données par page (pagesize) : 10 ;

  • Le nombre total d'éléments correspondant à la recherche (total) : 137 (un filtre a été appliqué en fonction des droits de l'utilisateur connecté) ;

  • Le fait que l'affichage de toutes les données ait été sollicité (pagerall) : faux ;

  • La liste des valeurs possibles (autorisées par le serveur) pour le paramètre pageSize (balises <authpsize>) : 10, 20, 50, 100, 200, 500.

Ces informations ne sont pas présentes si toutes les données sont affichées.

La balise <scores> fournit des informations de scoring correspondant à cette recherche. Les informations de scoring ne sont pas dans chaque balise <data>, mais dans une balise séparée, car la pertinence d'une donnée n'est attachée à cette donnée que dans le cadre d'une recherche bien précise.

<scores max="0.0" >
  <data id="c_5622" score="0.0" />
  [...]
</scores>

3.4.3 Administration

La ressource suivante permet de récupérer des informations relative à l'application JPlatform, son état courant, et d'effectuer certains actes d'administration.

URI templates Méthodes HTTP Descriptions
/rest/admin/status GET
POST
Ressource représentant l'application JPlatform courante.
Utilisée avec la méthode GET, renvoie une représentation XML de l'application (identique au résultat de admin/statusXml.jsp).
Utilisée avec la méthode POST, peut effectuer des actions d'administration, en fonction du paramètre "action" (de la queryString).

Les actions d'administration disponibles sont les suivantes :

  • disableDataWrite : désactive les écritures;

  • enableDataWrite : active les écritures;

  • importDataFromSource: réalise l'import de la source de donnée dont l'identifiant est la valeur du paramètre importSourceId.

La liste des actions disponibles sera étendue au fur et à mesure des nouvelles versions de JPlatform.

Un exemple d'utilisation de cette ressource sera décrit plus loin, avec l'outil curl, pour vérifier régulièrement l'état du site.

Note : Cette ressource n'est pas disponible au format JSON.

3.5 Authentification

L'authentification est réalisée de la même manière que pour l'accès à l'interface de JPlatform. Il est donc pertinent de se référer à l'article Authentifications spécifiques avec les AuthenticationHandler. La différence la plus importante se situe au niveau de la réponse en cas de requête effectuée auprès d'une ressource inaccessible pour une raison ou une autre.

Dans l'interface web de JPlatform, si on essaie d'accéder à une ressource interdite ou inexistante, une redirection (code HTTP 303 See Other) est effectuée vers la JSP front/forbidden.jsp, avec l'URL de la ressource demandée en paramètre redirect, de manière à ce qu'il y ait après authentification, une redirection vers la ressource précédemment demandée.

Dans le cas d'une requête OpenAPI, le comportement est différent :

  • si la ressource n'existe pas : le statut de la réponse HTTP est 404 Not Found ;

  • si la ressource n'est pas disponible parce que la méthode n'a pas été activée (au niveau de l'administration de JPlatform Open API) le statut de la réponse est 500 Internal Server Error (parce que c'est du au serveur) ;

  • si la ressource n'est pas disponible parce que l'accès à l'Open API est restreinte au niveau des adresses IP (administration de l'Open API) le statut de la réponse est 403 Forbidden (parce que c'est à cause du client que la ressource n'est pas accessible, et que l'on ne veut pas en expliquer la raison) ;

  • si la ressource est disponible, mais nécessite une authentification et que l'utilisateur n'est pas authentifié, un code de réponse 403 Forbidden est envoyé ;

  • si la ressource est disponible, nécessite une authentification et que l'utilisateur est authentifié mais n'a pas les droits suffisants, un code de réponse 401 Unauthorized est envoyé.

Parmi les authentifications possibles, les deux suivantes sont particulièrement intéressantes dans le cas de l'utilisation de JPlatform Open API.

3.5.1 Authentification Basique HTTP en HTTPS

Le protocole HTTP défini en standard les authentifications Basic et Digest. Cet article du cabinet Hervé Shauer Consultants donne une description précise, exhaustive et en français de ces mécanismes d'authentification, de plus la problématique de la confidentialité des informations d'authentification est abordée.

Le principe de l'authentification basique est que la requête HTTP contient l'en-tête Authorization, qui est de la forme suivante :

Authorization: Basic <encodage en base 64 de "<login>:<mot de passe>" >

Cette authentification n'offre aucune confidentialité pour le login et le mot de passe.

Il est donc pertinent d'utiliser cette méthode d'authentification en conjonction avec HTTPS.

3.5.2 Clé d'authentification

JPlatform 6 offre aux administrateurs la possibilité de générer des clés d'authentifications permettant d'effectuer des requêtes précises au nom d'un utilisateur particulier en positionnant un paramètre appelé authKey. Une clé particulière est définie étant donnés :

  • une URL ou un préfixe d'URL ;

  • l'utilisateur au nom duquel est générée la clé ;

  • la durée de validité ;

  • les méthodes HTTP autorisés ;

  • un filtre d'adresses IP autorisées.

Pour l'administrateur, l'interface de génération de clé d'authentification se trouve dans : Espace d'administration > Exploitation > Génération de clés d'authentification

 

Fig. 3. Génération de clés d'authentification

 

Dans les développements, si l'on connaît précisément les requêtes OpenAPI nécessaires, il est possible de générer les clés d'authentification correspondantes et de ne permettre que les requêtes correspondant à ces requêtes.

Remarque importante : la clé d'authentification doit être positionnée dans la queryString, même si la requête est en POST.

En effet, lorsque l'on utilise un navigateur, les seules requêtes en POST proviennent des formulaires HTML, et les données que l'on soumet dans le corps de la requête représentent alors une table de correspondance de paramètres. En général, cette représentation respecte le format déterminé par le type mime www-url-encoded.

Pour une utilisation plus générale (programmatique) des requêtes HTTP, le contenu du POST peut être de différente nature : text/xml ou autre. Il n'est donc pas possible de positionner un paramètre dans le corps de la requête.

La méthode ServletRequest.getParameter() de l'API J2EE est surchargée dans le contexte de JPlatform Open API. Elle ne récupère pas les paramètres dans le corps de la requête.

3.5.3 Authentification par certificat client

Si le client OpenApi possède dans son magasin de certificats un certificat de connexion à la plateforme, l'authentification pourra être effectué par ce biais, déléguant ainsi l'authentification à l'opérateur plutôt qu'au développeur.

Pour plus d'informations, consulter la page Authentification par certificat SSL client

3.6 Ajout de nouveaux services

Toutes les classes citées ci-dessous se situent dans le package com.jalios.jcms.rest.

3.6.1 Principes

Pour ajouter un nouveau service, au sein d'un module, il faut implémenter une classe Java qui doit étendre la classe abstraite JcmsRestResource(ou une de ses sous-classe) et la déclarer dans le fichier plugin.xml de la manière suivante :

<openapi>
   <resource class="com.jalios.jcmsplugin.demo.DemoResource"
  uriTemplate="/demo/{paramdemo1}/{paramdemo2}/>
</openapi>

Lorsque la ressource est appelée par le client, un préfixe /rest est ajouté devant l'uriTemplate déclarée, en plus du context-path. On pourra par exemple appeler dans un navigateur :

http://localhost:8080/jcms/rest/workspace/j_4

Il y a deux autres classes abstraites offrant des facilités pour des ressources représentant des données ou des listes de données, à savoir :

  • DataRestResource : à utiliser si une ressource représente une Data au sens JPlatform ;
  • DataCollectionRestResource : à utiliser si une ressource représente une collection de Data.

Méthodes HTTP

Par défaut, seule la méthode GET est autorisée. Pour l'interdire, il faut surcharger la méthode allowGet :

  public boolean allowGet() {
   // interdit la méthode HTTP GET
   return false;
}

Les autres méthodes doivent être explicitement autorisées en surchargeant les méthodes allow correspondantes, comme ci-dessous :

  public boolean allowDelete() {
  // autorise la méthode HTTP DELETE
  return true;
}

public boolean allowPost() {
  // autorise la méthode HTTP POST
  return true;
}

public boolean allowPut() {
  // autorise la méthode HTTP PUT
  return true;
}

Cycle de vie des ressources

Lorsqu'une requête HTTP est traitée (c'est-à-dire commence par http://<nom-de-domaine>/<context-path>/rest/), JPlatform regarde s'il y a une URI template avec une ressource associée.

Dans ce cas, la ressource est instanciée avec appel du constructeur avec les arguments suivants : Context, Request, Response. Ce sont trois objets Restlet.

Si le statut de la réponse n'a pas été explicité, une méthode Java est appelée, en fonction de la méthode HTTP :

Méthode HTTP Méthode Java à implémenter dans une ressource héritant de JcmsRestResource
GET getItemXmlRepresentation(item)
POST doPost(Representation)
PUT doPut(Representation)
DELETE doDelete(Representation)

Ce sont les mêmes méthodes à implémenter dans une classe héritant de DataRestResource ou DataCollectionRestResource.

En implémentant JcmsRestResource, il faut, indiquer dans le constructeur l'encodage (par exemple UTF-8) et le type des représentations disponibles (par exemple text/xml) :

  // indique que l'encodage du corps de la réponse est en UTF-8 
setXmlUTF8Encoding();
// indique que l'on supporte la représentation text/xml de la réponse
getVariants().add(new Variant(MediaType.TEXT_XML));

Le moteur de Restlet détermine, en fonction de ce qui est demandé par la requête HTTP, dans l'en-tête HTTP Accept et ce qui est disponible comme type de représentation, celle qui est utilisée. En fonction de ce calcul, telle méthode correspondant au type de la représentation est appelée (voir la liste dans la description de la classe JcmsRestResource).

S'il n'y a pas de représentation disponible conformément aux souhaits de la requête, la réponse HTTP a un code 406 Not Acceptable, avec des en-têtes Content-type, Content-Encoding, Content-Language précisant ce qui est disponible.

Depuis JPlatform 10, le type de représentation JSON est disponible. Par défaut l'implémentation de l'OpenApi génère correctement le contenu JSON en utilisant la mécanique décrite dans la fiche Export JSON dans JPlatform 10.

Il est tout à fait possible de personnaliser cet export via l'implémentation de la méthode updateObjectMapperBuilder(ObjectMapperBuilder objectMapperBuilder).

3.6.2 JcmsRestResource

 

Il s'agit d'une super classe générique. On l'étend quand on ne sait pas quelle autre classe étendre, c'est-à-dire quand la ressource que l'on veut représenter n'est pas une Data JPlatform ou une collection de Data.

En étendant cette classe, on bénéficie de l'accès, entre autre, aux attributs suivants :

  • j2eeRequest : requête courante, JavaEE, classique ;
  • formQueryString : Objet Restlet Form encapsulant la queryString de la requête (mais pas les éventuels paramètres positionnés en POST, dans le corps de la requête).
Constructeur Description
public JcmsRestResource(Context context,
Request request,
Response response);
Constructeur qu'il faut invoquer dans le constructeur de la classe qui l'étend. Initialise les attributs qui doivent l'être.

Ce constructeur ne définit ni de représentations par défaut, ni d'encodage de caractères. C'est laissé à la charge du constructeur qui l'invoque.

Parmi les méthodes de JcmsRestResource, il y a celles qui sont à surcharger pour obtenir un comportement, et celle qui sont faites pour être utilisées dans les classes héritées.

Les méthodes suivantes peuvent être surchargées. Elles ont toutes le modificateur protected.

Méthodes Description
String getItemXmlRepresentation(Object item); Cette méthode est à implémenter pour renvoyer la représentation sous forme XML (Méthode GET). Si une liste est positionnée dans le pager, la méthode est appelée pour chaque élément de la liste affiché avec l'élément en paramètre. Sinon l'élément en paramètre est null.
String getPlainTextRepresentation(); Cette méthode est à implémenter pour renvoyer la représentation sous forme plein text (Méthode GET).
String getJSONRepresentation();

Cette méthode est à implémenter pour renvoyer la représentation sous forme JSON (Méthode GET).

String getAtomRepresentation(); Cette méthode est à implémenter pour renvoyer la représentation sous forme ATOM (Méthode GET).
void doPost(Representation entity); Cette méthode est à implémenter lorsque la requête est appelée avec la méthode POST.
void doPut(Representation entity); Cette méthode est à implémenter lorsque la requête est appelée avec la méthode PUT.
void doDelete(); Cette méthode est à implémenter lorsque la requête est appelée avec la méthode DELETE.

Cette classe fournit aussi des méthodes utilitaires :

Méthodes Description
void forward(String path); Transmet la requête au path spécifié.
String getXmlProlog(); Retourne le prologue XML si l'encodage de caractères a été spécifié.
void setXmlUTF8Encoding(); Indique que l'encodage de caractères est UTF-8

void generateErrorRepresentation(String errorMessage, String applicationErrorCode, Status httpStatus, Response response);

Génère une représentation XML d'une erreur, et la positionne dans la réponse.

3.6.3 DataRestResource

Cette classe est à étendre pour implémenter une ressource qui représente une Data JPlatform. Elle étend JcmsRestResource et ajoute un attribut data (de type Data).

Par défaut, le constructeur de DataRestResource propose la représentation XML, et l'encodage de caractères en UTF-8. C'est le cas le plus fréquent.

Elle surcharge également la méthode getItemXmlRepresentation(), en renvoyant data.exportXml().

Pour créer une ressource représentant une Data, il suffit donc :

  • d'étendre DataRestResource ;

  • d'implémenter le constructeur, dans lequel on positionne l'attribut data.

Si les déclarations par défaut d'encodage de caractères ou de type de représentation ne conviennent pas, il faut les positionner explicitement dans le constructeur :

 xmlEncoding = "ISO-8859-1";
getVariants().remove(0);
getVariants().add(new Variant(MediaType.ATOM_XML));

Si l'on veut une représentation autre que XML, ou un résultat autre que data.exportXml(), il faut surcharger getItemXmlRepresentation(), ou autre méthode de JcmsRestResource.

3.6.4 DataCollectionRestResource

Votre ressource doit étendre cette classe si elle produit une liste de Data. Cette classe étend DataRestResource.

Dans le constructeur il faut spécifier la collection des données via l'appel à la méthode pagerData.setCollection.

Dans l'exemple suivant, le module d'espaces de travail hiérarchique est installé sur un site. On souhaite proposer une ressource donnant les exports XML de tous les sous-espaces de travail d'un espace de travail. Voici une implémentation possible de cette ressource.

package com.jalios.jcmsplugin.hierarchicalworkspaces;

import java.util.*;
import org.restlet.Context;
import org.restlet.data.*;
import com.jalios.jcms.rest.DataCollectionRestResource;
import com.jalios.jcms.workspace.Workspace;
public class ChildrenWorkspacesResource extends DataCollectionRestResource {
   public ChildrenWorkspacesResource(Context context, Request request, Response response) {
     super(context, request, response);

     // Récupération du paramètre parent à partir de la requête
     Workspace parent = channel.getWorkspace(
      (String)request.getAttributes().get("wid"));
    if (parent == null) {
      response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
      return;
    }
    // Vérification du droit d'accès à cette ressource
    if (getLoggedMember() == null ||
        (!getLoggedMember().isAdmin() && !getLoggedMember().isAdmin(parent))) {
      response.setStatus(Status.CLIENT_ERROR_FORBIDDEN);
      return;
    }

    // Liste des sous-espaces
    Set allChildrenSet = TreeHelper.getAllSubWorkspaces(parent);
    TreeSet childrenSet = new TreeSet();
    // Filtre : uniquement les espaces dont l'utilisateur connecté est administrateur
    for (Workspace child : allChildrenSet) {
      if (getLoggedMember().isAdmin() || getLoggedMember().isAdmin(child)) {
        childrenSet.add(child);
      }
    }
    // On positionne la collection
    pagerData.setCollection(childrenSet);

    // On indique qu'il n'y a pas besoin d'entourer chaque item d'un élément XML supplémentaire
    pagerData.setItemTagName("");
  }
}

Et si on veut retourner des informations particulières du workspace au lieu de sa représentation XML, on surcharge en plus getItemXmlRepresentation() :

   @Override
  protected String getItemXmlRepresentation(Object item) {
    if (item != null) {
      Workspace workspace = (Workspace) item;
      return getCustomDataAsXML(workspace);
    }
    return super.getItemXmlRepresentation(item);
  }
  private String getCustomDataAsXML(Workspace workspace) {
  // ...
  }

3.6.5 Contexte OpenAPI

Dans les développements, côté serveur, il est possible de savoir si la requête est une requête OpenAPI en exécutant le code suivant :

RestUtil.isRest(request)

Cette méthode utilitaire teste le fait que le resourcePath de la requête J2EE (c'est-à-dire ce qui suit le context-path) commence par rest/. En particulier, si une requête OpenAPI fait une redirection vers une ressource qui ne fait pas partie de l'OpenAPI, cette méthode renvoi false lors de la seconde requête, alors qu'elle renvoi true si la requête OpenAPI fait un transfert (forward) vers une autre ressource.

Dans les DataController, il est possible également d'avoir cette information en utilisant le context :

  public void beforeWrite(Data data, int op, Member mbr, Map context) {
  if (((Boolean)context.get(CTXT_REST))) {
  // your code ...
  }
}

3.6.6 Outil pour le développement

Lors d'un développement client ou serveur (ajout de ressource) relatif à Open API, un proxy de développement peut être utile, comme par exemple :

Cela permet de voir le contenu exact des requêtes et réponses HTTP.

 

Fig. 4. Charles Proxy

3.7 Open API vs. Import/Export

Depuis la version 5.7, JPlatform dispose d'un mécanisme d'import/export de contenus. L’export fonctionne à base d'un service Web de type REST pour l'export des contenus correspondant à un ensemble de critères (les mêmes que pour une recherche). D’une certaine manière, le service Web d’export est assez proche de la recherche offerte par JPlatform Open API. Cependant, leur comportement varie sur deux points :

  1. JPlatform Open API utilise le même format d’échange XML que l’export. Cependant, dans le cas d’une recherche JPlatform Open API, celui-ci a été enrichi de balises spécifiques pour la pagination et le scoring.
  2. durant un export, les pièces jointes attachées aux contenus sont traitées comme des contenus à part entière (avec export des FileDocument correspondants). Les FileDocument attachés aux contenus à exporter sont systématiquement ajoutés lors d’un export, ce qui n’est pas le cas avec les requêtes JPlatform Open API de recherche.

4. JPlatform OpenAPI Client

4.1 Quel client pour JPlatform Open API ?

4.1.1 Navigateurs

N'importe quel programme pouvant effectuer des requêtes HTTP est en mesure d'être client de JPlatform Open API.

Le logiciel par excellence effectuant des requêtes HTTP est le navigateur internet. Il est effectivement possible de l'utiliser pour un certain nombre de requête, en particulier les requêtes GET. Cela est particulièrement intéressant dans le cadre du développement, surtout pour développer une ressource disponible uniquement avec la méthode GET.

Cela dit, la finalité de l'OpenAPI est de proposer une interface de services web que l'on appelle programmatiquement.

Il y a une restriction importante dans ce cas de figure : un navigateur ne peut effectuer que des requêtes HTTP utilisant les méthodes GET et POST. Cela est du au fait que lors d'une navigation classique (liens hypertexte et formulaires HTML), ce sont les seules méthodes utilisées.

Pour palier à ce manque, il est possible, avec JPlatform Open API, de simuler l'appel à une méthode autre que GET ou POST (par exemple DELETE) en utilisant la méthode POST, et en ajoutant un champ dans l'en-tête HTTP de cette manière :

 X-HTTP-Method-Override: DELETE

4.1.2 Clients HTTP

Dans un cadre programmatique, tout langage peut être utilisé pour être client de JPlatform Open API, du moment que ce langage propose une API HTTP. Typiquement, on peut utiliser :

  • Java avec

    • le package java.net du jdk ;

    • Apache HTTP Client ;

    • JPlatform Open API Client : son utilisation programmatique est décrite ci-dessous ;

  • .NET avec JPlatform .NET Open API Client. Un prochain article présentera son utilisation;
  • C, C++, C#;

  • Perl;
  • PHP;

  • scripts shell (via cURL);

  • ...

cURL est un outil en ligne de commande, disponible sous à peu près tous les systèmes d'exploitation, permettant d'effectuer des requêtes HTTP en précisant finement tous les paramètres de la requête. Cela permet donc de faire des scripts requêtant JPlatform Open API.

4.2 Utilisation

JcmsOpenAPIClient est une API disponible avec JPlatform, sous la forme d'une bibliothèque Java (jcmsopenapiclient.jar). Elle traite avec JPlatform au format XML uniquement.

Cette bibliothèque est incluse dans JPlatform 6.0, de manière à ce qu'une application JPlatform puisse être facilement cliente, au sens OpenAPI, d'une autre application JPlatform.

Pour utiliser cette bibliothèque dans une application Java, il faut ajouter les jar suivants dans le classpath de l'application :

acme.jar
activation.jar
chardet.jar
com.noelios.restlet.ext.net.jar
com.noelios.restlet.ext.servlet_2.4.jar
com.noelios.restlet.jar
commons-codec-1.3.jar
commons-collections-2.1.1.jar
commons-httpclient-3.0.1.jar
commons-io-1.3.1.jar
dom4j-1.6.1.jar
jakarta-oro.jar
jaliosutil.jar
jaxen-1.1.1.jar
jdom-1.0.jar
jsdk23.jar
ldapjdk.jar
log4j-1.2.15.jar
lucene-core-2.3.2.jar
mail.jar
org.restlet.jar
tritonus_share.jar
xercesImpl.jar

En particulier, pour lancer une application Java sous Windows, on pourra utiliser :

set JCMS_LIB=%JCMS_HOME%/WEB-INF/lib
set CLASSPATH=%CLASSPATH%;%JCMS_LIB%/jcmsopenapiclient.jar;%JCMS_LIB%/jdom-1.0.jar;%JCMS_LIB%/com.noelios.restlet.ext.net.jar;%JCMS_LIB%/com.noelios.restlet.ext.servlet_2.4.jar;%JCMS_LIB%/com.noelios.restlet.jar;%JCMS_LIB%/org.restlet.jar;%JCMS_LIB%/jaliosutil.jar;%JCMS_LIB%/commons-codec-1.3.jar;%JCMS_LIB%/commons-collections-2.1.1.jar;%JCMS_LIB%/commons-httpclient-3.0.1.jar;%JCMS_LIB%/commons-io-1.3.1.jar;%JCMS_LIB%/dom4j-1.6.1.jar;%JCMS_LIB%/log4j-1.2.15.jar;%JCMS_LIB%/jakarta-oro.jar;%JCMS_LIB%/jsdk23.jar;%JCMS_LIB%/acme.jar;%JCMS_LIB%/jaxen-1.1.1.jar;%JCMS_LIB%/xercesImpl.jar;%JCMS_LIB%/ldapjdk.jar;%JCMS_LIB%/tritonus_share.jar;%JCMS_LIB%/activation.jar;%JCMS_LIB%/mail.jar;%JCMS_LIB%/chardet.jar;%JCMS_LIB%/lucene-core-2.3.2.jar

Voici le squelette typique d'une application utilisant OpenAPIClient :

package com.mycompany.openapi.test;

import org.apache.log4j.Logger;
import com.jalios.rest.client.ClientSession;
import com.jalios.rest.client.JcmsApp;
import com.jalios.util.Util;

public class DemoClient {
  private static Logger logger = Logger.getLogger(DemoClient.class);

  protected ClientSession session;
  protected JcmsApp jcms;

  public DemoClient(String baseHref) {

    session = new ClientSession(baseHref);
    jcms = new JcmsApp(session);
  }

  public static void main(String[] args) {
    if (Util.isEmpty(args)) {
      logger.warn("Usage: 'java DemoClient <baseHref>'");
      return;
    }

    DemoClient exemple;
    try {
      exemple = new DemoClient(args[0]);
    } catch (IllegalArgumentException iae) {
      logger.warn("baseHref is not well formatted", iae);
      return;
    }
    ...
  }
}

Dans cette application, on se contente d'instancier les objets essentiels de l'API. L'objet ClientSession représente une session client envoyant des requêtes REST vers un serveur dont la base des URL est baseHref. Il y a donc des méthodes facilitant ces requêtes.

L'objet JcmsApp n'est à utiliser que si c'est effectivement une application JCMS qui est requêtée. On bénéficie alors de méthodes utilitaires supplémentaires, qui ont connaissance des ressources disponibles côté serveur et donc ces méthodes sont de plus haut niveau.

4.3 Authentification

On définit le mode d'authentification via l'objet ClientSession. Les authentifications sont réalisées par des implémentations de l'interface com.jalios.rest.client.Authentication. Deux implémentations sont proposées en standard : BasicAuthentication et AuthKeyAuthentication, permettant de réaliser respectivement des authentifications HTTP basique et utilisant des clés d'authentification JCMS.

Par exemple, dans le code suivant, on effectue des requêtes authentifié en tant qu'administrateur :

String login = "admin";
String passwd = "admin";
exemple.getSession().setAuthentication(new BasicAuthentication(login, passwd));
...

4.4 Accès aux données

Le code suivant affiche dans la sortie standard la représentation XML produit par l'export XML de la donnée d'identifiant j_300. Pour cela, on positionne comme valeur de baseHref celle d'un serveur JCMS vierge avec OpenAPI accessible en lecture.


try {
  String dataId = "j_300";
  JcmsResource dataResource = exemple.getJcmsApp().getData(dataId);
  System.out.println("Data exportXml :");
  System.out.println(dataResource.getEntityText());
} catch (RestException re) {
  logger.warn("Exception occured while data retrieved from openapi !", re);
}

On obtient alors le résultat suivant :

Data exportXml :

<?xml version='1.0' encoding='UTF-8' ?>
<data class='generated.Article' id='j_300' url='http://127.0.0.1:8080/jcms/j_300/welcome-to-jcms-57'>
  <field name='title'>Welcome to JCMS 5.7</field>
  <field name='titleML'>
    <entry>
      <key>fr</key>
      <value>Bienvenue dans JCMS 5.7</value>
    </entry>
  </field>
  <field name='pdate'>2007-01-19T17:24:00+01:00</field>
  <field name='udate'>2007-01-19T17:22:12+01:00</field>
  <field name='version' major='1' minor='0'>1.0</field>
  <field name='mainLanguage'>en</field>
  <field name='pstatus'>0</field>
  <field name='workspace' id='j_4' class='com.jalios.jcms.workspace.Workspace'>Workspace 1</field>
  <field name='templates'>
    <item>default</item>
    <item>query.default</item>
  </field>
  <field name='author' id='j_2' class='com.jalios.jcms.Member' login='admin'>Admin</field>
  <field name='opAuthor' id='j_2' class='com.jalios.jcms.Member' login='admin'>Admin</field>
  <field name='cdate'>2007-01-19T17:22:12+01:00</field>
  <field name='mdate'>2007-01-19T17:46:25+01:00</field>
  <field name='summary' mlField='summaryML' abstract='true'>You have installed JCMS 5.7; get more insight about JCMS while reading the cookbook [[http://support.jalios.com/howto/gettingstarted][JCMS Getting Started]].</field>
  <field name='summaryML'>
    <entry>
      <key>fr</key>
      <value>Vous avez installé avec succès JCMS 5.7; continuez votre découverte en consultant l&apos;article [[http://support.jalios.com/howto/gettingstarted][Installation et prise en main rapide de JCMS]].</value>
    </entry>
  </field>
  [...]
</data>

Pour extraire des informations de ce résultat XML, il est possible d'utiliser des expressions XPath. Dans l'exemple suivant, on extrait le titre de la publication.

String title = ClientUtil.getString(dataResource, "/data/field[@name='title']")

On peut également utiliser la classe DataElement. Cette classe permet de manipuler un élément JDOM qui représente une publication JCMS :

...
Element element = ClientUtil.getFirstElement(dataResource, "/data");
DataElement dataElement = new DataElement(element, session);
System.out.println("Id : " + dataElement.getId());
System.out.println("Main language : " + dataElement.getMainLanguage());
System.out.println("Title : " + dataElement.getTitle());
System.out.println("Abstract : " + XmlUtil.getStringNode(dataElement.getAbstractField()));
System.out.println("URL : " + dataElement.getUrl());
System.out.println("Creation date : " + dataElement.getCdate());
System.out.println("Modification date : " + dataElement.getMdate());
System.out.println("Publication date : " + dataElement.getPdate());

Ce qui produit le résultat suivant :

Id                      : j_300
Main language : en
Title : Welcome to JCMS 5.7
Abstract : You have installed JCMS 5.7; get more insight about JCMS while reading the cookbook [[http://support.jalios.com/howto/gettingstarted][JCMS Getting Started]].
URL : http://localhost:8080/jcms6/jcms/j_300/welcome-to-jcms-57
Creation date : Fri Jan 19 17:22:12 CET 2007
Modification date : Fri Jan 19 17:46:25 CET 2007
Publication date : Fri Jan 19 17:24:00 CET 2007

Il est possible, dans toutes les méthodes utilisant des identifiants de données, d'utiliser à leur place des identifiants virtuels. Par exemple, pour obtenir la catégorie racine, on peut invoquer la ressource suivante :

/data/$channel.root-category

Une classe MemberElement est également disponible pour faciliter la manipulation de la représentation d'une donnée de type Member.

4.5 Relateds

Il est d'usage, dans un contexte de constructions d'URI REST, d'utiliser les paramètres de la queryString pour préciser des paramètres d'un algorithme. C'est pourquoi l'on a par exemple des URI du type :

/search?text=mon+texte

Dans cet exemple, la ressource est le moteur de recherche, et text est un paramètre que l'on donne à l'algorithme de recherche.

La notion de "related" permet d'accéder aux résultats de certaines méthodes appelées sur les données de JCMS. Ces méthodes ne correspondent pas à des informations persistées associées à ces objets, mais sont déterminées à partir d'index JCMS ou de calculs.

Ces related permettent de récupérer par exemple :

  • les ExtraData,
  • les ExtraDBDta,
  • les champs calculés (provenant d'index par exemple).

L'URI suivante, par exemple :

/search?text=faq&related=linkIndexedDataSet.Class.generated.FaqEntry&related=linkCount

Permet de récupérer en une requête, la liste des résultats de recherche correspondant au texte "faq" et ajoutera à chaque donnée résultant, dans le XML, un élément XML de nom de balise "related" avec la liste des entrées de FAQ pointant ces objets (s'il y en a) et le nombre de lien vers ces objets (de toutes natures).

Le résultat pourra par exemple être :

<?xml version='1.0' encoding='UTF-8' ?>
<dataset>
<data class='generated.Faq' id='leader_8995' url='http://127.0.0.1:8080/en/jcms/leader_8995/faq-title-in-english-1264177749566'>
  <field name='title'>Faq Title in english 1264177749566</field>
  <field ...
     ...
  </field>
  <related name='linkIndexedDataSet.Class.generated.FaqEntry'>
    <item id='leader_8996' class='generated.FaqEntry'>Faq Entry Title in english 1264177749609</item>
  </related>
  <related name='linkCount'>1</related>
</data>
</dataset>

 

Voici la liste des related disponible. Notez que si un related est disponible pour un objet, il l'est également pour les sous-types.

Classe Related Description
Data extraData.<identifiant de l'extra data> Valeur de l'extraData correspondante (persistée dans le store).
Data extraDBData.<identifiant de l'extra DB data> Valeur de l'extraBDData correspondante (persistée en base).
Data allExtraData Toutes les ExtraData et ExtraDBData disponible sur la donnée.
Data extraInfo.<identifiant de l'extra info> Valeur de l'extraInfo correspondante (en mémoire uniquement).
Data dataImage Image de la donnée, si un champ correspondant est défini.
Data linkCount Nombre de liens de données JCMS pointant vers cette donnée.
Data lockDate Si la donnée est verrouillée, donne la date de verrouillage.
Data allReferrerSet Ensemble de toutes les représentations de toutes les données pointant vers la donnée courante, ou pointant vers les données pointant vers la donnée courante, et ainsi de suite.
Data allReferrerSet.<nom d'une classe> Comme précédemment, mais seulement les données de la classe précisée, ou ayant un chemin vers la donnée courante, composé de données de la classe précisée.
Data linkIndexedDataSet.Class.<nom d'une classe> Ensemble des représentations des données de la classe précisée ayant un lien vers la donnée courante.
Data

linkIndexedDataSet.ClassField.<nom d'une classe>.<nom technique d'un champs>

Comme précédement, mais par le lien dont le nom technique est précisé.
Data lockMember Représentation du membre qui détient le verrou sur la donnée courante.
Member workspaceSet Ensemble des espaces de travail pour lequel le membre concerné a des droits de contribution.
Category childrenSet Ensemble de catégories filles de la catégorie concernée.
Workspace catSet.loggedMember Ensemble des catégories racines de l'espace de travail concerné, pour lesquels le membre connecté a des droits de consultation.

 

4.6 Pronfondeur d'export - depth (uniquement depuis JPlatform 10)

JPlatform 10 ajoute la possibilité de pouvoir obtenir les résultats des requêtes REST au format JSON (via le header accept: application/json).

Par défaut, le serializer JSON effectue une sérialisation en profondeur des objets, aboutissant à un grande nombre de données souvent inutiles (par exemple l'ensemble des groupes de l'auteur d'un publication) ou à des boucles infinies (sérialisation d'un utlisateur A appartenant à un groupe dont il est l'auteur, ...). Une sécurité a donc été mise en place : la limitation de la profondeur de sérialisation

Dans ce cas, les objets JPlatform sérialisés au niveau de profondeur maximal seront remplacés par une structure contenant leur classe, leur id et leur titre.

Ex.

 "declaredGroups": [
        {
            "class": "com.jalios.jcms.Group",
            "id": "jn1_339640",
            "name": "xxxxxx"
        },
        {
            "class": "com.jalios.jcms.Group",
            "id": "jn1_103636",
            "name": "XXXXX"
        }
    ],

Par défaut, la profondeur est fixé à 1 mais elle peut être modifiée en précisant le paramètre depth dans la requête OpenAPI.

4.7 Modification des données

JCMS Open API Client permet aussi de créer des données. Dans l'exemple ci-dessous on créé un Article :

// Creation of an article
// Preparation of parameters for EditArticleHandler
Form fields = new Form();
fields.add("author", "j_2");
fields.add("title", "Title in english " + System.currentTimeMillis());
fields.add("title", "Titre en français " + System.currentTimeMillis());
fields.add("summary", "textfieldvalue 1");
fields.add("summary", "textfieldvalue 2");
fields.add("content", "textareafieldvalue 1");
fields.add("content", "textareafieldvalue 2");
// Call to the resource to create an Article, in POST
JcmsResource response = jcms.createData("Article", fields);

Reference article = session.getCreatedDataRef(response);

Les champs à positionner sont les champs correspondant du javabean EditArticleHandler.

Les champs multilingues (eg. title), doivent être alimentés dans l'ordre de déclaration des langues de l'application JCMS appelée.

L'exemple ci-dessous effectue une mise à jour de l'article précédemment créé:

fields = new Form();
fields.add("content", "textareafieldvalue modified");
fields.add("content", "textareafieldvalue 2");
jcms.updateData(article, fields);

Enfin, le code ci-dessous effectue la suppression de l'article :

jcms.deleteData(article);

Il est possible de créer des FileDocument à travers l'Open API. Cela nécessite une requête en multipart de la ressource data.

Il a donc été ajouté également le support des requêtes en multipart dans l'Open API Client. En utilisant l'API de plus haut niveau, la création d'un FileDocument se fait de la manière suivante :

File testFile = getResourceFile("image.jpg");
Form fields = new Form();
fields.add("title", "The title");
fields.add("description", "The description");
fields.add("pstatus", "0");

JcmsResource resourceFDCreated = jcms.createFileDocument(testFile, fields);

Ce qui donne une requête HTTP comme cela :

POST /en/rest/data/FileDocument HTTP/1.1
Host: icare.jalios.net:8080
User-Agent: JaliosJCMS-JavaRestClient/24777 (http://www.jalios.com/)
Accept: text/xml
Content-Type: multipart/form-data; boundary=qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb
Authorization: Basic YWRtaW46YWRtaW4=
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 5253

--qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb
Content-Disposition: form-data; name="filename"; filename="test.jpg"
Content-Type: image/jpeg; charset=ISO-8859-1
Content-Transfer-Encoding: binary

ÿØÿà<encodage binaire du fichier>’éÓÌ¡â{¬k1
...
:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰
--qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb
Content-Disposition: form-data; name="title"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The title
--qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb
Content-Disposition: form-data; name="description"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The description
--qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb
Content-Disposition: form-data; name="pstatus"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

0
--qCN-ff31sPDlgy9S9ohQqLZbE3pZhMiixb--

 

4.8 Recherches

Une recherche avec JCMSOpenAPIClient se fait de la manière suivante, en fournissant les mêmes critères de recherche que pour une recherche JCMS habituelle :

Form fields = new Form();
fields.add("types", "generated.Article");
fields.add("text", "Welcome");


JcmsResource resource = jcmsApp.search(fields);

List dataList = ClientUtil.getNodeList(resource, "/dataset/data");
Element scores = ClientUtil.getFirstElement(resource, "/dataset/scores");

System.out.println("Search returns " + dataList.size() + " element(s)");
// For each XML data, construct an real Java object
for (Object data : dataList) {
  if (data instanceof Element) {
    Element element = (Element) data;
    DataElement dataElement = new DataElement(element, session);

    String id = dataElement.getId();
    float score = Util.toFloat(XmlUtil.getString(scores, "./data[@id='" + id + "']/@score"), 0);
    System.out.println("Data '" + dataElement.getTitle() + "' (" + id + ") has score :" + score);
  }
}

4.9 Administration

La visualisation des informations du site est disponible de la manière suivante. Dans l'exemple ci-dessous, on vérifie que le site est opérationnel :

JcmsResource adminResource = jcmsApp.getAdminStatus();
System.out.println("Site running : " + ClientUtil.getString(adminResource, "/status/state/running"));

Les actions d'administration sont invoquées de la façon suivante :

// Disabling  data write 
jcmsApp.disableDataWrite("OpenApi Disable Data Write");

// Enabling data write
jcmsApp.enableDataWrite("OpenApi Enable Data Write");

// Trigger import from data source 'importSourceId'
jcmsApp.importDataFromSource(importSourceId);

4.10 ClientUtil et XmlUtil

Des méthodes utilitaires ont été ajoutées à partir de JCMS 6.0 SP1 dans les classes XmlUtil et ClientUtil.

Ces nouvelles méthodes proposent d'utiliser la syntaxe XPath pour accéder à des éléments d'un flux XML.

La syntaxe XPath est spécifiée par le W3C. Le site w3schools propose un tutoriel d'introduction à XPath (en anglais).

XmlUtil propose essentiellement différentes déclinaisons utilitaires des deux méthodes suivantes :

public static List<?> getNodeList(String xmlString, String xpathExpression) throws JDOMException;
public static String getStringNode(Object node);

La méthode getNodeList() utilise l'expression XPath en second argument pour rechercher dans le flux contenu dans la chaîne de caractère en premier argument. Elle renvoie la liste des éléments correspondant à cette recherche. Une exception de type JDOMException est envoyée si xmlString n'est pas un flux XML bien formé ou si xpathExpression est une expression XPath est invalide.

La nature des objets de la liste retournée est determinée par ce qui peut être retourné par l'expression XPath (Text, Attribute, Element, Comment, ProcessingInstruction, Boolean, Double, String).

La méthode getStringNode() renvoie une représentation sous forme de chaîne de caractères d'un élément de la liste retournée par getNodeList() (le contenu d'une balise, la valeur d'un attribut, la valeur d'une instruction ou d'un commentaire XML, ...).

ClientUtil fournit les méthodes encapsulant les méthodes de XmlUtil, mais permettant de spécifier une ressource au lieu d'une chaîne de caractère.

public static List<?> getNodeList(JcmsResource resource, String xpathExpression) throws JDOMException

Il faut tenir compte des contraintes de performances. L'exécution d'une expression XPath sur un flux XML de grosse taille peut être longue. Ne réalisez pas toutes les expressions XPath sur la totalité de la ressource.

Typiquement, si la ressource renvoie une liste de data, on utilise la méthode ClientUtil.getNodeList pour récupérer la liste des noeuds correspondant aux data. On utilise ensuite plusieurs fois la méthode XmlUtil.getSingleNode(Object doc, String xpathExpression) sur chaque nœud.

A partir de JCMS 6.1 SP2 des méthodes utilitaires avec une valeur par défaut (et ne levant pas d'exception) ont été ajoutées, par homogénéité avec les méthodes des classes utilitaires de JCMS.

Par exemple, la méthode suivante de la classe ClientUtil peut générer une JDomException :

public static String getString(JcmsResource resource, String xpathExpression) throws JDOMException;

Il a été ajouté une méthode qui renvoit une valeur par défaut si nécessaire :

public static String getString(JcmsResource resource, String xpathExpression, String def);

5. Exemples

5.1 Vérifier l'état du site

Nous allons réaliser cet exemple de deux manières différentes :

  1. sous la forme d'un script Unix utilisant cURL et cron ;

  2. en Java avec JCMS Open API Client et une alarme JDring.

5.1.1 Avec un script Unix

Pour vérifier si un site tourne régulièrement, on utilise le script suivant :

#!/bin/sh
TMP_OUT_FILE="monitorJCMS.output.$$.tmp"

curl --config ./monitorJCMS.curl.conf --output $TMP_OUT_FILE

date
if grep -q "<running>true</running>" $TMP_OUT_FILE
then
  echo "Site is running"
else
  echo "Site is not running"
fi
rm $TMP_OUT_FILE

Et le fichier de configuration de curl suivant :

url = "http://localhost:8080/jcms6/rest/admin/status"
user-agent = "JCMSCmdLineMonitoring/0.1"
include
basic
user = "admin:admin"
stderr = "stderr"

Pour exécuter ce script toutes les cinq minutes, placer la ligne suivant en crontab :

0,5,10,15,20,25,30,35,40,45,50,55 * * * * monitorJCMS.sh

5.1.2 Avec JDring et OpenAPIClient

La même chose est possible en utilisant un JCMS pour superviser d'autres JCMS.

...
public class OpenAPIRemoteMonitoringListener implements AlarmListener, PluginComponent {
  JcmsApp jcmsApp;
  Channel channel = Channel.getChannel();
  Logger logger = Logger.getLogger(OpenAPIRemoteMonitoringListener.class);

  public void handleAlarm(AlarmEntry entry) {
    if (!isRunning(jcmsApp)) {
      // If it is not running, a mail is sent to the admin
      String subject = "Site "+ jcmsApp.getSession().getBaseUrl() + " is not running at " + new Date();
      MailMessage msg = new MailMessage(channel.getDefaultEmail());
      try {
        msg.setTo(channel.getDefaultAdmin())
           .setSubject("[Monitoring ALERT] " + subject)
           .setContentText("This mail was sent automatically by " + channel.getName())
           .send();
      } catch (MessagingException ex) {
        logger.warn("Impossible to send mail with subject '" + subject + "'", ex);
      }
      logger.warn(subject);
    } else {
      // Otherwise, a simple debug log is done
      logger.debug("Site "+ jcmsApp.getSession().getBaseUrl() + " is running at " + new Date() ) ;
    }
  }

  private boolean isRunning(JcmsApp jcmsApp) {
    try {
      // The call to the REST resource
      JcmsResource adminResource = jcmsApp.getAdminStatus();
      // Dig into the XML to find the information and return the value as a boolean
      return ClientUtil.getBoolean(adminResource, "/status/state/running", false);
    } catch (RestException ex) {
      // If the resource was not found, the site is supposed to be unreachable
      return false;
    }
  }

  public boolean init(Plugin plugin) {
    String baseHref = "http://127.0.0.1:8080/en/";
    jcmsApp = new JcmsApp(new ClientSession(baseHref));
    jcmsApp.getSession().setAuthentication(new BasicAuthentication("admin", "admin"));

    return true;
  }
}

5.2 Alimentation de la base des membres

L'exemple suivant est une application de gestion des membres à distance. Elle est disponible au complet en entier en pièce jointe de cet article.

Nous décrivons ici les méthodes read(), create(), modify()delete() et disable(), ainsi que la méthode unable().

La méthode read() appelle la ressource correspondant à ce membre à partir de son login. Ensuite elle instancie un objet DataElement à partir du XML récupéré, ce qui permet d'introspecter dans le XML de l'élément pour récupérer des informations et les afficher. Ensuite, on se contente d'afficher ce qu'il y a dans l'élément correspondant à la valeur author de l'attribut name.

  public void read(String login, boolean details) {
  JcmsResource memberResource;
  try {
    memberResource = jcms.getMember(login);
  } catch (RestException ex) {
    System.out.println("An error occured while calling the remote site");
    ex.printStackTrace(System.err);
    return;
  }

  DataElement memberData;
  try {
    memberData = new DataElement(ClientUtil.getFirstElement(memberResource, "/data"), session);
  } catch (JDOMException ex) {
    System.out.println("An error occured while parsing the result");
    ex.printStackTrace(System.err);
    return;
  }
  System.out.println("Member is :" + XmlUtil.getStringNode(memberData.getField("author")));
  if (details) {
    // display the whole XML
    System.out.println("Details for login : "+ login + "\n" + memberResource.getEntityText()) ;
  }
}

 

La méthode create() instancie et positionne les attributs d'un objet Form à partir des options positionnées en argument. Au passage, on vérifie la présence de tous les paramètres obligatoires. Ensuite, on invoque la méthode JcmsApp.createData pour invoquer la ressource /rest/data/Member, pour réaliser une création.

  public static String[] createMandatoryFields = new String[]{"name", "login"};
public static String[] createMandatoryMultiValuedFields = new String[]{"gids"};
public static String[] createOptionnalFields = new String[]{"firstname", "salutation", "jobTitle", "email", "phone", "mobile", "address", "info", "admin", "emailFormat", "emailVisible", "newsletter", "language"};

public void create(Map<String, String> options) {
  // Fields are filled from parameters in arguments.
  Form fields = new Form();
  for (int i = 0; i < createMandatoryFields.length; i++) {
    String fieldName = createMandatoryFields[i];
    String fieldValue = options.get(fieldName);
    if (Util.isEmpty(fieldValue)) {
      System.out.println("Bad usage : Field " + fieldName + " not specified." + usage);
      return;
    } else {
      fields.add(fieldName, fieldValue);
    }
  }
  for (int i = 0; i < createMandatoryMultiValuedFields.length; i++) {
    String fieldName = createMandatoryMultiValuedFields[i];
    String fieldValue = options.get(fieldName);
    if (Util.isEmpty(fieldValue)) {
      System.out.println("Bad usage : Field " + fieldName + " not specified." + usage);
      return;
    } else {
      String[] fieldValues = fieldValue.split(",");
      for (int j = 0; j < fieldValues.length; j++) {
        fields.add(fieldName, fieldValues[j]);
      }
    }
  }
  for (int i = 0; i < createOptionnalFields.length; i++) {
    String fieldName = createOptionnalFields[i];
    String fieldValue = options.get(fieldName);
    if (Util.notEmpty(fieldValue)) {
      fields.add(fieldName, fieldValue);
    }
  }

  {
    // Password is special, because in the web interface, it must be filled twice
    String password = options.get("password");
    if (Util.isEmpty(password)) {
      System.out.println("Bad usage : Field password not specified." + usage);
      return;
    } else {
      fields.add("password1", password);
      fields.add("password2", password);
    }
  }

  // /rest/data/Member is invoqued in POST to create the member
  JcmsResource response = jcms.createData("Member", fields);

  // Does the response indicates a creation has been done ?
  if (Status.SUCCESS_CREATED.equals(response.getStatus())) {
    System.out.println("Member created");
    System.out.println("Location: " + response.getRedirectRef().toString());
  } else {
    System.out.println("Problem : status : " + response.getStatus().getDescription() + " (" + response.getStatus().getCode() + ")");
    return;
  }
}

 

La méthode modify() utilise le même principe que pour la création, sauf que si l'utilisateur indique le login et non pas l'identifiant, on fait une première requête vers la ressource /rest/data/Member/<login>, de manière à récupérer l'identifiant.

  public static String[] modifyOptionnalFields = new String[]{"name", "firstname", "salutation", "jobTitle", "email", "phone", "mobile", "address", "info", "admin", "emailFormat", "emailVisible", "newsletter", "language"};
public static String[] modifyOptionnalMultiValuedFields = new String[]{"gids"};

public void modify(Map<String, String> options) {
  // If member is is not specified, look for it from the login
  String memberId = options.get("id");
  if (Util.isEmpty(memberId)) {
    memberId = jcms.getIdFromLogin(options.get("login"));
    if (Util.isEmpty(memberId)) {
      System.out.println("Empty memberId");
      return;
    }
  }

  // Fields are filled from parameters in arguments.
  Form fields = new Form();
  for (int i = 0; i < modifyOptionnalMultiValuedFields.length; i++) {
    String fieldName = modifyOptionnalMultiValuedFields[i];
    String fieldValue = options.get(fieldName);
    if (Util.notEmpty(fieldValue)) {
      String[] fieldValues = fieldValue.split(",");
      for (int j = 0; j < fieldValues.length; j++) {
        fields.add(fieldName, fieldValues[j]);
      }
    }
  }
  for (int i = 0; i < modifyOptionnalFields.length; i++) {
    String fieldName = modifyOptionnalFields[i];
    String fieldValue = options.get(fieldName);
    if (Util.notEmpty(fieldValue)) {
      fields.add(fieldName, fieldValue);
    }
  }
  {
    String fieldName = "password";
    String fieldValue = options.get(fieldName);
    if (Util.notEmpty(fieldValue)) {
      fields.add(fieldName + "1", fieldValue);
      fields.add(fieldName + "2", fieldValue);
    }
  }

  // /rest/data/<memberId> is invoqued with PUT to modify the member
  JcmsResource response = jcms.updateData(memberId, fields);
  if (Status.SUCCESS_OK.equals(response.getStatus())) {
    System.out.println("Member modified");
  } else {
    System.out.println("Problem : status : " + response.getStatus().getDescription() + " (" + response.getStatus().getCode() + ")");
    return;
  }
}

 

Enfin, la méthode delete() se charge de la suppression du membre :

  public void delete(Map<String, String> options) {
  // If member is is not specified, look for it from the login
  String memberId = options.get("id");
  if (Util.isEmpty(memberId)) {
    memberId = jcms.getIdFromLogin(options.get("login"));
    if (Util.isEmpty(memberId)) {
      System.out.println("Empty memberId");
      return;
    }
  }
  // /rest/data/<memberId> is invoked with DELETE
  JcmsResource response = jcms.deleteData(memberId);
  if (Status.SUCCESS_OK.equals(response.getStatus())) {
    System.out.println("Member deleted");
  } else {
    System.out.println("Problem : status : " + response.getStatus().getDescription() + " (" + response.getStatus().getCode() + ")");
    return;
  }
}

 

La méthode enable() invoque la ressource correspondant au membre à partir de son login en POST, avec comme paramètre l'action enable.

  public void enable(Map<String, String> options) {
  // If member is is not specified, look for it from the login
  String login = options.get("login");
  String password = options.get("password");

  if (Util.isEmpty(login) || Util.isEmpty(password)) {
    System.out.println("Bad usage : Fields login and password are mandatory." + usage);
    return;
  }
  JcmsResource response = jcms.enableMember(login, password);
  if (Status.SUCCESS_OK.equals(response.getStatus())) {
    System.out.println("Member enabled");
  } else {
    System.out.println("Problem : status : " + response.getStatus().getDescription() + " (" + response.getStatus().getCode() + ")");
    return;
  }
}

 

La méthode disable() invoque la ressource correspondant au membre à partir de son login en POST, avec comme paramètre l'action disable et le nouveau mot de passe (nécessaire pour la réactivation d'un membre).

  public void disable(Map<String, String> options) {
  String login = options.get("login");
  if (Util.isEmpty(login)) {
    System.out.println("Bad usage : Field login not specified." + usage);
    return;
  }
  JcmsResource response = jcms.disableMember(login);
  if (Status.SUCCESS_OK.equals(response.getStatus())) {
    System.out.println("Member disabled");
  } else {
    System.out.println("Problem : status : " + response.getStatus().getDescription() + " (" + response.getStatus().getCode() + ")");
    return;
  }
}

5.3 Recherches multi-sites

Dans l'article sur le développement de queryFilter, on étend la recherche au moteur de recherche Google. L'exemple suivant consiste à effectuer la même chose, mais en intégrant les résultats de recherche de deux sites JCMS différents (l'exemple peut être étendu à un nombre quelconque de sites).

On créé le module CrossJcmsSearchPlugin. Puis on déclare dans le fichier plugin.xml de ce module un nouveau gabarit de l'objet WebPage et un QueryFilter pour la recherche à distance dans un JCMS.

[…]
<types>
  <templates type="WebPage">
    <template name="site" file="doCrossJcmsSearchResultDisplay.jsp" usage="query">
      <label xml:lang="en">Remote Content Template</label>
      <label xml:lang="fr">Gabarit contenu à distance</label>
    </template>
  </templates>
</types>
[…]
<plugincomponents>
  <queryfilter class="com.jalios.jcmsplugin.crossjcmssearch.CrossJCMSSearchQueryFilter" />
</plugincomponents>
[…]

Ce QueryFilter est un client pour la ressource JCMS Open API de recherche du JCMS distant. Il utilisera donc JCMSOpenAPIClient.

package 
package com.jalios.jcmsplugin.crossjcmssearch;

import […]

public class XSSearchQueryFilter extends QueryFilter implements PluginComponent {
  private String alterSiteBaseUrl;

  public boolean init(Plugin plugin) {
    // In configuration, alterSiteBaseUrl
    […]
  }

  public QueryResultSet filterResultSet(QueryHandler qh,
                                        QueryResultSet set, Map context) {
    HttpServletRequest request = qh.getRequest();
    // Check if the request must be extended to other site
    if (request == null || Util.isEmpty(qh.getText()) ||
        Util.isEmpty(request.getParameter(getSearchParam())) ) {
      return set;
    }
    try {
      // Init the current client session
      ClientSession session = new ClientSession(alterSiteBaseUrl);
      JcmsApp jcms = new JcmsApp(session);

     // Dig parameters in the query handler and in the request, to prepare the search request
     Form fields = prepareSearchForm(qh, request);

     // Perform the remote request
     JcmsResource resource = jcms.search(fields);

     // Dig into the list of data, which is the result
     List dataList = ClientUtil.getNodeList(resource, "/dataset/data");
     Element scores = ClientUtil.getFirstElement(resource, "/dataset/scores");


     // For each XML data, contruct an real Java object
     for (Object data : dataList) {
       if (data instanceof Element) {
         Element element = (Element) data;
         DataElement dataElement = new DataElement(element, session);
         WebPage result = prepareResult(element, dataElement);


         String id = dataElement.getId();
         float score = Util.toFloat(XmlUtil.getString(scores, "./data[@id='" + id + "']/@score"), 0);

         set.add(result, score);
      }
    } catch (IllegalArgumentException iae) {
    } catch (RestException e) {
    } catch (JDOMException ex) {
    }
    return set;
  }
  […]
  public String getSearchParam() {
    return "xsites";
  }
  public String getSearchLabel(String lang) {
    return "Other Sites";
  }
  public boolean getSearchDefault() {
    return false;
  }
}

Dans la méthode filterResultSet :

  • on vérifié la présence du paramètre xsites indiquant que la recherche tient compte du site distant ;
  • on instancie la session cliente et l'application JCMS distante, au sens de la librairie JCMSOpenAPIClient ;
  • on instancie et on prépare un object Form, reprenant les paramètres de recherche de la requête ;
  • on effectue la requête de recherche distante ;
  • on introspecté le XML de réponse pour rechercher les valeurs des datas et leur score lors de la recherche ;
  • on instancie des objets WebPage, avec les bonnes valeurs et indication dans le QueryResultSet de retour, avec les valeurs des scores.

La méthode prepareSearchForm() instancie l'objet Form et remplit les paramètres pour préparer la requête de recherche.

private Form prepareSearchForm(QueryHandler qh, HttpServletRequest request) { 
  Form fields = new Form();
  if (Util.notEmpty(qh.getTypes())) {
    for (String type : qh.getTypes()) {
      fields.add("types", type);
    }
  }
  if (Util.notEmpty(qh.getText())) {
    fields.add("text", qh.getText());
  }

  fillFields(fields, request, "text");
  fillFields(fields, request, "mode");
  fillFields(fields, request, "searchedAllFields");
  fillFields(fields, request, "catName");
  fillFields(fields, request, "catMode");
  fillFields(fields, request, "dateType");
  fillFields(fields, request, "dateSince");
  fillFields(fields, request, "dateSince_user");
  fillFields(fields, request, "dateSince_unit");
  fillFields(fields, request, "beginDay");
  fillFields(fields, request, "beginMonth");
  fillFields(fields, request, "beginYear");
  fillFields(fields, request, "endDay");
  fillFields(fields, request, "endMonth");
  fillFields(fields, request, "endYear");
  fillFields(fields, request, "replaceFileDoc");
  return fields;
}

private static void fillFields(Form fields, HttpServletRequest request, String fieldName) {
  String value = request.getParameter(fieldName);
  value = value == null ? "" : value;
  fields.add(fieldName, value);
}

 

 

La méthode prepareResult instancie un objet WebPage et positionne ce qui est possible de récupérer du XML de la réponse. Elle positionne également query.site comme template, ce qui signifie, que l'on utilise le gabarit déclaré dans le fichier plugin.xml, comme gabarit de résultat de recherche.

private WebPage prepareResult(Element element, DataElement dataElement) {
  WebPage result = new WebPage();
  Channel.getChannel().getQueryManager().prepareExternalResult(result);

  result.setMainLanguage(dataElement.getMainLanguage());
  result.setTitle(dataElement.getTitle());
  Element titleML = dataElement.getField("titleML");
  if (titleML != null) {
    result.setTitleML(ImportUtil.parseFieldTextML(element, "titleML"));
  }
  result.setDescription(XmlUtil.getStringNode(dataElement.getAbstractField()));
  Element abstractML = dataElement.getMLField(dataElement.getAbstractField());
  if (abstractML != null) {
    result.setDescriptionML (ImportUtil.parseFieldTextML(element, dataElement.getAbstractMLName()));
  }
  result.setUrl(dataElement.getUrl());
  result.setTemplate("query.site");
  result.setCdate(dataElement.getCdate());
  result.setMdate(dataElement.getMdate());
  result.setPdate(dataElement.getPdate());

  return result;
}

Remarquez que ce qui est positionné dans le champ template est la concaténation de query (l'usage), et ce qui est déclaré dans le fichier plugin.xml (à savoir site), séparé par un point.

 

Fig. 7. Recherche cross site : affichage du résultat

5.4 Ajout de nouvelles ressources

Dans cet exemple, nous allons fournir une nouvelle ressource correspondant aux espaces de travail, et affichant leurs tableaux de bord.

Pour cela, on crée le module WorkspaceResource. Dans le fichier plugin.xml, nous déclarons une nouvelle ressource :

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plugin PUBLIC "-//JALIOS//DTD JCMS-PLUGIN 1.4//EN" "http://support.jalios.com/dtd/jcms-plugin-1.4.dtd">
<plugin name="WorkspaceResource" version="0.1" author="Jalios SA" initialize="true">
  <label xml:lang="en">Workspace Resource Plugin</label>
  <description xml:lang="en">Module Workspace Resource</description>
  <openapi>
    <resource class="com.jalios.jcmsplugin.workspaceresource.WorkspaceResource" uriTemplate="/workspace/{wid}" />
  </openapi>

  [...]
</plugin>

Nous pourrons donc accéder à une ressource Espace de travail en utilisant une URL de la forme.

http://<nom-de-domaine>/<context-path>/rest/workspaces/j_4

Pour proposer de nouvelles ressources, JCMS Open API propose trois classes abstraites à étendre :

  • DataRestResource, pour implémenter une ressource REST représentant une data JCMS ;
  • DataCollectionRestResource, pour implémenter une ressource REST représentant une liste de data JCMS ;
  • JcmsRestResource pour les autres cas.

La classe WorkspaceResource va donc naturellement étendre DataRestResource.

L'accès à cette ressource sera restreint aux administrateur centraux et administrateurs des espaces de travail concernés.

Voici l'implémentation de cette classe :

package com.jalios.jcmsplugin.workspaceresource;

import org.restlet.Context;
import org.restlet.data.*;

import com.jalios.jcms.Channel;
import com.jalios.jcms.rest.DataRestResource;
import com.jalios.jcms.workspace.Workspace;

public class WorkspaceResource extends DataRestResource {

  public WorkspaceResource(Context context, Request request, Response response) {
    super(context, request, response);
    // L'appel de cette ressource nécessite une authentification
    if (Util.isEmpty(getLoggedMember())) {
      // ie status code HTTP 401 Unauthorized
      response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
      return;
    }
    // On récupère l'identifiant de l'espace de travail
    String wid = (String) request.getAttributes().get("wid");
    // L'attribut data est défini dans la classe DataRestResource
    data = Channel.getChannel().getWorkspace(wid);

    // On gère le cas où l'identifiant ne correspond pas à un espace de travail
    if (data == null) {
      // ie status code HTTP 404 Not Found
      response.setStatus(Status.CLIENT_ERROR_NOT_FOUND, "Workspace doesn't exists");
      return;
    }
    // L'utilisateur connecté a-t-il le droit d'accéder à ces informations
    if (!getLoggedMember().isAdmin() &&
        !getLoggedMember.isAdmin((Workspace)data)) {
      // Le code CLIENT_ERROR_FORBIDDEN correspond au code réponse HTTP 403 Forbidden
      response.setStatus(Status.CLIENT_ERROR_FORBIDDEN);
      return;
    }
  }

  @Override
  protected String getItemXmlRepresentation(Object item) {
    // Quand on traite une donnée, on utilise l'objet data, alors que dans le cas d'une liste, on utilise les "item" en paramètre
    // Ici data ne peut pas être null et ne peut être qu'un espace de travail
    // Les cas exceptionnels sont traités dans le constructeur
    return getDataReportAsXML((Workspace) data);
  }

  private String getDataReportAsXML(Workspace workspace) {
    ...
  }
}

Un objet de cette classe est instancié à chaque requête vers la requête en question.

La classe de l'objet request dans le constructeur est une classe du framework Restlet. On peut récupérer ce qui correspond aux éléments entre accolade dans l'URI template en invoquant :

String wid = (String) request.getAttributes().get("wid");

La clé widest celle qui est définie dans la définition de l'URI template (dans le fichier plugin.xml).

Dans le constructeur, on distingue les cas où :

  • l'utilisateur n'est pas authentifié ;
  • l'identifiant ne correspond à aucun espace de travail ;
  • l'utilisateur connecté n'a pas le droit d'accéder à la ressource.

Dans ces cas de figure, on précise un code de retour HTTP (autre que 200). Aucune autre méthode de la classe n'est alors appelée.

Si tout va bien (l'espace de travail existe et l'utilisateur est authentifié et a accès à l'espace de travail), alors, la méthode getItemXmlRepresentation() est appelée et le résultat est envoyé dans le corps de la réponse HTTP (précédé de <?xml version='1.0' encoding='UTF-8' ?>).

Après avoir compilé la classe, redémarré et s'être connecté en tant qu'administrateur nous pouvons interroger l'url suivante :

http://<nom-de-domaine>/<context-path>/rest/workspace/j_4

Le résultat, sous forme XML, est affiché dans le navigateur (si ce n'est le cas, faites afficher la source).

In brief...

Cet article décrit JCMS Open API, une API de Services Web RESTful extensible. Un nombre important d'exemples illustre également comment, en utilisant une API utilitaire la JCMS Open API Client, on peut facilement interroger la JCMS Open API. Les principes des services web RESTful et un état de l'art de l'interopérabilité sont également brièvement décrits. 

Subject
Published

12/11/17

Writer
  • Benoît Dissert