Description
Continuing the conversation of connectivity, Stephen will provide you with a walkthrough of using WebService Before and After Operation connector rules to aggregate data when the provider’s data is in a state that is hard or impossible to tie to the Account itself, as well as to cache data from aggregation start to aggregation end to reduce network use and avoid API rate limits.
API Layout
each of my API endpoints can be used as bulk endpoints, or singular get-by-ID.
each Employee is in the form of:
{
"id": 1,
"firstName": "Rutter",
"lastName": "Mailey",
"emailAddress": "[email protected]",
"companyId": 3,
"departmentId": 15,
"WorkType": 2,
"jobId": 10
}
and each Job, Company, and Department are in the form of:
{
"id": "2",
"title": "Batman"
}
Additional Resources
Web Service After Operation Rule
the source code below can be applied as a Web Services After Operation Rule to:
- use temporary storage (transientValues) data if present
- call additional APIs and parse/store their values
- the RestClient operates in the context of the connector itself, inheriting authentication, headers, etc. there is no need to hard-code any credentials or manually handle authentication
- iterating over each User from the paginated data, append additional data to the response, such as a job title
import org.json.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import connector.common.JsonUtil;
import connector.common.JsonUtil.*;
import sailpoint.object.*;
import connector.common.Util;
/*
* CREATED BY: Stephen Holinaty
* Date: 4/1/2024
* Purpose: Demo code for Developer Days 2024, presenting how Web Service After Operation
* rules can be used for efficiently calling, and using temporary storage to
* utilize, additional APIs to overload the standard output with additional data
*/
//setting a log prefix allows us to easily filter ccg.log for relevant items
String logPrefix = "WSAO_DevDays2024";
//retrieve the transient values, if they exits, from application storage. will be Null if not set.
Map transientValues = (Map) application.getAttributeValue("transientValues");
//build our return object structure
List returnObj = new ArrayList();
Map processedData = new HashMap();
log.info(logPrefix + " starting to process");
//only call additional APIs if we do not already have them in memory
if(transientValues == null) {
///////// START LOOKUPS ///////////
Map transientValues = new HashMap();
//set up the requirements for making API calls
List<String> allowedStatuses = new ArrayList();
allowedStatuses.add( "2**" );
Map header = requestEndPoint.getHeader();
//retrieve the configured Base URL from the Source
String baseUrl = application.getAttributeValue("genericWebServiceBaseUrl");
//call the jobs API, build a key/value Map of job IT : job Title
Map JobCodeLookupMap = new HashMap();
try{
String jobsurl = baseUrl + "/jobs";
//executeGet operates in the context of the source restClient.
//will follow Authentication on the source/endpoint.
String jobs = restClient.executeGet(jobsurl, header, allowedStatuses );
JSONArray jobsJSONArray = new JSONArray(jobs);
for (int i = 0; i < jobsJSONArray.length(); i++) {
JSONObject jobJSONObject = jobsJSONArray.getJSONObject(i);
String jobId = jobJSONObject.getString("id");
String jobTitle = jobJSONObject.getString("title");
JobCodeLookupMap.put(jobId,jobTitle);
}
//store the resultant map into transient values
transientValues.put("JobCodeLookupMap", JobCodeLookupMap);
} catch (Exception e) {
log.error(logPrefix + "********ERROR AFTER RULE: " + e.getMessage(), e );
}
//repeat for the company API
Map companyLookupMap = new HashMap();
try{
String companyUrl = baseUrl + "/company";
String companies = restClient.executeGet(companyUrl, header, allowedStatuses );
JSONArray companyJSONArray = new JSONArray(companies);
for (int i = 0; i < companyJSONArray.length(); i++) {
JSONObject companyJSONObject = companyJSONArray.getJSONObject(i);
String companyId = companyJSONObject.getString("id");
String companyName = companyJSONObject.getString("companyName");
companyLookupMap.put(companyId,companyName);
}
transientValues.put("companyLookupMap", companyLookupMap);
} catch (Exception e) {
log.error(logPrefix + "********ERROR AFTER RULE: " + e.getMessage(), e );
}
//repeat for the department API
Map departmentLookupMap = new HashMap();
try{
String departmenturl = baseUrl + "/department";
String departments = restClient.executeGet(departmenturl, header, allowedStatuses );
JSONArray departmentJSONArray = new JSONArray(departments);
for (int i = 0; i < departmentJSONArray.length(); i++) {
JSONObject departmentJSONObject = departmentJSONArray.getJSONObject(i);
String departmentId = departmentJSONObject.getString("id");
String departmentName = departmentJSONObject.getString("name");
departmentLookupMap.put(departmentId,departmentName);
}
transientValues.put("departmentLookupMap", departmentLookupMap);
} catch (Exception e) {
log.error(logPrefix + "********ERROR AFTER RULE: " + e.getMessage(), e );
}
//store "our" transient values into the application (until the aggregation completes, wherein it will erase)
application.setAttribute("transientValues", transientValues);
log.error(logPrefix + "setting transients");
}
/////// END LOOKUPS //////////
// ensure that we have data to actuall operate on
if(rawResponseObject != null && processedResponseObject != null){
//retrieve the transientValues from application storage
transientValues = (Map) application.getAttributeValue("transientValues");
//iterate over every Record in the response
// processedResponseObject will have the JSONPath values defined on the source endpoint.
for (Map employeeEntry : processedResponseObject) {
log.error(logPrefix + "updating processed data");
// grab key values from the record, from the JSONPath'd output of the endpoint itself
String jobId = employeeEntry.get("jobId");
String companyId = employeeEntry.get("companyId");
String departmentId = employeeEntry.get("departmentId");
//null safety. dont look it up if it doesnt exist.
if (transientValues.containsKey("JobCodeLookupMap")){
HashMap JobCodeLookupMap = (HashMap) transientValues.get("JobCodeLookupMap");
//only try to add the value if it actually exists.
if (JobCodeLookupMap.containsKey(jobId)){
employeeEntry.put("jobTitle",JobCodeLookupMap.get(jobId));
} else{
log.error(logPrefix + "lookup failed for jobcode: " + jobId);
}
}
//repeat for Company code
if (transientValues.containsKey("companyLookupMap")){
HashMap companyLookupMap = (HashMap) transientValues.get("companyLookupMap");
if (companyLookupMap.containsKey(companyId)){
employeeEntry.put("companyName",companyLookupMap.get(companyId));
}else{
log.error(logPrefix + "lookup failed for company code: " + companyId);
}
}
//repeat for department
if (transientValues.containsKey("departmentLookupMap")){
HashMap departmentLookupMap = (HashMap) transientValues.get("departmentLookupMap");
if (departmentLookupMap.containsKey(departmentId)){
employeeEntry.put("departmentName",departmentLookupMap.get(departmentId));
}else{
log.error(logPrefix + "lookup failed for department: " + departmentId);
}
}
//add the modified record to our new return result
returnObj.add(employeeEntry);
}
//add the data to the reply, and send it
processedData.put("data", returnObj);
log.info(logPrefix + "finished running");
return processedData;
} else{
log.error(logPrefix + "Response Object was empty, standard endpoint data will be passed.");
}