Jahia : External Data Provider (deuxième partie)

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 et jnt: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.

Jahia - Activités

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 :

Jahia - Visualisation

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 :

  1. Aller sur https://www.strava.com/settings/api pour récupérer son ID Client, son Secret Client et son Domaine d’Autorisation,
  2. Lancer dans un navigateur
    https://www.strava.com/oauth/authorize?client_id=xx&response_type=code&redirect_uri=yy&approval_prompt=force&scope=write
  3. Accepter la confirmation de demande d’autorisation,
  4. Une redirection est faite sur une nouvelle URL. Cette URL dispose d’un paramètre code que vous pouvez récupérer,
  5. Lancer dans un terminal : $ curl -X POST https://www.strava.com/oauth/token -F client_id=xx -F client_secret=yy -F code=zz
  6. 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).

Création d'un contenu Jahia

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.