Record SF LMS External Learning Events via BTP/CPI using OData API

This post is to demonstrate how to record external learning event using BTP/CPI (sorry I am confused with the exact wordings like BTP/CPI/Integration Suite/Cloud Integration/Integration Flow/…) as they keep changing from time to time.

Why this blog is out:

Since the LMS import data tool as well as Learning History connector can only record internal learning event, so API development is the only for the external learning event import, else you have to make in LMS frontend….nightmare.

Notes before deep-diving:

In the BTP/CPI, the message body should be in XML format instead of JSON format

  • At the beginning I refer KBA#2421887 – LMS – Admin/supervisor/user records external event through OData API (Product Enhancement b1702 – LRN-15619) and make a POC via Postman which the message body is in JSON format
  • However, when I put the same to BTP/CPI, it doesn’t work with error message below
  • Later on I change it in XML format, then it results another error message (but more meaningful), which means I am finally on the right track

Limitation on this API (ie. Record Learning Event to Learning History Web Service)

  • Call limiting: 100 calls per min
  • Array limiting: 10 records per each call

Details please find: https://help.sap.com/viewer/5aab9bef78fc4c4fa199c1f7aa142720/2111/en-US/e568f59a0fa144f2b7599970a2a86bcd.html

How to enable the LMS Webservices and obtain OAuth Token

  • KBA#2279128 – Enable API Webservices LMS data in LMS instance
  • KBA#2318897 – LMS ODATA Webservices Knowledge Support and Tips

How to connect LMS with BTP/CPI

Which service should be used for ‘External’ Learning Event recording?

https://help.sap.com/viewer/5aab9bef78fc4c4fa199c1f7aa142720/2111/en-US/90505b0d331049e5afff352911ea64c3.html

You can find there are 2 micro-services for admin/user:

  • learningevent-service/v1
  • learningEvent/v1

From the Learning OData API Reference guide (see above) as well as KBA#2421887 – LMS – Admin/supervisor/user records external event through OData API (Product Enhancement b1702 – LRN-15619), only micro-service learningevent-service/v1 supports external learning event recording.

iFlow Design

The original design is to extract learning records from legacy learning system and post to LMS as external learning events (It is simpler version as there is not much resources to have a completed end-to-end flow from sync-ing items, learning assignments, and learning events at this moment)

So the design in my demo iflow:

  1. Triggered by external API call
  2. Query (GET) student/user records from LMS in order to mimic the record extraction from legacy and transform it to corresponding external learning event format (in XML) referring the metadata
  3. Splitting the message so that only 10 records will be processed for each call (ie. LMS API limitation)
  4. Record (POST) external learning event records

*Only focus on the yellow-arrow flow, while others are built for testing during development which can be ignored.

Step 1: Triggered by external API call

It is pretty simple by creating a HTTPS Sender Adapter with defining the address so that the iflow can be called (eg. by Postman).

In this example I have defined the address /LMSpartlms0035/postExtLearningEvent, and I will use Postman to trigger the iflow via the URL:

https://xxxxxxxxtrial.it-cpitrial03-rt.cfapps.ap21.hana.ondemand.com/http/LMSpartlms0035/postExtLearningEvent

(you are free to use other triggering approaches, like using Timer event to schedule run)

Step 2: Query (GET) student/user records from LMS in order to mimic the record extraction from legacy and transform it to corresponding external learning event format (in XML) referring the metadata

For this step, you are recommended to refer the scenario 1 in https://blogs.sap.com/2020/09/17/successfactors-integrations-beginners-guide-exploring-learning-management-system-continued/ on how to:

  1. Create OAuth2 credential in CPI
  2. Query data from LMS API (in the above blog it queries scheduledoffering-service while I query searchStudent instead, and only 10 student records will be returned)

*My demo will not really use the data resulted from the searchStudent API but only using the returned record count to mimic message splitting requirement.

import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import groovy.xml.*;
import groovy.util.*; def Message processData(Message message)
{ def currentDT = new Date(); def body = message.getBody(java.lang.String) as String; def Students = new XmlSlurper().parseText(body); int i = 0; def writer = new StringWriter(); def OutputBody = new MarkupBuilder(writer); OutputBody.mkp.xmlDeclaration(version: "1.0", encoding: "utf-8"); OutputBody.ExternalLearningEvents() { ExternalLearningEvent{ //Loop for each Student Element for(def Row: Students.Student) { i = i + 1; //Mapping externalLearningEvents{ element{ description(Row.studentID.text()) studentID('adminCW') completionDate(currentDT.getTime()) completionTimeZoneID('GMT') grade('A') creditHours('0.5') cpeHours('0.5') contactHours('0.5') totalHours('0.5') instructorName('BTP') comments(i.toString()) } } } } } message.setBody(writer.toString()); return message;
} //Sample XML
//<?xml version="1.0" encoding="UTF-8"?>
//<ExternalLearningEvents>
// <ExternalLearningEvent>
// <externalLearningEvents>
// <element>
// <description>External Learning Event 1</description>
// <studentID>adminCW</studentID>
// <completionDate number="true">1641357063000</completionDate>
// <completionTimeZoneID>US/Eastern</completionTimeZoneID>
// <grade>Test Grade</grade>
// <creditHours number="true">7</creditHours>
// <cpeHours number="true">0.5</cpeHours>
// <contactHours number="true">0.5</contactHours>
// <totalHours number="true">0.5</totalHours>
// <instructorName>Test Instructor</instructorName>
// <comments>Test Comment From Admin</comments>
// </element>
// </externalLearningEvents>
// <externalLearningEvents>
// <element>
// <description>External Learning Event 2</description>
// <studentID>adminCW</studentID>
// <completionDate number="true">1641357063000</completionDate>
// <completionTimeZoneID>US/Eastern</completionTimeZoneID>
// <grade>Test Grade</grade>
// <creditHours number="true">7</creditHours>
// <cpeHours number="true">0.5</cpeHours>
// <contactHours number="true">0.5</contactHours>
// <totalHours number="true">0.5</totalHours>
// <instructorName>Test Instructor</instructorName>
// <comments>Test Comment From Admin</comments>
// </element>
// </externalLearningEvents>
// </ExternalLearningEvent>
//</ExternalLearningEvents>

Then I use a groovy script to:

  1. Loop the student records, which is in XML format, from previous searchStudent API call
  2. Transform into external learning event record for each student record in XML format (the required format can be checked by the metadata https://xxxxxxxx.scdemo.successfactors.eu/learning/odatav4/public/admin/learningevent-service/v1/$metadata

*Again, you can refer scenario 2 from https://blogs.sap.com/2020/09/17/successfactors-integrations-beginners-guide-exploring-learning-management-system-continued/ on how to build the correct format by referring the metadata file

Step 3: Splitting the message so that only 10 records will be processed for each call (ie. LMS API limitation)

In this step, a general splitter is used to split the message for every 10 externalLearningEvents records are reached.

*You can refer https://blogs.sap.com/2020/09/21/sap-cloud-platform-integration-general-splitter/ on the general splitter usage

Step 4: Record (POST) external learning event records

In this step, create a SuccessFactors OData v4 adapter to call the learningevent-service API.

Testing

Postman is used to call the iflow URL (1), the request message body (2) is ignored in my demo. And the response body (3) is exactly the response body from LMS API.

From the response body (3), you can see external learning events are created with completion date 1641380378089 (which is unix epoch timestamp) in GMT timezone. It is converted into:

GMT: Wednesday, January 5, 2022 10:59:38.089 AM
Your time zoneWednesday, January 5, 2022 6:59:38.089 PM GMT+08:00

(I use https://www.epochconverter.com for the conversion)

In SuccessFactors LMS, external learning events completed on 5 Jan 2022 06:59PM Asia/Hong Kong (aka GMT+8) are found.

Challenges to Me

  • I am new to BTP/CPI iflow development, it takes time for me to figure out the iflow message concept (eg. message header/body/exchange properties)
  • I am new to Web development, it takes time for me to get familiar with XML/JSON usage, groovy script

More to Go

  • Exception handling (in case the POST call is failed, instead of acknowledging them in BTP/CPI monitor, more human-readable way should be built for end-user)
  • Message mapping (in my demo, a groovy script is used to build POST call message body. But for a real integration scenario, message mapping between the learning records from legacy and learning records in LMS should be made. Learnt there are some many message mapping approaches like WSDL, XSLT, scripting. But which one is better to use? any limitation?)

Last, thanks again.. (and what I have referred during the POC)

Last….I need your input

I am new to BTP/CPI iflow development as well as web development. I do believe there should be a more professional/friendly/easy way for the same purpose. Please kindly comment and share your experience.