Dans l’article précédent, nous avons vu comment nous connecter à une source de données externe (Strava) pour pouvoir récupérer des données dans Jahia. Dans cet article de blog, nous allons :
- détailler la récupération de données provenant de Strava (read),
- envoyer des données à Strava depuis Jahia (write).
Lecture de données
Nous souhaitons récupérer les activités sportives d’un compte Strava dans Jahia. Ces activités seront stockées sous forme de noeuds Jahia.
Strava, par l’intermédiaire de son API, nous permet de récupérér nos activités :
$ curl -G https://www.strava.com/api/v3/activities -d access_token=xxx -d per_page=10 | jq .
Ici nous récupérons un flux JSON, dans notre console, correspondant aux 10 dernières activités d’un utilisateur (identifié grâce à un token), que nous formattons grâce à l’utilitaire jq. Le token se récupère sur le site http://www.strava.com/developers en créant une application avec votre compte Strava. Le flux JSON est une liste d’activités.
[
{
"id": 409230323,
"resource_state": 2,
"external_id": "garmin_push_922486293",
"upload_id": 458731497,
"athlete": {
"id": 5907546,
"resource_state": 1
},
"name": "25' + (3x35\"+3x25\"+3x15\" en cotes) + 10'",
"distance": 10003.3,
"moving_time": 3055,
"elapsed_time": 3055,
"total_elevation_gain": 73,
"type": "Run",
"start_date": "2015-10-09T04:26:59Z",
...
},
{
"id": 408269294,
"resource_state": 2,
"external_id": "garmin_push_921231051",
"upload_id": 457740455,
"athlete": {
"id": 5907546,
"resource_state": 1
},
"name": "45' footing",
"distance": 10106.6,
"moving_time": 2833,
"elapsed_time": 2833,
"total_elevation_gain": 13,
"type": "Run",
"start_date": "2015-10-07T16:45:05Z",
...
},
...
]
On peut grâce à ce flux définir le mapping entre une activité Strava et un contenu Jahia (un noeud), que nous nommerons stravaActivity
dans le fichier definitions.cnd :
[jnt:stravaActivity] > jnt:content, jmix:structuredContent - id (string) hidden - name (string) - distance (string) - type (string) - moving_time (string) - start_date (string) - filename (string)
Créons notre provider sous forme de bean dans le fichier de configuration Spring strava-provider-writable.xml, de la même façon que dans l’article précédent :
<bean id="StravaWritableProvider" class="org.jahia.modules.external.ExternalContentStoreProvider" parent="AbstractJCRStoreProvider">
<property name="key" value="StravaWritableProvider"></property>
<property name="mountPoint" value="/sites/strava-site/contents/strava-activities"></property>
<property name="externalProviderInitializerService" ref="ExternalProviderInitializerService"></property>
<property name="extendableTypes">
<list>
<value>jnt:contentFolder</value>
<value>jnt:stravaActivity</value>
</list>
</property>
<property name="dataSource" ref="StravaDataSourceWritable"></property>
</bean>
<bean name="StravaDataSourceWritable" class="org.jahia.modules.strava.StravaDataSourceWritable" init-method="start">
<property name="apiKeyValue" value="${access_token}"></property>
<property name="apiKeyValuePost" value="${access_token_post}"></property>
</bean>
On retrouve dans cette définition :
- le point de montage (endroit où seront stockés les noeuds),
- les types de noeuds (
jnt:contentFolder
etjnt:stravaActivity
) que l’on souhaite traiter, - la classe (StravaDataSourceWritable) qui va implémenter le provider.
La classe ExternalDataSourceProviderWritable
va définir notre External Data Provider. Elle implémente l’interface ExternalDataSource
qui nous permet de lire les données provenant d’une source externe (Strava). Une des méthodes les plus importantes est la méthode getItemByIdentifier
qui fait le lien entre la définition d’un noeud et son identifiant dans le JCR (Java Content Repository). En voici une version raccourcie :
public ExternalData getItemByIdentifier(String identifier) throws ItemNotFoundException {
if (identifier.equals(“root”)) {
return new ExternalData(
identifier, "/", “jnt:contentFolder”, new HashMap<string , String[]>()
);
}
Map<string , String[]> properties = new HashMap<>();
String[] idActivity = identifier.split("-");
if (idActivity.length == 3) {
try {
JSONArray activities = getCacheStravaActivities(false);
// Find the activity by its identifier
int numActivity = Integer.parseInt(numActivity[0]) - 1;
JSONObject activity = (JSONObject) activities.get(numActivity);
// Add some properties
properties.put(NAME, new String[]{activity.getString(NAME)});
properties.put(DISTANCE, new String[]{activity.getString(DISTANCE)});
properties.put(TYPE, new String[]{activity.getString(TYPE)});
...
// Return the external data (a node)
ExternalData data = new ExternalData(
identifier, "/" + identifier, “jnt:stravaActivity”, properties
);
return data;
} catch (Exception e) {
throw new ItemNotFoundException(identifier);
}
} else {
// Node not again created
throw new ItemNotFoundException(identifier);
}
}
On définit un noeud de type dossier (jnt:contentFolder
) à la racine et dans les autres cas on reçoit l’identifiant d’une activité pour pouvoir créer un noeud de type jnt:stravaActivity
. La méthode getCacheStravaActivities
(expliquée ci-dessous) récupère la liste des activités. Suivant l’identifiant que la méthode reçoit, on peut retrouver l’activité correspondant et ainsi définir un noeud de type stravaActivity
avec les propriétés que l’on souhaite récupérer (distance, temps, nom, type …).
La méthode getCacheStravaActivites
se contente de faire un appel HTTP (basé sur la requête expliquée plus haut) sur le site de Strava, grâce au token d’identification, pour récupérer l’ensemble des activités de l’utilisateur. Un système de cache est mis en place pour permettre d’interroger Strava le moins souvent possible (mais ceci sort du scope).
Nous pouvons ainsi visualiser l’ensemble de nos activités sous forme de noeuds depuis l’explorateur de contenu Jahia.
Afin de pouvoir effectuer des recherches dans le JCR sur nos nouveaux contenus, la classe ExternalDataSourceProviderWritable
va implémenter l’interface ExternalDataSource.Searchable
disposant d’une seule méthode :
public List<string> search(ExternalQuery query) throws RepositoryException {
List<string> results = new ArrayList<>();
String nodeType = QueryHelper.getNodeType(query.getSource());
if (NodeTypeRegistry.getInstance().getNodeType(“jnt:stravaActivity”).isNodeType(nodeType)) {
try {
JSONArray activities = getCacheStravaActivities(false);
for (int i = 1; i < = activities.length(); i++) {
JSONObject activity = (JSONObject) activities.get(i - 1);
String path = "/" + StravaUtils.displayNumberTwoDigits(i) + "-" + ACTIVITY;
path += "-" + activity.get(ID);
results.add(path);
}
// paths contains all the path of the activities
// example of a path : /08-activity-401034489
} catch (JSONException e) {
throw new RepositoryException(e);
}
}
return results;
}
Cette méthode nous permet de récupérer tous les chemins dans le JCR pour nos activités.
Un peu d’affichage
Pour mettre nos activités en forme, nous allons créer un composant (noeud) stravaActivities
dans le fichier definitions.cnd :
[jnt:stravaActivities] > jnt:content, jmix:structuredContent
Nous associons une vue (stravaActivities.jsp) à notre composant, qui va récupérer les activités stockées dans le JCR (grâce à une requête SQL-2) et les afficher sous forme de tableaux :
<jcr:sql var="res" sql="select * from [jnt:stravaActivity]">
<table id="activitiesTable" class="table table-striped table-bordered">
<thead>
<th class="strava-align">Type</th>
<th class="strava-align">Date</th>
<th>Activity name</th>
<th class="strava-align">Distance</th>
<th class="strava-align">Time</th>
</thead>
<tbody>
<c:foreach items="${res.nodes}" var="stravaActivity" varStatus="status">
<tr>
<td class="strava-align">${stravaActivity.properties['type'].string}</td>
<td class="strava-align">${stravaActivity.properties['start_date'].string}</td>
<td>
<a href="https://www.strava.com/activities/${stravaActivity.properties['id'].string}">
${stravaActivity.properties['name'].string}
</a>
</td>
<td class="strava-align">${stravaActivity.properties['distance'].string}</td>
<td class="strava-align">${stravaActivity.properties['moving_time'].string}</td>
</tr>
</c:foreach>
</tbody>
</table>
</jcr:sql>
Nous pouvons désormais déployer notre module sur un site et créer une page où l’on ajoute notre composant stravaActivities
pour visualiser le résultat suivant :
Pour disposer de ce rendu, le module bootstrap de Jahia a été déployé sur le site.
Alimentation de données
Nous allons maintenant voir comment alimenter notre source de données grâce à la création de nouveaux contenus Jahia via notre External Data Provider.
Dans notre exemple, nous voulons pouvoir ajouter des nouvelles activités Strava en créant simplement un noeud de type stravaActivity
. Cette création de noeud entraînera un téléchargement de l’activité sur le site de Strava.
Pour cela, notre External Data Provider va implémenter l’interface ExternalDataProvider.Writable
qui définit quatre méthodes :
move
,order
,removeByItem
,saveItem
.
La méthode la plus importante est la méthode saveItem
qui va recevoir un ExternalData (dans notre exemple un noeud Jahia de type stravaActivity
) avec les propriétés du noeud modifiées.
Cette méthode va contenir le code qui permet d’uploader une activité sur Strava. Pour uploader une activité sur Strava, l’utilisateur doit fournir un fichier de type GPX, TCX ou FIT. Ces fichiers sont écrits au format XML. Voici un exemple simplifié de fichier GPX :
< ?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" ...>
<metadata>
<link href="connect.garmin.com"/>
<text>Garmin Connect</text>
<time>2015-10-02T19:00:37.000Z</time>
</metadata>
<trk>
<name>La Noctambule : 10km RP</name>
<desc>Record 10km</desc>
<trkseg>
<trkpt lon="2.2641055192798376" lat="48.901649694889784">
<ele>51.0</ele>
<time>2015-10-02T19:00:37.000Z</time>
<extensions>
<gpxtpx:trackpointextension>
<gpxtpx:hr>88</gpxtpx:hr>
</gpxtpx:trackpointextension>
</extensions>
</trkpt>
<trkpt lon="2.264131586998701" lat="48.90167106874287">
<ele>51.0</ele>
<time>2015-10-02T19:00:38.000Z</time>
<extensions>
<gpxtpx:trackpointextension>
<gpxtpx:hr>88</gpxtpx:hr>
</gpxtpx:trackpointextension>
</extensions>
</trkpt>
…
</trkseg></trk></gpx>
Pour pouvoir uploader des fichiers sur Strava, un token d’accès en écriture est nécessaire. La récupération de ce token se fait ainsi :
- Aller sur https://www.strava.com/settings/api pour récupérer son ID Client, son Secret Client et son Domaine d’Autorisation,
- Lancer dans un navigateur
https://www.strava.com/oauth/authorize?client_id=xx&response_type=code&redirect_uri=yy&approval_prompt=force&scope=write - Accepter la confirmation de demande d’autorisation,
- Une redirection est faite sur une nouvelle URL. Cette URL dispose d’un paramètre code que vous pouvez récupérer,
- Lancer dans un terminal : $ curl -X POST https://www.strava.com/oauth/token -F client_id=xx -F client_secret=yy -F code=zz
- Le token est fourni en réponse.
L’API Strava nous permet l’upload de fichier de cette manière :
$ curl -X POST https://www.strava.com/api/v3/uploads -H "Authorization: Bearer xxx" -F file=@la-noctambule.gpx -F data_type=gpx
où xxx est l’access token d’écriture que nous avons récupéré.
On peut ainsi créer un noeud de type stravaActivity
, via l’explorateur de contenu Jahia, et juste remplir la propriété filename
(avec le chemin de notre fichier).
Une fois le nom de fichier spécifié, on clique sur Save ce qui va avoir pour conséquence d’appeler la méthode saveItem
. Cette méthode reçoit le nom du fichier pour permettre de réaliser un appel HTTP de type POST sur Strava pour uploader l’activité.
De plus, l’appel de cette méthode crée automatiquement un nouveau noeud de type stravaActivity
. L’utilisateur pourrait compléter les champs nom, distance, type, … mais on préfère que ces informations soient remplies par Strava.
Voici une version simplifiée de la méthode saveItem
:
public void saveItem(ExternalData data) throws RepositoryException {
...
// On récupère la propriété filename du noeud stravaActivity que l’on créé
String filename = data.getProperties().get(FILENAME)[0];
...
// Post http sur Strava pour uploader le fichier
HttpPost httpPost = new HttpPost(“https://www.strava.com/api/v3/uploads”);
...
httpClient.execute(httpPost);
...
// On rafraichit l’ensemble des noeuds
getCacheStravaActivities(true);
}
En fin de méthode, nous rafraîchissons les noeuds de type stravaActivity
pour réordonner les activités avec la nouvelle créée et remplir automatiquement les propriétés (nom, distance, temps, type d’activité, date de début) de la nouvelle activité.
Conclusion
Cet article nous a permis de voir la façon avec laquelle on peut communiquer entre Jahia et une source de données externe en implémentant un External Data Provider.
La suite logique de cet article serait d’implémenter les méthodes move, order et removeByItem et de modifier la méthode saveItem pour permettre la modification d’un contenu en plus de la création que nous avons géré.
Pour aller plus loin, on peut regarder du côté de l’interface ExternalDataSource pour personnaliser notre External Data Provider via d’autres interfaces :
- public interface LazyProperty
- public interface Searchable
- public interface Writable
- public interface Initializable
- public interface AccessControllable
- public interface Referenceable
- public interface CanLoadChildrenInBatch
- public interface CanCheckAvailability
Le code complet de mon exemple de module est disponible sur mon Github.