Apex passing parameters to Batch job during scheduling

Here it the use case:

You have 1 job that you want to run weekly and monthly but the monthly job also has to generate a notification. You don’t need to create two classes but can pass a parameter to the schedule to know it is a weekly or monthly job.

The batch Job class that accepts constructor parameter jobRunFrequency
global class AccountBatchJob implements System.Schedulable, Database.Batchable<SObject>, Database.Stateful, Database.AllowsCallouts {

	private static final String RUN_WEEKLY_JOB = 'RUN_WEEKLY_JOB';
	//This is the key that needs to be used to generate notifications for Monthly 
	private static final String RUN_MONTHLY_JOB = 'RUN_MONTHLY_JOB';
	private String jobRunFrequency;

	String query = 'Select Id, FirstName, LastName from Account';

	public AccountBatchJob(){
		this.jobRunFrequency = RUN_WEEKLY_JOB;
	}

	public AccountBatchJob(String jobRunFrequency){
		this.jobRunFrequency = jobRunFrequency;
	}

        global Database.QueryLocator start(Database.BatchableContext BC) {
		return Database.getQueryLocator(query);
	}

    global void execute(Database.BatchableContext BC, List<Account> accounts) {
     ....
     if (RUN_MONTHLY_JOB.equalsIgnoreCase(jobRunFrequency)){
     ......
     }
    }
}

Test class for batch Job passing parameter check if it is the Monthly job

Test.startTest();
	Database.executeBatch(new AccountBatchJob('RUN_MONTHLY_JOB'), 1);
Test.stopTest();

Test class Scheduler Job to check it if is the Monthly job

Test.startTest();
	String jobId = System.schedule('AccountBatchJob', '0 0 0 15 3 ? 2022', new AccountBatchJob('RUN_MONTHLY_JOB'));
	CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];
	System.assertEquals('0 0 0 15 3 ? 2022', ct.CronExpression);
Test.stopTest();

Apex Cache Tracker to remove session cache

Here is the problem that needed to be solved:
1. Use session cache to cache some user information like their profile or user settings
2. When a external systems update the information and try to remove the session cache as it got updated it fails because it does not have access to a users session cache

Solution:
Build a session tracker in Org Cache to track missed removal of session cache

Step 1: When a session cache remove fails add it to the Org level tracker cache. The format of session keys are the following:
key + userId (Salesforce userId)

That way I can keep track of what userId’s I need to remove session cache
String splitStrKey = key.substring(0, key.length()-18);
String splitUserId = key.substring(key.length()-18, key.length());

will split into key and userId

public Boolean remove(String key) {
      if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
        Boolean removed = false;
        if (sessionCacheEnabled && sessionPartition!=null) {
              System.debug('inside session cache remove key: ' + key);
              removed = sessionPartition.remove(key);
              if (!removed && key!=null && key.length() > 18){
                String splitStrKey = key.substring(0, key.length()-18);
                String splitUserId = key.substring(key.length()-18, key.length());
                System.debug('Session remove failed key ' + splitStrKey + ' for ' + splitUserId + ' adding to tracker');
                removeFailedAddToTracker(splitStrKey, splitUserId, null);
              }
          } else if (orgPartition!=null){
              removed = orgPartition.remove(key);
          }

        if (removed) {
          System.debug(LoggingLevel.DEBUG, 'Removed key ' + key);
          return true;
        } else{
          System.debug(LoggingLevel.DEBUG, 'Not removed key not found ' + key);
          return false;
        }
      } else {
        System.debug(LoggingLevel.DEBUG, 'Skipping cache remove in batch ' + key);
        return false;
      }
    }

Step 2: Add session keys to Org level tracker cache (CACHE_USER_CACHE_TRACKER), we will add it as Map<Id, Map> where Map contains UserId as key and Map of all the session keys that needs to be removed

public void removeFailedAddToTracker(String keyPrefix, Id userId, Set<Id> clientIds){
      if (App_Service.isIntegrationUser(userId)){
        Map<String, String> mapOfContactUserIds = (Map<String, String>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CONTACT_MAP);
        Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
        if (clientIds!=null && !clientIds.isEmpty()){
          for (Id clientId : clientIds){
            if (mapOfContactUserIds!=null && mapOfContactUserIds.containsKey(clientId)){
              Id userIdToClearCache = mapOfContactUserIds.get(clientId);

              if (mapOfExistingCacheQueueTracker!=null){
                if (mapOfExistingCacheQueueTracker.containsKey(userIdToClearCache)){
                  Map<String, Id> existingCacheStrings = mapOfExistingCacheQueueTracker.get(userIdToClearCache);
                  existingCacheStrings.put(keyPrefix, userIdToClearCache);
                  mapOfExistingCacheQueueTracker.put(userIdToClearCache, existingCacheStrings);
                } else {
                  mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
                }
              } else {
                mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
                mapOfExistingCacheQueueTracker.put(userIdToClearCache, new Map<String, Id>{keyPrefix=>userIdToClearCache});
              }
            }
          }
        } else {
          if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(userId)){
            Map<String, Id> existingMap = mapOfExistingCacheQueueTracker.get(userId);
            existingMap.put(keyPrefix, userId);
            mapOfExistingCacheQueueTracker.put(userId, existingMap);
          } else {
            if (mapOfExistingCacheQueueTracker==null){
              mapOfExistingCacheQueueTracker = new Map<Id, Map<String, Id>>();
            }
            mapOfExistingCacheQueueTracker.put(userId, new Map<String, Id>{keyPrefix=>userId});
          }
        }

        if (mapOfExistingCacheQueueTracker!=null)
          getOrgPartition().put(App_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
      }
    }

Step 3: Every time before we check if session cache contains a key we will see if the same key is contained in the CACHE_USER_CACHE_TRACKER cache. If yes remove it from the users session cache so that cache can be removed and new query can be done

  public Boolean containsKey(String cacheKey){
      Boolean containsKey = false;
      if (!System.isBatch() && !System.isFuture() && !Test.isRunningTest()){
           if (sessionCacheEnabled==false && orgPartition!=null){
              if (orgPartition.get(cacheKey)!=null){
                containsKey = true;
              }
          } else if (sessionCacheEnabled && sessionPartition!=null) {
              if (sessionPartition.get(cacheKey)!=null){
                containsKey = true;

                Map<Id, Map<String, Id>> mapOfExistingCacheQueueTracker = (Map<Id, Map<String, Id>>)getOrgPartition().get(App_Rest_Constants.CACHE_USER_CACHE_TRACKER);
              	if (mapOfExistingCacheQueueTracker!=null && mapOfExistingCacheQueueTracker.containsKey(UserInfo.getUserId())){
            			Map<String, Id> flagsToClear = mapOfExistingCacheQueueTracker.get(UserInfo.getUserId());

                  Boolean removeFlag = false;
            			for (String flagToClear : flagsToClear.keySet()){
            				if (flagToClear.equalsIgnoreCase(cacheKey)){
                      String keyToRemove = flagToClear + flagsToClear.get(flagToClear);
            					Boolean removeItemFromCache = getSessionPartition().remove(keyToRemove);
                      
                      removeFlag = true;
                      containsKey = false;
            				}
            			}

                  if (removeFlag){
                    for (String flagToClear : flagsToClear.keySet()){
                      flagsToClear.remove(cacheKey);
                      if (flagsToClear.isEmpty() && flagsToClear.size()==0){
                        mapOfExistingCacheQueueTracker.remove(UserInfo.getUserId());
                      } else {
                        mapOfExistingCacheQueueTracker.put(UserInfo.getUserId(), flagsToClear);
                      }
                    }
                    
                    getOrgPartition().put(App_Rest_Constants.CACHE_USER_CACHE_TRACKER, mapOfExistingCacheQueueTracker);
                  }

            		}
              }
          }
       }
      return containsKey;
    }

Apex Coding Challenge Find Highest Frequency of Numbers

Problem: Find the number that has the highest frequency in a list of integers.

Input: 1,6,2,1,6,1

Output: 1 //because 1 occurs 3 times in the list

Option 1: Use Hashmap to iterate list

	List<Integer> nums = new List<Integer>{1,6,2,1,6,1};
		Map<Integer, Integer> numMap = new HashMap<>();
		for (Integer num : nums){
			if (numMap.containsKey(num)){
				Integer numFreq = numMap.get(num);
				numMap.put(num, numFreq+1);
			} else {
				numMap.put(num, 1);
			}
		}
		
		Integer biggestFreq = 0;
		Integer biggestVal = 0;
		for (Integer num : numMap.keySet()){
			if (numMap.get(num) > biggestFreq){
				biggestFreq = numMap.get(num);
				biggestVal = num;
			}
		}
		
		System.debug(biggestVal);

Option 2: Use wrapper class with compare to sort wrapper

List<Integer> nums = new List<Integer>{1,6,2,1,6,1};

Map<Integer, NumFrequencyWrapper> numMap = new HashMap<>();
for (Integer num : nums){
	if (numMap.containsKey(num)){
		NumFrequencyWrapper numFreqWrapper = numMap.get(num);
		numFreqWrapper.setFrequency(numFreqWrapper.getFrequency()+1);
		numMap.put(num, numFreqWrapper);
	} else {
		NumFrequencyWrapper numFrequencyWrapper = new NumFrequencyWrapper();
		numFrequencyWrapper.setNum(num);
		numFrequencyWrapper.setFrequency(1);
		numMap.put(num, numFrequencyWrapper);
	}
}
	
List<NumFrequencyWrapper> frequencyWrapperList = new List(numMap.values());
Collections.sort(frequencyWrapperList, new Untitled.NumFrequencyWrapperCompare());
System.debug(frequencyWrapperList.get(0).getNum());

public class NumFrequencyWrapper {
	private Integer num;
	private Integer frequency;
	
	public void setNum(Integer num){
		this.num = num;
	}
	
	public Integer getNum(){
		return num;
	}
	
	public void setFrequency(Integer frequency){
		this.frequency = frequency;
	}
	
	public Integer getFrequency(){
		return this.frequency;
	}
}
	
public class NumFrequencyWrapperCompare implements Comparator<NumFrequencyWrapper>{
	public int compare(NumFrequencyWrapper a, NumFrequencyWrapper b) { 
		return  b.getFrequency() - a.getFrequency(); 
	} 
}  

Option 3: Using buckets to group index of frequencies together

List<Integer> nums = new List<Integer>{1,6,2,1,6,1};
Integer returnNums = 2;
Map<Integer, Integer> numMap = new Map<Integer, Integer>();
for (Integer num : nums){
	if (numMap.containsKey(num)){
		Integer numFreq = numMap.get(num);
		numMap.put(num, numFreq+1);
	} else {
		numMap.put(num, 1);
	}
}

Map<Integer, List<Integer>> mapOfBucketWithValues = new Map<Integer, List<Integer>>();
for (Integer num : numMap.keySet()){
	Integer numFrequency = numMap.get(num);
	if (mapOfBucketWithValues.containsKey(numFrequency)){
		List<Integer> existingIndexNum = mapOfBucketWithValues.get(numFrequency);
		existingIndexNum.add(num);
		mapOfBucketWithValues.put(numFrequency, existingIndexNum);
	} else {
		List<Integer> numList = new ArrayList<>();
		numList.add(num);
		mapOfBucketWithValues.put(numFrequency, numList);
	}
}

for (Integer k=nums.size(), returnedNums=0; 1<=k; k--){
	if (mapOfBucketWithValues.containsKey(k)){
		for (Integer numBucket : mapOfBucketWithValues.get(k)){
			if (returnedNums < returnNums){
				System.debug(numBucket);
				returnedNums++;
			}
		}
	}	
}

Apex Clear all fields for a SObject record

The clearOutRecords would iterate all the fields passed as the currentRecord, then:
1. Exclude the fields as part of the fieldsToExcludeForClearOut Set and relationship fields
2. Check if the field is not null and updateable
3. Special logic to set fields to predefined values
4. Set all other fields to null
5. Return the SObject with fields as null


private static Set<String> fieldsToExcludeForClearOut = new Set<String>{'Cases', 'DoNotCall', 
'HasOptedOutOfFax', 'HasOptedOutOfEmail', 'LastName', 
'FirstName', 'Email', 'AccountId', 'CreatedDate',
'IsDeleted','Interval__c','OwnerId',
'OtherGeocodeAccuracy','MailingGeocodeAccuracy',
'BillingGeocodeAccuracy','ShippingGeocodeAccuracy'};

    public SObject clearOutRecords(SObject currentRecord, String sObjectName){
      SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
      DescribeSObjectResult objDef = objToken.getDescribe();
      Map<String, SObjectField> fieldsSobject = objDef.fields.getMap();
      Map<String, Object> fields = currentRecord.getPopulatedFieldsAsMap();
      Type classType = Type.forName(sObjectName);
      SObject mergedRecord = (SObject)JSON.deserialize('{}', classType);
      for (String field : fields.keySet()){
        if (!fieldsToExcludeForClearOut.contains(field) && !field.contains('__r')){
          if (currentRecord.get(field)!=null && fieldsSobject.get(field).getDescribe().isUpdateable()){
            if ('User_Status__c'.equals(field)){
              mergedRecord.put(field, 'Incomplete');
            } else if ('Is_Mail_Same_As_Home__c'.equals(field)){
              mergedRecord.put(field, false);
            } else {
              mergedRecord.put(field, null);
            }
            } else if ('Id'.equals(field)){
            mergedRecord.put(field, currentRecord.get(field));
          }
        }
      }
      return mergedRecord;
    }

Initializing the clearOutRecords method

1. Query the fields that you would like to clear
2. Pass the Object to the clearOutRecords method

Contact queryContact = [Select Id, FirstName, LastName, Email, Birthdate, MailingState, Age__c from Contact where MailingState!=null limit 1 ];

Contact clearedOutContact = (Contact)App_Service.instance.clearOutRecords(queryContact, 'Contact');
        

Output

Contact:{Id=0036300000TQZIwAAP, Birthdate=null, MailingState=null}

Salesforce Platform Events Streaming using Spring Boot

Create EmpConnector connection
The first thing is to create a configuration that on startup will connect to the EmpConnector and start listing for incoming events on a specific topic.

1. Set all your Bayeux Parameters (bayeuxParameters)
2. Create EmpConnector connection (empConnector)
3. Start listening to topic (startAndPublishAsyncEventToExchange)
4. Adding listeners to the topic (startAndPublishAsyncEventToExchange)

package com.app.core.events;

import com.app.core.service.IncomingRequestHandler;
import com.app.utils.AppPlanUtils;
import com.salesforce.emp.connector.BayeuxParameters;
import com.salesforce.emp.connector.EmpConnector;
import com.salesforce.emp.connector.TopicSubscription;
import com.salesforce.emp.connector.example.LoggingListener;
import org.cometd.bayeux.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import java.net.URL;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

import static com.salesforce.emp.connector.LoginHelper.login;

@Configuration
@ConfigurationProperties
@Lazy
public class SalesforceEventConfig {

    private static Logger itsLogger = LoggerFactory.getLogger(SalesforceEventConfig.class);

    @Value("${salesforce.username}")
    private String username;

    @Value("${salesforce.password}")
    private String password;

    @Value("${salesforce.token}")
    private String token;

    @Value("${salesforce.baseurl}")
    private String url;

    @Value("${salesforce.version:39.0}")
    private String version;

    @Autowired
    protected IncomingRequestHandler itsReqHandler;

    private static final Integer WAIT_TIME = 2;
    private static final Integer START_UP_WAIT_TIME = 5;
    private static final String EVENT_NAME = "App_Events__e";

    @Bean
    public BayeuxParameters bayeuxParameters() throws Exception {
        String theUrlEnv = AppUtils.getConfigVar("SALESFORCE_URL");
        String passwordAndToken = password + token;
        if (theUrlEnv != null) {
            url = getNextToken(0, theUrlEnv);
            username = getNextToken(url.length() + version.length() + 2, theUrlEnv);
            passwordAndToken = theUrlEnv.substring(url.length() + version.length() + username.length() + 3);
            itsLogger.info("Found SALESFORCE_URL to parse. url={}, version={}, username={}", url, version, username);
            url = url.split("/services/Soap/u/")[0];
        }

        BayeuxParameters bayeuxParams = getBayeuxParamWithSpecifiedAPIVersion(version);
        BayeuxParameters bayeuxParameters = login(new URL(url),username, passwordAndToken, bayeuxParams);
        return bayeuxParameters;
    }

    @Bean
    public EmpConnector empConnector(BayeuxParameters bayeuxParameters){
        itsLogger.debug("BayeuxParameters url {} version {}", bayeuxParameters.host(), bayeuxParameters.version());
        EmpConnector empConnector = new EmpConnector(bayeuxParameters);
        itsLogger.debug("EmpConnector isConnected {} isHandshook {}", empConnector.isConnected(), empConnector.isHandshook());
        return empConnector;
    }

    private static BayeuxParameters getBayeuxParamWithSpecifiedAPIVersion(String apiVersion) {
        BayeuxParameters params = new BayeuxParameters() {

            @Override
            public String version() {
                return apiVersion;
            }

            @Override
            public String bearerToken() {
                return null;
            }

        };
        return  params;
    }

    private String getNextToken(int startIdx, String origStr) {
        int theIdx = origStr.indexOf("|", startIdx);
        if (theIdx > 0) {
            return origStr.substring(startIdx, theIdx);
        } else {
            return origStr.substring(startIdx);
        }
    }

    @Bean
    public TopicSubscription startAndPublishAsyncEventToExchange(EmpConnector empConnector)  {
        TopicSubscription subscription = null;
        try {
            long replayFrom = EmpConnector.REPLAY_FROM_TIP;
            itsLogger.debug("Setup event consumer with replyFrom {}", replayFrom);

            SalesforceEventPayload eventPayload = new SalesforceEventPayload();
            Consumer<Map<String, Object>> consumer = event -> {
                eventPayload.setPayload(event);
                itsReqHandler.handleRequest(eventPayload.getPayload());
            };

            LoggingListener loggingListener = new LoggingListener(true, true);
            itsLogger.debug("Adding event listeners");
            empConnector.addListener(Channel.META_CONNECT, loggingListener)
                    .addListener(Channel.META_HANDSHAKE, loggingListener)
                    .addListener(Channel.META_DISCONNECT, loggingListener)
                    .addListener(Channel.META_SUBSCRIBE, loggingListener)
                    .addListener(Channel.META_UNSUBSCRIBE, loggingListener)
                    .addListener(Channel.META_DISCONNECT, loggingListener)
                    .addListener(Channel.SERVICE, loggingListener);

            itsLogger.debug("Starting Event Bus");
            empConnector.start().get(START_UP_WAIT_TIME, TimeUnit.SECONDS);

            itsLogger.debug("Subscribing to event {}", EVENT_NAME);
            subscription = empConnector.subscribe("/event/" + EVENT_NAME, replayFrom, consumer).get(WAIT_TIME, TimeUnit.SECONDS);

            itsLogger.debug(String.format("Subscribed: %s", subscription));

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }

        return subscription;
    }
}

Basic POJO to serialize events received for a specific topic

Note: as the ‘payload’ tag has some extra tags I remove them before serializing the payload to POJO.

package com.app.core.events;

import com.app.db.model.UserServiceRequestEvent;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.Serializable;
import java.util.Map;

public class SalesforceEventPayload implements Serializable{

    private static final String SALESFORCE_EVENT_KEY = "Event_Data__c=";

    private static Logger itsLogger = LoggerFactory.getLogger(SalesforceEventPayload.class);

    public SalesforceEventPayload() {}

    private UserServiceRequestEvent payload;

    public UserServiceRequestEvent getPayload() {
        return payload;
    }

    public void setPayload(Map<String, Object> fieldMappings) {
        ObjectMapper theMapper = new ObjectMapper();
        theMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        theMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
        theMapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true);
        theMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
        theMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
        if (fieldMappings.containsKey("payload")){
            String eventPayload = fieldMappings.get("payload").toString();
            String splitEventString = eventPayload.toString().split(SALESFORCE_EVENT_KEY)[1];
            String removeLastTwo = splitEventString.substring(1, splitEventString.length() - 2);
            String unescapeJson = StringEscapeUtils.unescapeJson(removeLastTwo);
            try{
                payload = theMapper.readValue(unescapeJson, UserServiceRequestEvent.class);
            } catch (JsonParseException e) {
                itsLogger.error("Event json parse exception {}", e.getMessage());
            } catch (JsonMappingException e) {
                itsLogger.error("Event json mapping exception {}", e.getMessage());
            } catch (IOException e) {
                itsLogger.error("Event io exception {}", e.getMessage());
            }
        }
    }
}

Apex remove sensitive data from json

When you need to remove sensitive data from json before logging the following method will remove a predefined list of keywords

String bodyArgs = '{"name":"test", "ssn":"324234234", "email":"test@mail.com"}';
Object bodyObj = (Object)JSON.deserializeUntyped(bodyArgs);

Map<String, Object> mapObj = new Map<String, Object>();
if (bodyObj instanceof List<Object>){
	List<Object> lstObjs = (List<Object>)JSON.deserializeUntyped(bodyArgs);
    for (Object lstObj : lstObjs){
       Map<String,Object> parseLstObj = (Map<String,Object>)JSON.deserializeUntyped(JSON.serialize(lstObj));
       mapObj.putAll(parseLstObj);
    }
} else {
	mapObj = (Map<String,Object>)JSON.deserializeUntyped(bodyArgs);
}

Map<String, String> newMappedValues = new Map<String, String>
System.debug(removeAttributes(mapObj, newMappedValues));
>>> Output: '{"name":"test"}'

removeAttributes method will iterate all the keys in the payload and remove the sensitive keys from the payload

private Set<String> removeSensitiveKeyValue = new Set<String>{'ssn', 'email', 'dob'};

public Map<String, String> removeAttributes(Map<String,Object> jsonObj, Map<String, String> mappedKeys)  {
	for(String key : jsonObj.keySet()) {
		if (removeSensitiveKeyValue.contains(key)){
			jsonObj.remove(key);
		} else {
	      if(jsonObj.get(key) instanceof Map<String,Object>) {
	          removeAttributes((Map<String,Object>)jsonObj.get(key), mappedKeys);
	      } else if(jsonObj.get(key) instanceof List<Object>) {
	          for(Object listItem : (List<Object>)jsonObj.get(key)) {
	           if(listItem instanceof Map<String,Object>)  {
	        	removeAttributes((Map<String,Object>)listItem, mappedKeys);
	           }
	         }
	      } else {
			mappedKeys.put(key, String.valueOf(jsonObj.get(key)));
		  }
	  	}
	}
	return mappedKeys;
}

Apex Coding Challenge for Iterating Lists

Here is the problem statement:

Given an expectedSum value find the sum (n+ (n+1) = expectedValue) from a list that equals the expectedSum. I am using the following list

1,3,4,4,5,9

and looking for the sum to be 8 (3+5, 4+4)

*Note the list is sorted

Solution 1:


        Integer expectedSum = 8;
		List<Integer> listInts = Arrays.asList(1,3,4,4,5,9);
		
		for (Integer k=0; k<listInts.size();k++){
			for (Integer j=k+1; j<listInts.size();j++){
				if (listInts.get(k)+listInts.get(j)==expectedSum){
					System.debug(listInts.get(k) + ' + ' + listInts.get(j));
				}
			}
		}	

The above time complexity for a nested for loop is O(n^2)

Solution 2:

Do a binary search to search if the diff is contained in the list


		Integer expectedSum = 8;
		List<Integer> listInts = Arrays.asList(1,3,4,4,5,9);
		for (Integer listInt : listInts){ 
			Integer diffInt = expectedSum-listInt;
			if (binarySearch(listInts, diffInt)!=null){
				System.debug(listInt + " + " + diffInt);
			}
		}
	
	
	public static Integer binarySearch(List<Integer> listInts, Integer searchInt){
		Integer startPos = 0;
		Integer listSize = listInts.size() - 1;
		Integer mid;
		while(startPos <= listSize){
			mid=(startPos+listSize)/2;
			if(listInts.get(mid) == searchInt){
				 return listInts.get(mid);
			}     
			else if(searchInt < listInts.get(mid)){
				listSize = mid - 1;
			}    
			else{
				startPos = mid + 1;
			}
		}
		return null;	 
	}

the above time complexity for binary search is for each element in the array O(n log n) Returning null is not a good practice try to return another number or throw an exception.

Solution 3:

Check if the diff is contained in the list by using the .contains method


		Integer expectedSum = 8;
		List<Integer> listInts = Arrays.asList(1,3,4,4,5,9);

		for (Integer listInt : listInts){
			Integer diffInt =expectedSum-listInt;
			if (listInts.contains(diffInt)){
				System.debug(listInt + ' + ' + diffInt);
			}
		}

the above time complexity for loop is O(n)

Option 4

Start on either end of the array and move inwards when you find a solution, if the sum of the outer and inner element is bigger that the expectedSum move the maxPointer inwards, if it is smaller move the minPointer inwards.

        Integer expectedSum = 1;
		List<Integer> listInts = Arrays.asList(1,3,4,4,5,9);

		Integer maxPointer = listInts.size()-1;
		Integer minPointer = 0;
		for (Integer k=0; k<listInts.size();k++){
			if (expectedSum < listInts.get(maxPointer)){
				maxPointer-=1;
			} else if (minPointer!=maxPointer){
				Integer sumPair =listInts.get(minPointer) + listInts.get(maxPointer);
				if ( sumPair == expectedSum){
					System.debug(listInts.get(minPointer) + " + " + listInts.get(maxPointer));
					minPointer +=1;
					maxPointer-=1;
				} else if (sumPair < expectedSum){
					minPointer +=1;
				} else {
					maxPointer-=1;;
				}
			}
			
		}

the above time complexity for loop is O(n)

Output:

4 + 4
5 + 3
3 + 5

Bonus:

How to do it with an unsorted list:

Iterate through the list and add the integer to the list, if the diff of the expected sum and integer is contained in the set then print it

		Integer expectedSum = 8;
		List<Integer> listInts = Arrays.asList(7,4,6,1,5,2,3);

		Set<Integer> intSetWithDiff = new HashSet<>();
		for (Integer listInt : listInts){
			Integer sumDiff = expectedSum - listInt;
			
			if (intSetWithDiff.contains(sumDiff)){
				System.debug(listInt + " + " + sumDiff);
			}
			intSetWithDiff.add(listInt);		
		}

Output:

1 + 7
2 + 6
3 + 5

Salesforce Object Level and Field Level Security Architecture

The Salesforce Security review require that both:

  1. Object Level (OLS)
  2. Field Level (FLS)

Security is applied for the following areas:

  1. Query of data – Selectors
  2. Triggers – Domains
  3. DML – Unit of Work

To check both (OLS) and FLS we can use fflib_SecurityUtils it has all the to methods to check if a user can access an object and also individual fields for that object. An Apex exception is thrown if the user does not have access to the given object and/or fields.

Example is when a community user tries to create an Account the following error occurs if OLS is enabled

fflib_SecurityUtils.CrudException: You do not have permission to insert Account
Class.fflib_SecurityUtils.checkObjectIsInsertable: line 309, column 1
Class.fflib_SObjectUnitOfWork.insertDmlByType: line 583, column 1
Class.fflib_SObjectUnitOfWork.doCommitWork: line 531, column 1
Class.fflib_SObjectUnitOfWork.commitWork: line 509, column 1
Class.App_Service_Test.testSecurityUOW: line 81, column 1

  1. Query of data – Selectors

The key purpose of this class is to make building dynamic SOQL queries safer and more robust than traditional string concatenation or String.format approaches. It also has an option to automatically check read security for the objects and fields given to it. Here are some of the methods we use to dynamically build all our queries.

Enable FLS check for queries before they run, by setting setEnforceFLS(true)

OR set the OLS and FLS when constructing a new Selector so it will be implement for all queries

OpportunitiesSelector oppsSelector = 
new OpportunitiesSelector(includeFieldSetFields, enforceObjectSecurity, enforceFLS);

2. Triggers – Domains

Domain classes has minimal functionality in it other than the routing of trigger events to the applicable virtual methods and object security enforcement.

Domain classes by OLS by default as part of it’s Configuration class:

3. DML – Unit of Work

This is not currently implemented to check the following:

1. Insert

  if (enforceOLS){
       fflib_SecurityUtils.checkObjectIsInsertable(sObjectType);
  }

2. Update

 if (enforceOLS){
       fflib_SecurityUtils.checkObjectIsUpdateable(sObjectType);
  }


3. Delete 

if (enforceOLS){
     fflib_SecurityUtils.checkObjectIsDeletable(m_sObjectTypes[objectIdx--]);
  }

Disadvantages of OLS and FLS implemented across your app

OLS – this is a check that is done to see if the user profile has access to do CRUD on the specific object. If this check is not done and user tries to do any of the CRUD operations it will just result in a Salesforce error. Doing this check on an object level is not too big of an overhead. If these checks are not done, Permission errors will be thrown as the profile does not have the correct permission.

FLS – this is not recommended as we have to iterate through every field of the object and check if the user has the privileges to read/update/insert/delete to that field. This slows down operations down drastically and only needs to be used in a few uses cases.

Possible solutions run security checks on app

Have a way to enable both OLS and FLS in your code then run through all your test and see if any break. If the break GREAT fix your Object Settings/FLS so that they work. When running in Production have a way to disable this as it may/will slow down your operations/services.

Apex Interview Question > FizzBuzz

Consider the following problem:

Write a short program that prints each number from 1 to 100 in one line

For each multiple of 3, print “Fizz” instead of the number.

For each multiple of 5, print “Buzz” instead of the number.

For numbers which are multiples of both 3 and 5, print “FizzBuzz” instead of the number.
Write a solution (or reduce an existing one) so it has as few characters as possible.

Solution 1: For loop with multiple if statements

String str = '';
for(Integer i = 1; i&amp;lt;=100; i++){
    if(Math.mod(i,3)==0)
        str+='Fizz';
    if(Math.mod(i,5)==0)
        str+='Buzz';
    else if(Math.mod(i, 3) != 0)
		str+=i;
    str +=',';
}
System.debug(str);

Output

1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,
Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,31,32,Fizz,34,Buzz,
Fizz,37,38,Fizz,Buzz,41,Fizz,43,44,FizzBuzz,46,47,Fizz,49,Buzz,
Fizz,52,53,Fizz,Buzz,56,Fizz,58,59,FizzBuzz,61,62,Fizz,64,Buzz,
Fizz,67,68,Fizz,Buzz,71,Fizz,73,74,FizzBuzz,76,77,Fizz,79,Buzz,
Fizz,82,83,Fizz,Buzz,86,Fizz,88,89,FizzBuzz,91,92,Fizz,94,Buzz,
Fizz,97,98,Fizz,Buzz

Apex FeedItem Trigger Share to Community

When uploading a new FeedItem we want to share it to a specific community. First thing we need to do is share the FeedItem to a community by sharing it to the User. As the documentation states: Only feed items with a Group or User parent can set a NetworkId or a null value for NetworkScope.

https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_feeditem.htm

NetworkId—The ID of the community in which the FeedItem is available. If left empty, the feed item is only available in the default community.

FeedItemTrigger Trigger

trigger FeedItemTrigger on FeedItem (after insert, before insert) {
    fflib_SObjectDomain.triggerHandler(App_Domain_FeedItem.class);
}

FeedItemTrigger Domain Class

public class App_Domain_FeedItem extends fflib_SObjectDomain {

	public override void onBeforeInsert(){
		List<ContentDocumentLink> contentDocumentLinksList = new List<ContentDocumentLink>();
		for(FeedItem record : (List<FeedItem>) Records){
			record.Body = record.ParentId;
			record.NetworkScope = {NetworkId};
			record.ParentId = UserInfo.getUserId();
			record.Visibility='AllUsers';
		}
	}
}

Now that the file is shared with the user we can give View sharing back to the original object using ContentDocumentLink by using the RelatedRecordId

public class App_Domain_FeedItem extends fflib_SObjectDomain {
public override void onAfterInsert(){
		List<ContentDocumentLink> contentDocumentLinksList = new List<ContentDocumentLink>();
		List<Documents__c> bpDocumentsList = new List<Documents__c>();

		Map<Id, Id> feedItemToRelatedRecordIdMap = new Map<Id, Id>();
		for(FeedItem record : (List<FeedItem>) Records){
			feedItemToRelatedRecordIdMap.put(record.Id, record.RelatedRecordId);
		}

		Map<Id, Id> mapOfRelatedRecordContentVersionMap = new Map<Id, Id>();
		List<ContentVersion> contentDocumentVersions = [Select Id, ContentDocumentId from ContentVersion where id IN :feedItemToRelatedRecordIdMap.values()];
		for (ContentVersion contentDocumentVersion : contentDocumentVersions){
			mapOfRelatedRecordContentVersionMap.put(contentDocumentVersion.Id, contentDocumentVersion.ContentDocumentId);
		}

		for(FeedItem record : (List<FeedItem>) Records){
			String bpDocumentId = record.Body;

			Id relatedRecordId = feedItemToRelatedRecordIdMap.get(record.Id);
			Id contentDocumentId = null;
			if (mapOfRelatedRecordContentVersionMap.containsKey(relatedRecordId)){
				 contentDocumentId = mapOfRelatedRecordContentVersionMap.get(relatedRecordId);
			}

			if (contentDocumentId!=null){
				ContentDocumentLink cdl = new ContentDocumentLink(LinkedEntityId = bpDocumentId , ContentDocumentId=contentDocumentId	, shareType = 'V');
				contentDocumentLinksList.add(cdl);
			}

			Documents__c updateDocument = new Documents__c(Id=bpDocumentId);
			updateDocument.Document_Status__c='Downloaded';
			String formatDownloadUrl = '/CommunityApi/sfc/servlet.shepherd/version/download/{0}?asPdf=false&operationContext=CHATTER';
			updateDocument.Download_Url__c = String.format(formatDownloadUrl, new List<String>{contentDocumentId});
			updateDocument.File_Id__c = contentDocumentId;
			updateDocument.File_Name__c = record.Title;
			updateDocument.File_Size__c = record.ContentSize;
			updateDocument.File_Type__c = record.ContentType!=null ? mimeTypeMap.containsKey(record.ContentType.toLowerCase()) ? mimeTypeMap.get(record.ContentType.toLowerCase()) : record.ContentType  : record.ContentType;
			updateDocument.File_Permission__c='R';

			bpDocumentsList.add(updateDocument);
		}

		if (!contentDocumentLinksList.isEmpty()){
			insert contentDocumentLinksList;
		}

		if (!bpDocumentsList.isEmpty()){
			update bpDocumentsList;
		}
	}
}

Now the Feeditem is shared both to the user and the object. It can be downloaded by the logged in user.