Le module de gestion de la connexion de Salesforce vers Mercedes Cloud (3/3) : La récupération des informations des voitures

1.         Utilisation pour stocker / mettre à jour la liste des cars dans SF

a)              Un objet en mémoire pour récupérer le contenu de la réponse du serveur

Quand on demandait la voiture à la main avec Curl, on obtenait la réponse suivante

[
{
"id":                "00338353A46599799B",
"licenseplate":     "S-GG-9041",
"finorvin":         "1HM8CE2A6H6CEC802"
}
]

C’est un texte qui décrit la liste de voitures. Dans ce cas une seule voiture.

Pour pouvoir la manipuler, il faut définir un objet en mémoire dans Salesforce pour Apex.

class Vehicule {
public String salesforceId        { get; set; }
public String vehiculeId { get; set; }
public String licenseplate       { get; set; }
public String finorvin           { get; set; }
}

Et on va transformer chaque voiture de la liste dans un de ses objets.

b)              Le code qui analyse le contenu de la réponse du serveur et crée les voitures en mémoire

Du coup on va utiliser l’outil de Salesforce qui sert à lire un message JSON

JSON.deserializeUntyped( leMessage );

Et stocker en mémoire les voitures dans une liste

public List<Vehicule> vehicules { get; set; }

Comme pour l’appel à l’API précédente (session), on peut maintenant écrire le code qui appelle Mercedes, analyse la réponse et mémorise les voitures

public void loadVehiculesFromServer() {
  • Préparation de la requête
//Set HTTPRequest Method
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setHeader('authorization', 'Bearer '+access_token );
req.setHeader('accept', 'application/json');
req.setEndpoint(API_URL_Vehicules);
  • Appel de la requête
//Execute web service call here
Http http = new Http();
HTTPResponse res = http.send(req);
  • Récupération de la réponse
vehiculesJson = res.getBody();
System.debug('response:' + vehiculesJson);
  • Décodage de la réponse
List<Object> results =
(List<Object>)JSON.deserializeUntyped(vehiculesJson);

vehicules = new List<Vehicule>();

for(Object o:results)  {
Map<String,Object> m = (Map<String,Object>)o;
Vehicule v = new Vehicule();
v.vehiculeId = (String) m.get('id');
v.licenseplate = (String)  m.get('licenseplate');
v.finorvin =   (String) m.get('finorvin');
vehicules.add(v);
}
}

c)              Le code qui met à jour / crée les voitures dans la base SF à partir des voitures en mémoire

 

Ce qu’on veut maintenant c’est d’enregistrer les voitures qu’on vient d’obtenir dans Salesforce (mais sans créer des double à chaque appel). Pour ça on doit vérifier si la voiture est déjà là ou pas.

Cela se fait avec un programme qui  :

  1. lit les id Mercedes des voitures retournées par l’api Mercedes (ce qu’on vient de coder)
  2. charge les voitures de la base SF avec ces ID Mercedes pour cette connexion, et les place dans une carte
  3. pour chaque voiture retournée par l’API Mercedes
    1. regarde si elle est déjà dans la base salesforce
    2. sinon ; il prépare la création
    3. si oui ; il prépare la mise à jour des données dans la base salesforce
  4. il met à jour toutes les voitures qui existaient déjà
  5. il crée toutes celles qui n’existaient pas
public void storeVehiculesInSalesforce() {

//  Etape 1
Set<String> vehiculeMBIds = new Set<String>();
for (Vehicule v:vehicules) {
vehiculeMBIds.add(v.vehiculeId);
}

//  Etape 2
List<Car__c> listSFCars = [
SELECT Id, Mercedes_ID__c, Mercedes_API_Connection__c, Car_number__c
FROM Car__c
WHERE   Mercedes_API_Connection__c = :salesforceConnectionId
AND Mercedes_ID__c in :vehiculeMBIds
];

Map<String,Car__c> mapSFCars = new Map<String,Car__c>();
for (Car__c c:listSFCars) {
mapSFCars.put(c.Mercedes_ID__c, c);
}

//  Etape 3
List<Car__c> carsToCreate = new List<Car__c>();
List<Car__c> carsToUpdate = new List<Car__c>();

for (Vehicule v:vehicules) {
//  Etape 3a
Car__c theSFCar =  mapSFCars.get(v.vehiculeId);

if (theSFCar == null) {

//  Etape 3b
carsToCreate.add(
new Car__c(
Mercedes_API_Connection__c = salesforceConnectionId,
Mercedes_ID__c = v.vehiculeId,
Car_number__c = v.licenseplate
)
);

} else {

//  Etape 3c
theSFCar.Car_number__c = v.licenseplate;
carsToUpdate.add(theSFCar);
}

}

insert carsToCreate;
update carsToUpdate;

}

Maintenant à chaque fois qu’on appelle ce code, la liste des voitures dans Salesforce est mise à jour avec la réponse de Mercedes.

2.         Utilisation pour demander la position de toutes les voitures et les stocker dans la base SF

On va faire la même chose pour l’appel qui demande à Salesforce où se trouve chaque voiture.

a)              La réponse du serveur

En Curl, on obtenait la réponse suivante

{
"longitude": {"value":11.372609,"retrievalstatus":"VALID","timestamp":1548425864},
"latitude":  {"value":47.90321,"retrievalstatus":"VALID","timestamp":1548425864},
"heading":   {"value":52.520008,"retrievalstatus":"VALID","timestamp":1548425864}
}

b)              Un objet en mémoire pour récupérer le contenu de la réponse du serveur

On va aussi créer un objet Apex pour récupérer le résultat

class VehiculeLocation {
DateTime requestedOn { get; set; }
Decimal longitude    { get; set; }
Decimal latitude     { get; set; }
Decimal heading { get; set; }
}

c)              Un objet dans SF pour stocker la position

A chaque fois qu’on appelle la demande de position, on veut stocker cela comme enregistrement en plus. On a besoin de créer un nouvel objet dans Salesforce.

On l’appelle « Car Status », et il a les champs Position, Direction et date d’observation.

Voici ce que cela donne dans l’Object Manager.

d)              Des champs sur la voiture pour mémoriser la dernière position

On va en profiter aussi pour enregistrer la dernière position de la voiture dans l’objet voiture lui-même : un champ « Last Position ».

e)              Le code qui analyse le contenu de la réponse du serveur

Comme auparavant, on va appeler la requête et analyser le résultat. En rouge ce qui est particulier pour ce cas.

public VehiculeLocation getVehiculeLocation(String aVehiculeMbId) {

//Set HTTPRequest Method
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setHeader('authorization', 'Bearer '+access_token );
req.setHeader('accept', 'application/json');
req.setEndpoint(API_URL_Vehicules+'/'+aVehiculeMbId+'/location');

//Execute web service call here
Http http = new Http();
HTTPResponse res = http.send(req);

//Helpful debug messages
String jsonResponse = res.getBody();
System.debug('response:' + jsonResponse);

Map<String, Object> results =(Map<String, Object>)JSON.deserializeUntyped(jsonResponse);

VehiculeLocation vl = new VehiculeLocation();
vl.requestedOn = System.now();
vl.longitude= (Decimal) ((Map<String, Object>) results.get('longitude')).get('value');
vl.latitude= (Decimal) ((Map<String, Object>) results.get('latitude')).get('value');
vl.heading=(Decimal) ((Map<String, Object>) results.get('heading')).get('value');
System.debug('pos:'+vl);

return vl;
}

f)               Le code qui stocke dans la base SF les positions des voitures récupérées

Comme pour la liste des voitures, on va enregistrer les positions obtenues.

On va donc écrire un programme qui

  1. pour chaque véhicule
    1. appelle le serveur pour obtenir la position (le code du paragraphe précèdent)
    2. retrouve la voiture dans la base SF
    3. crée en mémoire l’objet Car_Status__c à stocker dans SF
    4. mémorise sur la voiture sa dernière position
  2. enregistre tous les objets Car_Status__c en une fois
  3. met à jour toutes les voitures (nouvelles positions) en une fois

public void getAndStoreVehiculeLocations() {

list<Car_Status__c> statusToInsert = new list<Car_Status__c>();

//  Etape 1
for (Vehicule v:vehicules) {

//  Etape 1a
VehiculeLocation vl = getVehiculeLocation(v.vehiculeId);

// Etape 1b
Car__c theCar = [
SELECT
Id,
Mercedes_ID__c,
Mercedes_API_Connection__c,
Car_number__c
FROM Car__c
WHERE Mercedes_API_Connection__c = :salesforceConnectionId
AND Mercedes_ID__c = :v.vehiculeId
];

// Etape 1c
statusToInsert.add(
new Car_Status__c(
Car__c = theCar.id,
Heading__c = vl.heading,
Location__longitude__s = vl.longitude,
Location__latitude__s = vl.latitude,
Requested_On__c = vl.requestedOn
)
);

// Etape 1d
theCar.Last_Position__longitude__s = vl.longitude;
theCar.Last_Position__latitude__s = vl.latitude;
carsToUpdate.add(theCar);
}

// Etape 2
insert statusToInsert;

// Etape 3
update(carsToUpdate);

}

3.         Enregistrer automatiquement toutes les 5 ‘ la position de la voiture

Si on veut suivre les voitures en permanence, il faut répéter l’enregistrement de la position régulièrement.

Ceci est le code pour une seule connexion qu’on veut exécuter toutes les 5 minutes (le code Apex qu’on a écrit avant, dans la classe Mercedes API, avec ‘a011t00000ByCf5AAF’ qui est l’identifiant de l’objet pour la connexion à Mercedes) :

MercedesAPI api = new MercedesAPI('a011t00000ByCf5AAF');
api.loadVehiculesFromServer();
api.getAndStoreVehiculeLocations();

Pour cela, on doit le placer dans une classe Apex programmable qui :

  1. Charge la liste des connexions, et pour chacune
  2. Fait les appels pour la liste des positions
global class MercedesStatusRequestScheduler implements Schedulable {

global void execute(SchedulableContext SC) {
//  Etape 1
for (Mercedes_API_Connection__c c : [SELECT Id FROM Mercedes_API_Connection__c]) {
//  Etape 2
doRequest(c.id);
}

}

    @future(callout=true)
    public static void doRequest(String theConnectionId) {
       MercedesAPI api = new MercedesAPI(theid);
       api.loadVehiculesFromServer();
       api.getAndStoreVehiculeLocations();
   }

}

Pour chaque exécution souhaitée (toutes les 5 ‘ d’une heure, donc 12 fois), on doit enregistrer l’exécution dans le tableau d’exécution SF.

Voici le code qui permet d’enregistrer une exécution

public static void schedule(String name, String cron) {
MercedesStatusRequestScheduler leJob =
new MercedesStatusRequestScheduler();
String jobID = system.schedule(name,cron,leJob);
}

Pour le programmer toutes les 5’ on doit enregistrer 12 programmations ainsi

schedule('Mercedes-Status xh00m', '0 0 * * * ? *');
schedule('Mercedes-Status xh05m', '0 5 * * * ? *');
schedule('Mercedes-Status xh10m', '0 10 * * * ? *');
schedule('Mercedes-Status xh15m', '0 15 * * * ? *');
schedule('Mercedes-Status xh20m', '0 20 * * * ? *');
schedule('Mercedes-Status xh25m', '0 25 * * * ? *');
schedule('Mercedes-Status xh30m', '0 30 * * * ? *');
schedule('Mercedes-Status xh35m', '0 35 * * * ? *');
schedule('Mercedes-Status xh40m', '0 40 * * * ? *');
schedule('Mercedes-Status xh45m', '0 45 * * * ? *');
schedule('Mercedes-Status xh50m', '0 50 * * * ? *');
schedule('Mercedes-Status xh55m', '0 55 * * * ? *');

4.         Amélioration : indiquer quelle connexion ou quelle voiture on veut suivre

Pour le moment, on ne peut pas activer désactiver l’enregistrement des positions.

Pour décider à chaque exécution :

  • si on veut charger les voitures d’une connexion
  • quelle voiture on veut interroger

On ajoute une checkbox :

  • à l’objet custom connection : Scheduled_Job_Execution__c
  • à l’objet custom car : Record_Status__c

Et on modifie le code ainsi

  • Quand on charge l’objet de connexion (Mercedes API connection) en mémoire, on mémorise si on doit faire l’enregistrement des positions
public boolean scheduledJobsExecution {get; set;  }
public MercedesAPI(Id aSalesforceConnectionId) {
…

scheduledJobsExecution = theConnection.Scheduled_Job_Execution__c;
}
  • Dans le code programmé toutes les 5 minutes, on vérifie si la connexion doit être enregistrée
@future(callout=true)
public static void doRequest(String theid) {
MercedesAPI api = new MercedesAPI(theid);
if (api.scheduledJobsExecution) {
api.loadVehiculesFromServer();
api.getAndStoreVehiculeLocations();
}
}

Dans le code d’interrogation et de stockage des statuts, on vérifie si la voiture doit être suivie pour sa position

public void getAndStoreVehiculeLocations() {
list<Car_Status__c> statusToInsert = new list<Car_Status__c>();
for (Vehicule v:vehicules) {
Car__c theCar = [
SELECT
Id,
Mercedes_ID__c,
Mercedes_API_Connection__c,
Car_number__c,
Record_Status__c
FROM Car__c
WHERE Mercedes_API_Connection__c = :salesforceConnectionId
AND Mercedes_ID__c = :v.vehiculeId
];
 
             if (theCar.Record_Status__c==true) {
VehiculeLocation vl = getVehiculeLocation(v.vehiculeId);
statusToInsert.add(new Car_Status__c(
Car__c = theCar.id,
Heading__c = vl.heading,
Location__longitude__s = vl.longitude,
Location__latitude__s = vl.latitude,
Requested_On__c = vl.requestedOn
)); 
            }

}

insert statusToInsert;
}

 

Et voilà maintenant, quand le code s’exécute, on n’accède à la connexion et on n’appelle l’enregistrement des voitures que si les cases sont cochées pour cette connexion et cette voiture.