Set data value to lower case with a Web Services After Operation rule

I’m looking for guicance on what I may be doing wrong. If I have a syntax error, the aggregation will fail. So the rule seems to be running, but I never seem to be able to update the email and loginId values to be all lowercase.

The schema and naming is correct. I validated it with a Python script capturing the response JSON.

Below is the rule:

import sailpoint.object.Application;

import sailpoint.connector.webservices.EndPoint;

import sailpoint.connector.webservices.WebServicesClient;

import sailpoint.tools.Util;

import java.util.Map;

import java.util.List;

import java.util.ArrayList;

import java.util.HashMap;

Map execute(

Application application,

EndPoint requestEndPoint,

List processedResponseObject,

String rawResponseObject,

WebServicesClient restClient) {



log.error("AfterOperationRule: Entering rule with Rebuild Strategy.");



if (processedResponseObject == null || processedResponseObject.isEmpty()) {

    log.error("AfterOperationRule: processedResponseObject is null or empty. Exiting.");

    return null;

}



// 1. Create a new, empty list to hold our editable account maps.

List newList = new ArrayList();



for (Map account : processedResponseObject) {

    

    // 2. For each account, create a new, editable HashMap by copying the original.

    Map newMutableAccount = new HashMap(account);



    // --- Logic for handling the email attribute on the NEW map ---

    String emailAttributeName = "email";

    if (newMutableAccount.containsKey(emailAttributeName)) {

        Object emailValue = newMutableAccount.get(emailAttributeName);

        if (emailValue != null && emailValue instanceof String) {

            String originalEmail = (String) emailValue;

            String normalizedEmail = originalEmail.trim().toLowerCase();

            if (!originalEmail.equals(normalizedEmail)) {

                log.error("AfterOperationRule: Updating email on mutable copy.");

                newMutableAccount.put(emailAttributeName, normalizedEmail);

            }

        }

    }

    

    // --- Logic for handling the loginId attribute on the NEW map ---

    String loginIdAttributeName = "loginId";

    if (newMutableAccount.containsKey(loginIdAttributeName)) {

        Object loginIdValue = newMutableAccount.get(loginIdAttributeName);

        if (loginIdValue != null && loginIdValue instanceof String) {

            String originalLoginId = (String) loginIdValue;

            String normalizedLoginId = originalLoginId.trim().toLowerCase();

            if (!originalLoginId.equals(normalizedLoginId)) {

                log.error("AfterOperationRule: Updating loginId on mutable copy.");

                newMutableAccount.put(loginIdAttributeName, normalizedLoginId);

            }

        }

    }



    // 3. Add the fully modified, mutable account to our new list.

    newList.add(newMutableAccount);

}



// 4. Create the final map to return.

Map returnMap = new HashMap();



// 5. Put our NEW list (not the original) into the map under the "items" key.

returnMap.put("items", newList);



log.error("AfterOperationRule: Exiting rule. Returning rebuilt map with key 'items'.");

return returnMap;

}
``

Here is a sample output of the JSON response.

```
{
“allowAdhocMatching”: false,
“allowIntercompanySettlement”: false,
“allowUserMentions”: true,
“allowUserToEditIntercompanyConfig”: false,
“allowUserToEditJournalConfig”: false,
“annualHours”: 0,
“created”: “8/5/2022 3:11:28 PM”,
“defaultRoleId”: null,
“email”: “[email protected]”,
“firstName”: “Olga”,
“fullName”: “Smith, Olga”,
“id”: 27,
“isActive”: true,
“jobId”: null,
“jobTitle”: null,
“lastName”: “Smith”,
“lastUpdated”: “12/26/2024 2:56:13 PM”,
“loginId”: “[email protected]”,
“phoneNumber”: null,
“referenceField1”: “”,
“referenceField2”: “”,
“referenceField3”: “”,
“requiresJournalReviewer”: false,
“supervisor”: null,
“timeZoneId”: 15,
“allowUserToAccessJournalAnalyser”: false
}
```

the source connector is using items.

try this

returnMap.put(“data”, newList);

1 Like

Hi Satish, thank you for the reply and welcome to the Development Community.

that unfortunately didn’t help, but your logic checks out as to why we would make the change. I have added some debug logging and waiting for the customer to get me the ccg logs.

Hello @ts_fpatterson,

A couple of things that I have noticed, you’re importing EndPoint from the connectors instead of Endpoint, and you’re putting the result in items instead of data. Also, you didn’t call your function (maybe what you shared is a chunk of the code), so maybe that’s another reason too.

Funny thing, the documentation is wrong when referencing it as EndPoint but the JavaDoc doesn’t lie :smiley:

I made some changes to your code and removed unused imports and variables :slight_smile:

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;

    public Map execute(List processedResponseObject) {
        log.error("AfterOperationRule: Entering rule with Rebuild Strategy.");

        if (processedResponseObject == null || processedResponseObject.isEmpty()) {
            log.error("AfterOperationRule: processedResponseObject is null or empty. Exiting.");
            return null;
        }

        // 1. Create a new, empty list to hold our editable account maps.
        List newList = new ArrayList();
        for (Map account : processedResponseObject) {
            // 2. For each account, create a new, editable HashMap by copying the original.
            Map newMutableAccount = new HashMap(account);
            // --- Logic for handling the email attribute on the NEW map ---

            String emailAttributeName = "email";
            if (newMutableAccount.containsKey(emailAttributeName)) {
                Object emailValue = newMutableAccount.get(emailAttributeName);

                if (emailValue != null && emailValue instanceof String) {

                    String originalEmail = (String) emailValue;

                    String normalizedEmail = originalEmail.trim().toLowerCase();

                    if (!originalEmail.equals(normalizedEmail)) {
                        log.error("AfterOperationRule: Updating email on mutable copy.");
                        newMutableAccount.put(emailAttributeName, normalizedEmail);
                    }
                }
            }


            // --- Logic for handling the loginId attribute on the NEW map ---

            String loginIdAttributeName = "loginId";
            if (newMutableAccount.containsKey(loginIdAttributeName)) {
                Object loginIdValue = newMutableAccount.get(loginIdAttributeName);
                if (loginIdValue != null && loginIdValue instanceof String) {
                    String originalLoginId = (String) loginIdValue;
                    String normalizedLoginId = originalLoginId.trim().toLowerCase();
                    if (!originalLoginId.equals(normalizedLoginId)) {
                        log.error("AfterOperationRule: Updating loginId on mutable copy.");
                        newMutableAccount.put(loginIdAttributeName, normalizedLoginId);
                    }
                }
            }

            // 3. Add the fully modified, mutable account to our new list.
            newList.add(newMutableAccount);
        }

        // 4. Create the final map to return.
        Map returnMap = new HashMap();

        // 5. Put our NEW list (not the original) into the map under the "items" key.
        returnMap.put("data", newList);
        log.error("AfterOperationRule: Exiting rule. Returning rebuilt map with key 'data'.");
        return returnMap;
    }

    return execute(processedResponseObject);

I hope it works :smiley:

thanks for the assistance. We are still having issues.

Below is my current code with changes and some debug logging. Hope to get the ccg files next week. I believe the code is consistent with the requirements of the web services connector and the javadocs

```
import java.util.Map;

import java.util.List;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.Set; // Added for the diagnostic log

public Map execute(List processedResponseObject) {

log.error("AfterOperationRule: Entering rule.");



if (processedResponseObject == null || processedResponseObject.isEmpty()) {

    log.error("AfterOperationRule: processedResponseObject is null or empty. Exiting.");

    return null;

}



// --- DIAGNOSTIC BLOCK ADDED HERE ---

// This is the most important log. It will print the exact keys (attribute names)

// that the rule is receiving from the connector's Response Mapping.

try {

    Map firstAccount = (Map) processedResponseObject.get(0);

    if (firstAccount != null) {

        Set<String> keys = firstAccount.keySet();

        log.error("DIAGNOSTIC: Keys the rule received in the first account map are: " + keys.toString());

    }

} catch (Exception e) {

    log.error("DIAGNOSTIC: Could not inspect the first account to log its keys.", e);

}

// --- END DIAGNOSTIC BLOCK ---



log.error("AfterOperationRule: Received object of type: " + processedResponseObject.getClass().getName());

log.error("AfterOperationRule: Raw processedResponseObject: " + processedResponseObject.toString());



List accountsList = new ArrayList();



// Your defensive check for nested lists (harmless to keep)

if (processedResponseObject.get(0) instanceof Map) {

    Map firstElement = (Map) processedResponseObject.get(0);

    String\[\] possibleListKeys = {"data", "items", "results", "users", "accounts", "Resources"};

    for (String key : possibleListKeys) {

        if (firstElement.get(key) instanceof List) {

            accountsList = (List) firstElement.get(key);

            log.error("AfterOperationRule: Found nested list of accounts under key: '" + key + "'");

            break;

        }

    }

}



if (accountsList.isEmpty()) {

    accountsList = processedResponseObject;

    log.error("AfterOperationRule: No nested list found. Processing the object as the primary list of accounts.");

}



List newList = new ArrayList();

for (Map account : accountsList) {

    Map newMutableAccount = new HashMap(account);



    // Your existing lowercase logic

    String emailAttributeName = "email";

    if (newMutableAccount.containsKey(emailAttributeName)) {

        Object emailValue = newMutableAccount.get(emailAttributeName);

        if (emailValue != null && emailValue instanceof String) {

            String originalEmail = (String) emailValue;

            String normalizedEmail = originalEmail.trim().toLowerCase();

            if (!originalEmail.equals(normalizedEmail)) {

                log.error("AfterOperationRule: Normalizing email for account: " + newMutableAccount.get("loginId"));

                newMutableAccount.put(emailAttributeName, normalizedEmail);

            }

        }

    }



    String loginIdAttributeName = "loginId";

    if (newMutableAccount.containsKey(loginIdAttributeName)) {

        Object loginIdValue = newMutableAccount.get(loginIdAttributeName);

        if (loginIdValue != null && loginIdValue instanceof String) {

            String originalLoginId = (String) loginIdValue;

            String normalizedLoginId = originalLoginId.trim().toLowerCase();

            if (!originalLoginId.equals(normalizedLoginId)) {

                log.error("AfterOperationRule: Normalizing loginId for account: " + originalLoginId);

                newMutableAccount.put(loginIdAttributeName, normalizedLoginId);

            }

        }

    }



    newList.add(newMutableAccount);

}



Map returnMap = new HashMap();

returnMap.put("data", newList);

log.error("AfterOperationRule: Exiting rule. Returning rebuilt map.");

return returnMap;

}
```

@ts_fpatterson ,
There’s something weird that happens with WS connectors it’s VA caching of the configuration. Even if you make changes it doesn’t apply, what I recommend that you do is this : Rename the Http Operation you changed (add a number to it) and then duplicate it and delete the first one, save the connector and run your test.

This makes ISC think that it’s a new config and invalidates the cache. [ Important note : this is just an assumption but it works :slight_smile: ]

Lemme give you a quick “Hack” that I have been doing since forever to get the “logs” without CCG. This hack only works for the following Operations : Test Connection, Account / Entitlement aggregations, Account Create/Update/Grant Access/ Remove Access.

In my After Rule I do this, and when running Account Aggregation I get an exception with the full details :slight_smile:

String LOG = "DATA=\n\n";

LOG += requestEndPoint.getFullUrl();
LOG += "\n\n";

LOG += requestEndPoint.getBody();
LOG += "\n\n";

LOG += requestEndPoint.getBaseUrl();
LOG += "\n\n";

LOG += processedResponseObject;
LOG += "\n\n";

//LOG += rawResponseObject;
LOG += "\n\n";

throw new Exception(LOG);

Try to do the same in your case instead of waiting for CCG log for ages :smiley:

I hope this helps you debug!

thanks!

we have an expert services ticket open.

Here is a summary of our findings:

  1. The Goal: We are trying to use a simple AfterOperationRule to transform data (e.g., lowercase emails) after aggregation.

  2. The Problem: The rule’s code is never reached. Even a diagnostic rule designed to throw new RuntimeException() does not cause the aggregation to fail.

  3. API is Validated: We have built a standalone Python script that successfully calls the API, handles pagination, and fetches all 102 accounts. The raw JSON from the API is correctly formatted and contains the expected data.

  4. Configuration is Validated: We have isolated the test to a single, standalone parent aggregation endpoint with no children. We have confirmed the rootPath is correct ($.items[*]) and have as a test, explicitly set the resMappingObj with the $. JSONPath prefixes (e.g., "loginId": "$.loginId").

  5. Rule Exception Flag is Set: We have confirmed via the API that the source attribute "throwBeforeAfterRuleException" is set to true.

To confirm - the after Operation rule is attached to the HTTP Operation? and the name is correct, no typo’s, no whitespace before or after name?

Yes, I’m quite confused by it.

If I put in a new aggregation in front of the current one, where there are no child requests, aggregation now shows double the number of accounts on the aggregation. This is to be expected as both are running.

The rule name is correct

If I use the rule below it doesn’t throw an exception and continues to aggregate all 102 /204 accounts. This rule should execute no matter what. I tried with a new rule as well, with no spaces in the name of the rule to no avail.

```
import java.util.Map;

import java.util.List;

public Map execute(List processedResponseObject) {

// This rule has no logic. Its only purpose is to throw an exception

// to prove whether the rule is being executed by the connector at all.



String LOG = "--- FINAL DIAGNOSTIC TEST --- \\n\\n";



if (processedResponseObject == null) {

    LOG += "RESULT: The 'processedResponseObject' was NULL. The rule was called, but with no data.";

} else if (processedResponseObject.isEmpty()) {

    LOG += "RESULT: The 'processedResponseObject' was EMPTY. The rule was called, but with no data.";

    LOG += "This confirms a silent failure in the connector's Response Mapping.";

} else {

    LOG += "RESULT: The rule was called with " + processedResponseObject.size() + " account(s).";

}



throw new RuntimeException(LOG);

}
```

Maybe it is your logging levels?

Try something like the following

import java.util.HashMap;
import java.util.Map;
import java.util.List;

log.error("--- FINAL DIAGNOSTIC TEST ---");
if (processedResponseObject != null){ 
    log.error("RESULT: The rule was called with " + processedResponseObject.size() + " account(s).");
}

Still no luck, thanks all for contributing.

We are moving forward with a correlation rule instead.

1 Like

This is working as a correlation rule, but if anybody has an example of something working as a WebServices after operation rule, I would be interested in understanding it.

```

import java.util.HashMap;

import java.util.Map;

Map returnMap = new HashMap();

String email = account.getStringAttribute(\“email\”).toLowerCase();

String loginId = account.getStringAttribute(\“loginId\”).toLowerCase();

String workEmail = \“email\”;

if (idn.countIdentitiesBySearchableIdentityAttribute(workEmail, \“Equals\”, email) > 0) {returnMap.put(\“identityAttributeName\”, workEmail);

returnMap.put(\\"identityAttributeValue\\", email);

} else if (idn.countIdentitiesBySearchableIdentityAttribute(workEmail, \“Equals\”, loginId) > 0) {returnMap.put(\“identityAttributeName\”, workEmail);

returnMap.put(\\"identityAttributeValue\\", loginId);

}

return returnMap;
```